using System.Net.Http.Json; using System.Numerics; using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Common; using Managing.Core; using Managing.Domain.Accounts; using Managing.Domain.Candles; using Managing.Domain.Evm; using Managing.Domain.Statistics; using Managing.Domain.Trades; using Managing.Infrastructure.Evm.Abstractions; using Managing.Infrastructure.Evm.Models; using Managing.Infrastructure.Evm.Models.Gmx.v2; using Managing.Infrastructure.Evm.Models.Privy; using Managing.Infrastructure.Evm.Models.Proxy; using Managing.Infrastructure.Evm.Referentials; using Managing.Infrastructure.Evm.Services; using Managing.Infrastructure.Evm.Services.Gmx; using NBitcoin; using Nethereum.Contracts; using Nethereum.Contracts.Standards.ERC20.ContractDefinition; using Nethereum.HdWallet; using Nethereum.Hex.HexTypes; using Nethereum.Signer; using Nethereum.Web3; using static Managing.Common.Enums; using BalanceOfFunction = Nethereum.Contracts.Standards.ERC20.ContractDefinition.BalanceOfFunction; using BalanceOfOutputDTO = Nethereum.Contracts.Standards.ERC20.ContractDefinition.BalanceOfOutputDTO; using Chain = Managing.Domain.Evm.Chain; using TransferEventDTO = Nethereum.Contracts.Standards.ERC721.ContractDefinition.TransferEventDTO; namespace Managing.Infrastructure.Evm; public class EvmManager : IEvmManager { private readonly Web3 _web3; private readonly HttpClient _httpClient; private readonly string _password = "!StrongPassword94"; private readonly IEnumerable _subgraphs; private readonly GmxV2Service _gmxV2Service; private readonly IWeb3ProxyService _web3ProxyService; private readonly ICacheService _cacheService; public EvmManager(IEnumerable subgraphs, IWeb3ProxyService web3ProxyService, ICacheService cacheService) { var defaultChain = ChainService.GetEthereum(); _web3 = new Web3(defaultChain.RpcUrl); _httpClient = new HttpClient(); _subgraphs = subgraphs; _web3ProxyService = web3ProxyService; _gmxV2Service = new GmxV2Service(); _cacheService = cacheService; } public async Task GetAddressBalance(string address) { var balance = await _web3.Eth.GetBalance.SendRequestAsync(address); var etherAmount = Web3.Convert.FromWei(balance.Value); return etherAmount; } public async Task> GetContractHolders(string contractAddress, DateTime since) { var holders = new List(); var contract = _web3.Eth.ERC721.GetContractService(contractAddress); // Retrieve total supply to iterate over all token id generated within the contract var totalSupply = await contract.TotalSupplyQueryAsync(); for (int tokenId = 1; tokenId < 10; tokenId++) { // Retrieve the owner of the nft var tokenOwner = await contract.OwnerOfQueryAsync(tokenId); // If holder already have an nft we get the holder // Otherwise we create a new holder var holder = holders.FirstOrDefault(h => h.HolderAddress == tokenOwner) ?? new Holder(tokenOwner); // Retrieve all events related to the owner on the contract address var nfts = await GetNftEvent(contractAddress, tokenOwner); // Get tokenId related event var nftEvent = nfts.FirstOrDefault(n => n.Event.TokenId == new BigInteger(tokenId)); if (nftEvent != null) { // Retrieve the date of the nft event that occur in the blocknumber var blockDate = await GetBlockDate(nftEvent.Log.BlockNumber); var nft = new Nft(contractAddress, tokenId, blockDate); // Verify if the date of the holding is before the date passed from the parameters if (blockDate <= since) { holder.AddNft(nft); // If holder do not exist in the list we add it if (!holders.Exists(h => h.HolderAddress == tokenOwner)) { holders.Add(holder); } } else { Console.WriteLine($"TokenId #{tokenId} for owner {tokenOwner} date not in range ({blockDate:f})"); } } else { Console.WriteLine($"Error when getting tokenId #{tokenId} for owner {tokenOwner}"); } UpdateLine(tokenId, (int)totalSupply, holders.Count); } return holders; } public async Task>> GetNftEvent(string contractAddress, string tokenOwner) { return await NftService.GetNftEvent(_web3, tokenOwner, contractAddress); } public async Task GetBlockDate(int blockNumber) { return await GetBlockDate(new HexBigInteger(blockNumber)); } public async Task GetBlockDate(HexBigInteger blockNumber) { var block = await _web3.Eth.Blocks.GetBlockWithTransactionsByNumber.SendRequestAsync(blockNumber); var date = DateHelpers.GetFromUnixTimestamp((int)block.Timestamp.Value); return date; } private void UpdateLine(int count, int total, int holders) { Console.WriteLine($"{count}/{total} - {(((decimal)count * 100m) / (decimal)total)} % - Holders : {holders}"); } public string VerifySignature(string signature, string message) { var signer = new EthereumMessageSigner(); var addressRecovered = signer.EncodeUTF8AndEcRecover(message, signature); return addressRecovered; } public string SignMessage(string message, string privateKey) { var signer = new EthereumMessageSigner(); var signature = signer.EncodeUTF8AndSign(message, new EthECKey(privateKey)); return signature; } public (string Key, string Secret) GenerateAddress() { var mnemo = new Mnemonic(Wordlist.English, WordCount.Twelve); var wallet = new Wallet(mnemo.ToString(), _password); var account = wallet.GetAccount(0); return (account.Address, mnemo.ToString()); } public string GetAddressFromMnemo(string mnemo) { var wallet = new Wallet(mnemo, _password); return wallet.GetAccount(0).Address; } public async Task GetEtherBalance(Chain chain, string account) { var web3 = new Web3(chain.RpcUrl); var etherBalance = Web3.Convert.FromWei(await web3.Eth.GetBalance.SendRequestAsync(account)); var lastCandle = await GetCandle(Ticker.ETH); return new EvmBalance() { Balance = etherBalance, Price = lastCandle.Close, TokenName = "ETH", Value = etherBalance * lastCandle.Close }; } public async Task> GetAllBalances(Chain chain, string publicAddress) { var balances = new List(); foreach (var ticker in Enum.GetValues()) { try { var balance = await GetTokenBalance(chain.Name, ticker, publicAddress); if (balance != null && balance.Balance > 0) { balances.Add(balance); } } catch (Exception ex) { // TODO : handle exception } } var etherBalance = await GetEtherBalance(chain, publicAddress); etherBalance.Chain = chain; balances.Add(etherBalance); return balances; } public async Task GetTokenBalance(string chainName, Ticker ticker, string publicAddress) { var chain = ChainService.GetChain(chainName); var web3 = new Web3(chain.RpcUrl); var balanceOfMessage = new BalanceOfFunction() { Owner = publicAddress }; //Creating a new query handler var queryHandler = web3.Eth.GetContractQueryHandler(); var contractAddress = TokenService.GetContractAddress(ticker); if (contractAddress == Arbitrum.Address.Zero) return null; var balance = await queryHandler .QueryAsync(contractAddress, balanceOfMessage) .ConfigureAwait(false); var lastCandle = await GetCandle(ticker); var tokenUsdPrice = lastCandle.Close; var tokenDecimal = TokenService.GetDecimal(ticker); var balanceFromWei = Web3.Convert.FromWei(balance, tokenDecimal); var evmBalance = new EvmBalance { TokenName = ticker.ToString(), Balance = balanceFromWei, TokenAddress = contractAddress, Value = tokenUsdPrice * balanceFromWei, Price = tokenUsdPrice, Chain = chain }; return evmBalance; } public async Task> GetBalances(Chain chain, int page, int pageSize, string publicAddress) { var callList = new List(); var startItem = (page * pageSize); var tokens = TokenService.GetTokens(); var totaItemsToFetch = startItem + pageSize <= tokens.Count ? startItem + pageSize : tokens.Count + startItem; for (int i = startItem; i < totaItemsToFetch; i++) { var balanceOfMessage = new BalanceOfFunction() { Owner = publicAddress }; var call = new MulticallInputOutput(balanceOfMessage, tokens[i].Address); callList.Add(call); } var evmTokens = new List<(EvmBalance Balance, GeckoToken GeckoToken)>(); try { var web3 = new Web3(chain.RpcUrl); var geckoTokens = TokenService.GetGeckoTokens(); await web3.Eth.GetMultiQueryHandler().MultiCallAsync(callList.ToArray()); for (int i = startItem; i < totaItemsToFetch; i++) { var balance = ((MulticallInputOutput)callList[i - startItem]) .Output.Balance; if (balance > 0) { var tokenBalance = new EvmBalance() { Balance = Web3.Convert.FromWei(balance, tokens[i].Decimals), TokenName = tokens[i].Symbol, TokenAddress = tokens[i].Address, Chain = chain }; var geckoToken = geckoTokens.FirstOrDefault(x => string.Equals(x.Symbol, tokens[i].Symbol, StringComparison.InvariantCultureIgnoreCase)); evmTokens.Add((tokenBalance, geckoToken)!); } } if (evmTokens.Count > 0) { var ids = evmTokens.Select(x => x.GeckoToken?.Id).Distinct().ToList(); var prices = await GetPrices(ids).ConfigureAwait(false); foreach (var balance in evmTokens) { if (balance.GeckoToken != null) { var price = prices[balance.GeckoToken.Id.ToLower()]; balance.Balance.Price = price["usd"]; balance.Balance.Value = balance.Balance.Price * balance.Balance.Balance; } } } } catch (Exception ex) { // TODO : Handle error // No enable to reach rpc var test = ex.Message; } return evmTokens.Select(e => e.Balance).ToList(); } public async Task>> GetPrices(List geckoIds) { var idsCombined = string.Join(",", geckoIds); return await _httpClient.GetFromJsonAsync>>( "https://api.coingecko.com/api/v3/simple/price?ids=" + idsCombined + "&vs_currencies=usd"); } public async Task> GetAllBalancesOnAllChain(string publicAddress) { var chainBalances = new List(); var chains = ChainService.GetChains(); foreach (var chain in chains) { chainBalances.AddRange(await GetAllBalances(chain, publicAddress)); } return chainBalances; } public async Task> GetCandles(Ticker ticker, DateTime startDate, Timeframe timeframe, bool isFirstCall = false) { string gmxTimeframe = GmxHelpers.GeTimeframe(timeframe); int limit = isFirstCall ? 10000 : CalculateCandleLimit(startDate, timeframe); GmxV2Prices? gmxPrices = null; int maxRetries = 3; int delayMs = 1000; for (int attempt = 1; attempt <= maxRetries; attempt++) { try { gmxPrices = await _httpClient.GetFromJsonAsync( $"https://arbitrum-api.gmxinfra.io/prices/candles?tokenSymbol={ticker}&period={gmxTimeframe}&limit={limit}"); break; } catch (HttpRequestException ex) when (ex.InnerException is IOException) { Console.Error.WriteLine($"Attempt {attempt}: Network error while fetching candles: {ex.Message}"); if (attempt == maxRetries) throw; await Task.Delay(delayMs * attempt); } catch (Exception ex) { Console.Error.WriteLine($"Unexpected error: {ex.Message}"); throw; } } if (gmxPrices == null) return null; var filteredCandles = gmxPrices.Candles.Where(p => p[0] >= startDate.ToUnixTimestamp()).ToList(); var candles = new List(); var timeBetweenCandles = gmxPrices.Candles.Count > 2 ? gmxPrices.Candles[0][0] - gmxPrices.Candles[1][0] : 900; // Default 15 minutes for (int i = 0; i < filteredCandles.Count; i++) { var c = GmxV2Mappers.Map(filteredCandles[i], ticker, timeframe, (int)timeBetweenCandles); candles.Add(c); } return candles.OrderBy(c => c.Date).ToList(); } private int CalculateCandleLimit(DateTime startDate, Timeframe timeframe) { var now = DateTime.UtcNow; var minutesPerCandle = timeframe switch { Timeframe.OneMinute => 1, Timeframe.FiveMinutes => 5, Timeframe.FifteenMinutes => 15, Timeframe.ThirtyMinutes => 30, Timeframe.OneHour => 60, Timeframe.FourHour => 240, Timeframe.OneDay => 1440, _ => 15 }; var totalMinutes = (now - startDate).TotalMinutes; var candlesNeeded = (int)Math.Ceiling(totalMinutes / minutesPerCandle); return Math.Min(candlesNeeded + 5, 10000); } public decimal GetVolume(SubgraphProvider subgraphProvider, Ticker ticker) { var subgraph = GetSubgraph(subgraphProvider); var volume = subgraph.GetVolume(ticker).Result; return volume; } private ISubgraphPrices GetSubgraph(SubgraphProvider subgraphProvider) { return _subgraphs.First(s => s.GetProvider() == subgraphProvider); } public async Task> GetAvailableTicker() { var tokenList = await _httpClient.GetFromJsonAsync( "https://arbitrum-api.gmxinfra.io/tokens"); if (tokenList == null) return null; return GmxV2Mappers.Map(tokenList); } public async Task GetCandle(Ticker ticker) { var key = $"lastcandle-{ticker}"; var cachedCandle = _cacheService.GetValue(key); if (cachedCandle == null) { var lastCandles = await GetCandles(ticker, DateTime.UtcNow.AddMinutes(-5), Timeframe.OneMinute); cachedCandle = lastCandles.Last(); _cacheService.SaveValue(key, cachedCandle, TimeSpan.FromMinutes(5)); } return cachedCandle; } public async Task InitAddress(string publicAddress) { try { var response = await _web3ProxyService.CallPrivyServiceAsync( "/init-address", new { address = publicAddress }); return response; } catch (Exception ex) { // Log the error Console.Error.WriteLine($"Error initializing address: {ex.Message}"); return new PrivyInitAddressResponse { Success = false, Error = ex.Message }; } } public async Task GetAllowance(string publicAddress, Ticker ticker) { var contractAddress = TokenService.GetContractAddress(ticker); var allowance = await EvmBase.GetAllowance(_web3, publicAddress, contractAddress); return allowance; } public async Task Send( Chain chain, Ticker ticker, decimal amount, string publicAddress, string privateKey, string receiverAddress) { var account = new Wallet(privateKey, _password).GetAccount(publicAddress); var web3 = new Web3(account, chain.RpcUrl); try { if (ticker == Ticker.ETH) { return await SendEth(amount, receiverAddress, web3); } return await SendToken(ticker, amount, publicAddress, receiverAddress, web3); } catch (Exception ex) { return false; } } private static async Task SendEth(decimal amount, string receiverAddress, Web3 web3) { web3.TransactionManager.UseLegacyAsDefault = true; var ethService = web3.Eth.GetEtherTransferService(); var gas = await ethService.EstimateGasAsync(receiverAddress, amount); var transaction = await ethService.TransferEtherAndWaitForReceiptAsync(receiverAddress, amount, gas: gas); return transaction.Status.Value == 1; } private static async Task SendToken( Ticker ticker, decimal amount, string senderAddress, string receiverAddress, Web3 web3) { var contractAddress = TokenService.GetContractAddress(ticker); var transactionMessage = new TransferFunction { FromAddress = senderAddress, To = receiverAddress, Value = Web3.Convert.ToWei(amount) }; var transferHandler = web3.Eth.GetContractTransactionHandler(); var transferReceipt = await transferHandler.SendRequestAndWaitForReceiptAsync(contractAddress, transactionMessage); var transaction = await web3.Eth.Transactions.GetTransactionByHash.SendRequestAsync(transferReceipt.TransactionHash); return transaction != null; } public async Task CancelOrders(Account account, Ticker ticker) { if (account.IsPrivyWallet) { try { var response = await _web3ProxyService.CallGmxServiceAsync("/cancel-orders", new { account = account.Key, walletId = account.Secret, ticker = ticker.ToString() }); return response.Success; } catch (Exception ex) { Console.Error.WriteLine($"Error canceling orders via Fastify API: {ex.Message}"); return false; } } else { var wallet = new Wallet(account.Secret, _password).GetAccount(account.Key); var chain = ChainService.GetChain(Constants.Chains.Arbitrum); var web3 = new Web3(wallet, chain.RpcUrl); var service = new GmxV2Service(); return await service.CancelOrders(web3, account.Key, ticker); } } public async Task IncreasePosition( Account account, Ticker ticker, TradeDirection direction, decimal price, decimal quantity, decimal? leverage, decimal? stopLossPrice = null, decimal? takeProfitPrice = null) { Trade trade = null; try { // If this is a Privy wallet, call the GMX service through Fastify API if (account.IsPrivyWallet) { try { var response = await _web3ProxyService.CallGmxServiceAsync("/open-position", new { account = account.Key, walletId = account.Secret, tradeType = price > 0 ? "limit" : "market", ticker = ticker.ToString(), direction = direction.ToString(), price = price, quantity, leverage = leverage ?? 1.0m, stopLossPrice = stopLossPrice, takeProfitPrice = takeProfitPrice }); // Create a trade object using the returned hash var tradeType = price > 0 ? TradeType.Limit : TradeType.Market; var tradeStatus = TradeStatus.Requested; // Use a valid enum value that exists in TradeStatus trade = new Trade( DateTime.UtcNow, direction, tradeStatus, tradeType, ticker, quantity, price, leverage ?? 1.0m, account.Key, "" ); } catch (Exception e) { Console.WriteLine(e); throw; } } else { // Continue with the existing direct service call for non-Privy wallets var wallet = new Wallet(account.Secret, _password).GetAccount(account.Key); var chain = ChainService.GetChain(Constants.Chains.Arbitrum); var web3 = new Web3(wallet, chain.RpcUrl); trade = await _gmxV2Service.IncreasePosition(web3, account.Key, ticker, direction, price, quantity, leverage); } } catch (Exception ex) { throw; } return trade; } public async Task DecreasePosition( Account account, Ticker ticker, TradeDirection direction, decimal price, decimal quantity, decimal? leverage) { Trade trade = null; if (account.IsPrivyWallet) { try { var response = await _web3ProxyService.CallGmxServiceAsync("/close-position", new { account = account.Key, ticker = ticker.ToString(), direction = direction == TradeDirection.Long ? TradeDirection.Short.ToString() : TradeDirection.Long.ToString(), }); trade = new Trade( DateTime.UtcNow, direction, TradeStatus.Requested, TradeType.Market, ticker, quantity, price, leverage ?? 1, response.Hash, "" ); return trade; } catch (Exception e) { Console.WriteLine(e); throw; } } var wallet = new Wallet(account.Secret, _password).GetAccount(account.Key); var chain = ChainService.GetChain(Constants.Chains.Arbitrum); var web3 = new Web3(wallet, chain.RpcUrl); try { trade = await _gmxV2Service.DecreasePosition(web3, account.Key, ticker, direction, price, quantity, leverage ?? 1); } catch (Exception ex) { throw; } return trade; } public async Task DecreaseOrder(Account account, TradeType tradeType, Ticker ticker, TradeDirection direction, decimal price, decimal quantity, decimal? leverage, decimal? stopLossPrice = null, decimal? takeProfitPrice = null) { var wallet = new Wallet(account.Secret, _password).GetAccount(account.Key); var chain = ChainService.GetChain(Constants.Chains.Arbitrum); var web3 = new Web3(wallet, chain.RpcUrl); Trade trade; try { // TODO: This method in GmxV2Service might not exist or needs different handling for Privy wallets. // Commenting out for now as IncreasePosition is the priority. // trade = await _gmxV2Service.DecreaseOrder(web3, account.Key, tradeType, ticker, direction, price, // quantity, leverage); trade = null; // Placeholder return } catch (Exception ex) { throw; } return trade; } public async Task GetTrade(Account account, string chainName, Ticker ticker) { if (account.IsPrivyWallet) { var result = await _web3ProxyService.GetGmxServiceAsync( "/positions", new { account = account.Key, ticker = ticker.ToString() }); var position = result.Positions.FirstOrDefault(p => p.Ticker == ticker.ToString()); if (position == null) return null; // TODO: Map the position object to a Trade object var trade = new Trade( position.Date, MiscExtensions.ParseEnum(position.Direction), MiscExtensions.ParseEnum(position.Status), MiscExtensions.ParseEnum(position.TradeType), MiscExtensions.ParseEnum(position.Ticker), (decimal)position.Quantity, (decimal)position.Price, (decimal?)position.Leverage, account.Key, position.ExchangeOrderId ); return trade; } return await GetTrade(account.Key, chainName, ticker); } public async Task> GetPositions(Account account) { if (account.IsPrivyWallet) { var result = await _web3ProxyService.GetGmxServiceAsync( "/positions", new { account = account.Key }); return GmxV2Mappers.Map(result.Positions); } throw new NotImplementedException(); } public async Task GetTrade(string reference, string chainName, Ticker ticker) { var chain = ChainService.GetChain(chainName); var web3 = new Web3(chain.RpcUrl); return await _gmxV2Service.GetTrade(web3, reference, ticker); } public async Task> GetFundingRates() { return await _web3ProxyService.CallGmxServiceAsync>("/gmx/funding-rates", new { }); } public async Task QuantityInPosition(string chainName, Account account, Ticker ticker) { if (account.IsPrivyWallet) { var positions = await GetPositions(account); var positionForTicker = positions.FirstOrDefault(p => p.Ticker == ticker); if (positionForTicker != null) { return positionForTicker.Open.Quantity; } else { return 0; } } var chain = ChainService.GetChain(chainName); var web3 = new Web3(chain.RpcUrl); var quantity = await _gmxV2Service.QuantityInPosition(web3, account.Key, ticker); return quantity; } public async Task GetFee(string chainName) { var chain = ChainService.GetChain(chainName); var web3 = new Web3(chain.RpcUrl); var etherPrice = (await GetPrices(new List { "ethereum" }))["ethereum"]["usd"]; var fee = await GmxService.GetFee(web3, etherPrice); return fee; } public async Task> GetOrders(Account account, Ticker ticker) { if (account.IsPrivyWallet) { var result = await _web3ProxyService.GetGmxServiceAsync("/trades", new { account = account.Key, ticker = ticker.ToString() }); return GmxV2Mappers.Map(result.Trades); } else { var chain = ChainService.GetChain(Constants.Chains.Arbitrum); var web3 = new Web3(chain.RpcUrl); // var orders = await GmxService.GetOrders(web3, account.Key, ticker); var orders = await _gmxV2Service.GetOrders(web3, account.Key, ticker); return GmxV2Mappers.Map(orders); } return new List(); } public async Task SetAllowance(Account account, Ticker ticker, BigInteger amount) { if (account.IsPrivyWallet) { var allowance = await _web3ProxyService.CallPrivyServiceAsync("/approve-token", new { address = account.Key, walletId = account.Secret, ticker = ticker.ToString(), amount = amount.Equals(0) ? null : amount.ToString() }); return false; } var web3 = BuildWeb3ForAccount(account); var contractAddress = TokenService.GetContractAddress(ticker); var approval = await EvmBase.ApproveToken(web3, account.Key, contractAddress, Arbitrum.AddressV2.SyntheticsRouter, amount); return approval; } private Web3 BuildWeb3ForAccount(Account account) { var wallet = new Wallet(account.Secret, _password).GetAccount(account.Key); var chain = ChainService.GetChain(Constants.Chains.Arbitrum); return new Web3(wallet, chain.RpcUrl); } public async Task<(string Id, string Address)> CreatePrivyWallet() { var privyWallet = await _web3ProxyService.CallPrivyServiceAsync("/privy/create-wallet", new { }); return (privyWallet.Id, privyWallet.Address); } /// /// Signs a message using the embedded wallet /// /// The wallet id of the embedded wallet /// The address of the embedded wallet /// The message to sign /// The signature response public async Task SignMessageAsync(string embeddedWalletId, string address, string message) { // Construct the request body using the exact format from Privy documentati var requestBody = new { address = address, walletId = embeddedWalletId, message = message, }; var response = await _web3ProxyService.CallPrivyServiceAsync("sign-message", requestBody); return response.Signature; } // Overload to match IEvmManager interface public async Task> GetCandles(Ticker ticker, DateTime startDate, Timeframe timeframe) { return await GetCandles(ticker, startDate, timeframe, false); } }