diff --git a/assets/documentation/Architecture.drawio b/assets/documentation/Architecture.drawio index 9a3d86f..e830161 100644 --- a/assets/documentation/Architecture.drawio +++ b/assets/documentation/Architecture.drawio @@ -1,4 +1,4 @@ - + @@ -1439,7 +1439,7 @@ - + @@ -1501,7 +1501,7 @@ - + @@ -2043,6 +2043,11 @@ + + + + + @@ -2061,55 +2066,95 @@ - + - + - + - + - - - - + - - - - - - + - + - + - + - + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Managing.Api.Workers/Program.cs b/src/Managing.Api.Workers/Program.cs index 20d39be..cc91910 100644 --- a/src/Managing.Api.Workers/Program.cs +++ b/src/Managing.Api.Workers/Program.cs @@ -7,6 +7,7 @@ using Managing.Common; using Managing.Core.Middleawares; using Managing.Infrastructure.Databases.InfluxDb.Models; using Managing.Infrastructure.Databases.MongoDb.Configurations; +using Managing.Infrastructure.Evm.Models.Privy; using Microsoft.OpenApi.Models; using NSwag; using NSwag.Generation.Processors.Security; @@ -49,6 +50,7 @@ builder.Host.UseSerilog((hostBuilder, loggerConfiguration) => builder.Services.AddOptions(); builder.Services.Configure(builder.Configuration.GetSection(Constants.Databases.MongoDb)); builder.Services.Configure(builder.Configuration.GetSection(Constants.Databases.InfluxDb)); +builder.Services.Configure(builder.Configuration.GetSection(Constants.ThirdParty.Privy)); builder.Services.AddControllers().AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())); builder.Services.AddCors(o => o.AddPolicy("CorsPolicy", builder => diff --git a/src/Managing.Api/Program.cs b/src/Managing.Api/Program.cs index d3511c6..fbcc1e4 100644 --- a/src/Managing.Api/Program.cs +++ b/src/Managing.Api/Program.cs @@ -9,6 +9,7 @@ using Managing.Bootstrap; using Managing.Common; using Managing.Infrastructure.Databases.InfluxDb.Models; using Managing.Infrastructure.Databases.MongoDb.Configurations; +using Managing.Infrastructure.Evm.Models.Privy; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; @@ -58,6 +59,7 @@ builder.Host.UseSerilog((hostBuilder, loggerConfiguration) => builder.Services.AddOptions(); builder.Services.Configure(builder.Configuration.GetSection(Constants.Databases.MongoDb)); builder.Services.Configure(builder.Configuration.GetSection(Constants.Databases.InfluxDb)); +builder.Services.Configure(builder.Configuration.GetSection(Constants.ThirdParty.Privy)); builder.Services.AddControllers().AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())); diff --git a/src/Managing.Api/appsettings.Oda.json b/src/Managing.Api/appsettings.Oda.json index bc7818a..12661e9 100644 --- a/src/Managing.Api/appsettings.Oda.json +++ b/src/Managing.Api/appsettings.Oda.json @@ -8,6 +8,10 @@ "Organization": "managing-org", "Token": "Fw2FPL2OwTzDHzSbR2Sd5xs0EKQYy00Q-hYKYAhr9cC1_q5YySONpxuf_Ck0PTjyUiF13xXmi__bu_pXH-H9zA==" }, + "Privy": { + "AppId": "cm6f47n1l003jx7mjwaembhup", + "AppSecret": "63Chz2z5M8TgR5qc8dznSLRAGTHTyPU4cjdQobrBF1Cx5tszZpTuFgyrRd7hZ2k6HpwDz3GEwQZzsCqHb8Z311bF" + }, "Serilog": { "MinimumLevel": { "Default": "Information", diff --git a/src/Managing.Application.Abstractions/Repositories/IEvmManager.cs b/src/Managing.Application.Abstractions/Repositories/IEvmManager.cs index 88a54e1..0d528f2 100644 --- a/src/Managing.Application.Abstractions/Repositories/IEvmManager.cs +++ b/src/Managing.Application.Abstractions/Repositories/IEvmManager.cs @@ -18,21 +18,37 @@ public interface IEvmManager string VerifySignature(string signature, string message); Task> GetBalances(Chain chain, int page, int pageSize, string publicAddress); Task> GetAllBalancesOnAllChain(string publicAddress); - Task> GetCandles(SubgraphProvider subgraphProvider, Ticker ticker, DateTime startDate, Timeframe interval); + + Task> GetCandles(SubgraphProvider subgraphProvider, Ticker ticker, DateTime startDate, + Timeframe interval); + decimal GetVolume(SubgraphProvider subgraphProvider, Ticker ticker); Task> GetAvailableTicker(); Task GetCandle(SubgraphProvider subgraphProvider, Ticker ticker); Task InitAddress(string chainName, string publicAddress, string privateKey); - Task Send(Chain chain, Ticker ticker, decimal amount, string publicAddress, string privateKey, string receiverAddress); + + Task Send(Chain chain, Ticker ticker, decimal amount, string publicAddress, string privateKey, + string receiverAddress); + Task GetTokenBalance(string chainName, Ticker ticker, string publicAddress); Task CancelOrders(Account account, Ticker ticker); - Task IncreasePosition(Account account, Ticker ticker, TradeDirection direction, decimal price, decimal quantity, decimal? leverage = 1); + + Task IncreasePosition(Account account, Ticker ticker, TradeDirection direction, decimal price, + decimal quantity, decimal? leverage = 1); + Task GetTrade(Account account, string chainName, Ticker ticker); - Task DecreasePosition(Account account, Ticker ticker, TradeDirection direction, decimal price, decimal quantity, decimal? leverage); + + Task DecreasePosition(Account account, Ticker ticker, TradeDirection direction, decimal price, + decimal quantity, decimal? leverage); + Task QuantityInPosition(string chainName, string publicAddress, Ticker ticker); - Task DecreaseOrder(Account account, TradeType tradeType, Ticker ticker, TradeDirection direction, decimal price, decimal quantity, decimal? leverage); + + Task DecreaseOrder(Account account, TradeType tradeType, Ticker ticker, TradeDirection direction, + decimal price, decimal quantity, decimal? leverage); + Task GetFee(string chainName); Task> GetOrders(Account account, Ticker ticker); Task GetTrade(string reference, string arbitrum, Ticker ticker); Task> GetFundingRates(); -} + Task<(string Id, string Address)> CreatePrivyWallet(); +} \ No newline at end of file diff --git a/src/Managing.Application/Accounts/AccountService.cs b/src/Managing.Application/Accounts/AccountService.cs index 02e9106..e58e8cb 100644 --- a/src/Managing.Application/Accounts/AccountService.cs +++ b/src/Managing.Application/Accounts/AccountService.cs @@ -7,6 +7,7 @@ using Managing.Domain.Users; using Microsoft.Extensions.Logging; namespace Managing.Application.Accounts; + public class AccountService : IAccountService { private readonly IAccountRepository _accountRepository; @@ -48,6 +49,21 @@ public class AccountService : IAccountService request.Key = keys.Key; request.Secret = keys.Secret; } + else if (request.Exchange == Enums.TradingExchanges.Evm + && request.Type == Enums.AccountType.Privy) + { + if (string.IsNullOrEmpty(request.Key) || string.IsNullOrEmpty(request.Secret)) + { + var privyClient = await _evmManager.CreatePrivyWallet(); + request.Key = privyClient.Address; + request.Secret = privyClient.Id; + } + else + { + request.Key = request.Key; // Address + request.Secret = request.Secret; // Privy wallet id + } + } else { request.Key = request.Key; @@ -116,10 +132,8 @@ public class AccountService : IAccountService { var cacheKey = $"user-account-{user.Name}"; - return _cacheService.GetOrSave(cacheKey, () => - { - return GetAccounts(user, hideSecrets, false); - }, TimeSpan.FromMinutes(5)); + return _cacheService.GetOrSave(cacheKey, () => { return GetAccounts(user, hideSecrets, false); }, + TimeSpan.FromMinutes(5)); } private IEnumerable GetAccounts(User user, bool hideSecrets, bool getBalance) @@ -139,10 +153,8 @@ public class AccountService : IAccountService public IEnumerable GetAccountsBalancesByUser(User user, bool hideSecrets) { var cacheKey = $"user-account-balance-{user.Name}"; - var accounts = _cacheService.GetOrSave(cacheKey, () => - { - return GetAccounts(user, true, true); - }, TimeSpan.FromHours(3)); + var accounts = _cacheService.GetOrSave(cacheKey, () => { return GetAccounts(user, true, true); }, + TimeSpan.FromHours(3)); return accounts; } diff --git a/src/Managing.Bootstrap/ApiBootstrap.cs b/src/Managing.Bootstrap/ApiBootstrap.cs index 74f19d7..c55f4e7 100644 --- a/src/Managing.Bootstrap/ApiBootstrap.cs +++ b/src/Managing.Bootstrap/ApiBootstrap.cs @@ -31,6 +31,8 @@ using Managing.Infrastructure.Databases.MongoDb; using Managing.Infrastructure.Databases.MongoDb.Abstractions; using Managing.Infrastructure.Databases.MongoDb.Configurations; using Managing.Infrastructure.Evm; +using Managing.Infrastructure.Evm.Abstractions; +using Managing.Infrastructure.Evm.Models.Privy; using Managing.Infrastructure.Evm.Services; using Managing.Infrastructure.Evm.Subgraphs; using Managing.Infrastructure.Exchanges; @@ -90,6 +92,9 @@ public static class ApiBootstrap services.AddSingleton(sp => sp.GetRequiredService>().Value); + services.AddSingleton(sp => + sp.GetRequiredService>().Value); + // Evm services.AddGbcFeed(); services.AddUniswapV2(); @@ -129,6 +134,7 @@ public static class ApiBootstrap services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddTransient(); // Stream services.AddSingleton(); diff --git a/src/Managing.Bootstrap/WorkersBootstrap.cs b/src/Managing.Bootstrap/WorkersBootstrap.cs index 99af16e..4cda135 100644 --- a/src/Managing.Bootstrap/WorkersBootstrap.cs +++ b/src/Managing.Bootstrap/WorkersBootstrap.cs @@ -25,6 +25,8 @@ using Managing.Infrastructure.Databases.MongoDb; using Managing.Infrastructure.Databases.MongoDb.Abstractions; using Managing.Infrastructure.Databases.MongoDb.Configurations; using Managing.Infrastructure.Evm; +using Managing.Infrastructure.Evm.Abstractions; +using Managing.Infrastructure.Evm.Models.Privy; using Managing.Infrastructure.Evm.Services; using Managing.Infrastructure.Evm.Subgraphs; using Managing.Infrastructure.Exchanges; @@ -81,6 +83,9 @@ public static class WorkersBootstrap services.AddTransient(); + services.AddSingleton(sp => + sp.GetRequiredService>().Value); + // Evm services.AddUniswapV2(); services.AddGbcFeed(); @@ -112,6 +117,7 @@ public static class WorkersBootstrap services.AddTransient(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Messengers services.AddSingleton(); diff --git a/src/Managing.Common/Constants.cs b/src/Managing.Common/Constants.cs index 5209e2d..d268758 100644 --- a/src/Managing.Common/Constants.cs +++ b/src/Managing.Common/Constants.cs @@ -23,6 +23,11 @@ public const string MongoDb = "ManagingDatabase"; } + public class ThirdParty + { + public const string Privy = "Privy"; + } + public class Chains { public const string Ethereum = "Ethereum"; diff --git a/src/Managing.Common/Enums.cs b/src/Managing.Common/Enums.cs index 214438a..312ace6 100644 --- a/src/Managing.Common/Enums.cs +++ b/src/Managing.Common/Enums.cs @@ -37,7 +37,8 @@ public static class Enums Cex, Trader, Watch, - Auth + Auth, + Privy } public enum BotType @@ -190,10 +191,10 @@ public static class Enums Cancelled = 2, Filled = 3, } - - public static bool IsActive(this TradeStatus status) => - status == TradeStatus.Requested || - status == TradeStatus.Cancelled || + + public static bool IsActive(this TradeStatus status) => + status == TradeStatus.Requested || + status == TradeStatus.Cancelled || status == TradeStatus.Filled; diff --git a/src/Managing.Infrastructure.Web3/Abstractions/IPrivyService.cs b/src/Managing.Infrastructure.Web3/Abstractions/IPrivyService.cs new file mode 100644 index 0000000..182e835 --- /dev/null +++ b/src/Managing.Infrastructure.Web3/Abstractions/IPrivyService.cs @@ -0,0 +1,11 @@ +using Managing.Infrastructure.Evm.Models; + +namespace Managing.Infrastructure.Evm.Abstractions; + +public interface IPrivyService +{ + Task CreateWalletAsync(string chainType = "ethereum"); + + Task SendTransactionAsync(string walletId, string recipientAddress, long value, + string caip2 = "eip155:84532"); +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Web3/Abstractions/IPrivySettings.cs b/src/Managing.Infrastructure.Web3/Abstractions/IPrivySettings.cs new file mode 100644 index 0000000..4ec54e2 --- /dev/null +++ b/src/Managing.Infrastructure.Web3/Abstractions/IPrivySettings.cs @@ -0,0 +1,7 @@ +namespace Managing.Infrastructure.Evm.Abstractions; + +public interface IPrivySettings +{ + string AppId { get; set; } + string AppSecret { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Web3/EvmManager.cs b/src/Managing.Infrastructure.Web3/EvmManager.cs index 6409170..6e45b4c 100644 --- a/src/Managing.Infrastructure.Web3/EvmManager.cs +++ b/src/Managing.Infrastructure.Web3/EvmManager.cs @@ -37,6 +37,7 @@ public class EvmManager : IEvmManager private readonly IEnumerable _subgraphs; private Dictionary> _geckoPrices; private readonly GmxV2Service _gmxV2Service; + private readonly IPrivyService _privyService; private readonly List _eligibleTickers = new List() { @@ -44,12 +45,13 @@ public class EvmManager : IEvmManager Ticker.PEPE, Ticker.DOGE, Ticker.UNI }; - public EvmManager(IEnumerable subgraphs) + public EvmManager(IEnumerable subgraphs, IPrivyService privyService) { var defaultChain = ChainService.GetEthereum(); _web3 = new Web3(defaultChain.RpcUrl); _httpClient = new HttpClient(); _subgraphs = subgraphs; + _privyService = privyService; _geckoPrices = _geckoPrices != null && _geckoPrices.Any() ? _geckoPrices : new Dictionary>(); @@ -657,4 +659,11 @@ public class EvmManager : IEvmManager var chain = ChainService.GetChain(Constants.Chains.Arbitrum); return new Web3(wallet, chain.RpcUrl); } + + + public async Task<(string Id, string Address)> CreatePrivyWallet() + { + var privyWallet = await _privyService.CreateWalletAsync(); + return (privyWallet.Id, privyWallet.Address); + } } \ No newline at end of file diff --git a/src/Managing.Infrastructure.Web3/Models/Privy/PrivySettings.cs b/src/Managing.Infrastructure.Web3/Models/Privy/PrivySettings.cs new file mode 100644 index 0000000..c64d313 --- /dev/null +++ b/src/Managing.Infrastructure.Web3/Models/Privy/PrivySettings.cs @@ -0,0 +1,9 @@ +using Managing.Infrastructure.Evm.Abstractions; + +namespace Managing.Infrastructure.Evm.Models.Privy; + +public class PrivySettings : IPrivySettings +{ + public string AppId { get; set; } + public string AppSecret { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Web3/Models/Privy/PrivyWallet.cs b/src/Managing.Infrastructure.Web3/Models/Privy/PrivyWallet.cs new file mode 100644 index 0000000..71205de --- /dev/null +++ b/src/Managing.Infrastructure.Web3/Models/Privy/PrivyWallet.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Managing.Infrastructure.Evm.Models; + +public class PrivyWallet +{ + [JsonPropertyName("id")] public string Id { get; set; } + + [JsonPropertyName("address")] public string Address { get; set; } + + [JsonPropertyName("chain_type")] public string ChainType { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Web3/Services/Gmx/GmxV2Service.cs b/src/Managing.Infrastructure.Web3/Services/Gmx/GmxV2Service.cs index 0bb3d16..c334743 100644 --- a/src/Managing.Infrastructure.Web3/Services/Gmx/GmxV2Service.cs +++ b/src/Managing.Infrastructure.Web3/Services/Gmx/GmxV2Service.cs @@ -793,6 +793,7 @@ public class GmxV2Service var exchangeRouterService = new ExchangeRouterService(web3, Arbitrum.AddressV2.ExchangeRouter); var receipt = await exchangeRouterService.MulticallRequestAndWaitForReceiptAsync(multiCallFunction); + // Call privy api instead and send the txn var trade = new Trade(DateTime.UtcNow, direction, Enums.TradeStatus.Requested, diff --git a/src/Managing.Infrastructure.Web3/Services/PrivyService.cs b/src/Managing.Infrastructure.Web3/Services/PrivyService.cs new file mode 100644 index 0000000..673de53 --- /dev/null +++ b/src/Managing.Infrastructure.Web3/Services/PrivyService.cs @@ -0,0 +1,85 @@ +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using Managing.Infrastructure.Evm.Abstractions; +using Managing.Infrastructure.Evm.Models; + +public class PrivyService : IPrivyService +{ + private readonly HttpClient _privyClient; + private readonly string _appId; + private readonly string _appSecret; + + public PrivyService(IPrivySettings settings) + { + _privyClient = new HttpClient(); + _appId = settings.AppId; + _appSecret = settings.AppSecret; + + ConfigureHttpClient(); + } + + private void ConfigureHttpClient() + { + _privyClient.BaseAddress = new Uri("https://api.privy.io/"); + var authToken = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_appId}:{_appSecret}")); + // _privyClient.DefaultRequestHeaders.Authorization = + // new AuthenticationHeaderValue("Basic", $"{_appId}:{_appSecret}"); + _privyClient.DefaultRequestHeaders.Add("privy-app-id", _appId); + // add custom header + _privyClient.DefaultRequestHeaders.Add("Authorization", authToken); + _privyClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + } + + public async Task CreateWalletAsync(string chainType = "ethereum") + { + try + { + var json = JsonSerializer.Serialize(new { chain_type = chainType }); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await _privyClient.PostAsJsonAsync("/v1/wallets", content); + + var result = new PrivyWallet(); + + if (response.IsSuccessStatusCode) + { + result = await response.Content.ReadFromJsonAsync(); + } + else + { + throw new Exception(await response.Content.ReadAsStringAsync()); + } + + return result; + } + catch (Exception ex) + { + throw new Exception(ex.Message); + } + } + + public async Task SendTransactionAsync(string walletId, string recipientAddress, long value, + string caip2 = "eip155:84532") + { + var requestBody = new + { + method = "eth_sendTransaction", + caip2, + @params = new + { + transaction = new + { + to = recipientAddress, + value + } + } + }; + + return await _privyClient.PostAsJsonAsync( + $"/v1/wallets/{walletId}/rpc", + requestBody + ); + } +} \ No newline at end of file diff --git a/src/Managing.WebApp/src/components/organism/Backtest/backtestRowDetails.tsx b/src/Managing.WebApp/src/components/organism/Backtest/backtestRowDetails.tsx index 91706ca..772c58b 100644 --- a/src/Managing.WebApp/src/components/organism/Backtest/backtestRowDetails.tsx +++ b/src/Managing.WebApp/src/components/organism/Backtest/backtestRowDetails.tsx @@ -59,14 +59,14 @@ const BacktestRowDetails: React.FC = ({ diff --git a/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx b/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx index 3c77db8..8805ba0 100644 --- a/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx +++ b/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx @@ -275,9 +275,9 @@ const BacktestTable: React.FC = ({ list, isFetching }) => { // Get average optimized money management for every backtest const optimized = list!.map((b) => b.optimizedMoneyManagement) - const stopLoss = optimized.reduce((acc, curr) => acc + curr.stopLoss, 0) + const stopLoss = optimized.reduce((acc, curr) => acc + (curr?.stopLoss ?? 0), 0) const takeProfit = optimized.reduce( - (acc, curr) => acc + curr.takeProfit, + (acc, curr) => acc + (curr?.takeProfit ?? 0), 0 ) diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index fcab89d..dacfe7e 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -1631,7 +1631,7 @@ export class TradingClient extends AuthorizedApiBase { url_ = url_.replace(/[?&]$/, ""); let options_: RequestInit = { - method: "GET", + method: "POST", headers: { "Accept": "application/json" } @@ -1960,6 +1960,7 @@ export enum AccountType { Trader = "Trader", Watch = "Watch", Auth = "Auth", + Privy = "Privy", } export interface User { diff --git a/src/Managing.WebApp/src/pages/settingsPage/account/accountModal.tsx b/src/Managing.WebApp/src/pages/settingsPage/account/accountModal.tsx index 84681f0..21f3953 100644 --- a/src/Managing.WebApp/src/pages/settingsPage/account/accountModal.tsx +++ b/src/Managing.WebApp/src/pages/settingsPage/account/accountModal.tsx @@ -116,8 +116,8 @@ const AccountModal: React.FC = ({ showModal, toggleModal }) => { - {selectedExchange != TradingExchanges.Evm && - selectedType != AccountType.Trader ? ( + {(selectedExchange != TradingExchanges.Evm && selectedType != AccountType.Trader ) || + (selectedExchange == TradingExchanges.Evm && selectedType == AccountType.Privy )? ( <>