using System.Numerics; using Managing.ABI.GmxV2.DataStore; using Managing.ABI.GmxV2.DataStore.ContractDefinition; using Managing.ABI.GmxV2.ExchangeRouter; using Managing.ABI.GmxV2.ExchangeRouter.ContractDefinition; using Managing.ABI.GmxV2.Multicall3.ContractDefinition; using Managing.ABI.GmxV2.Reader; using Managing.ABI.GmxV2.Reader.ContractDefinition; using Managing.Common; using Managing.Core; using Managing.Domain.Statistics; using Managing.Domain.Trades; using Managing.Infrastructure.Evm.Models.Gmx.v2; using Managing.Infrastructure.Evm.Referentials; using Nethereum.Contracts; using Nethereum.Contracts.Standards.ERC20.ContractDefinition; using Nethereum.Hex.HexConvertors.Extensions; using Nethereum.Hex.HexTypes; using Nethereum.Util; using Nethereum.Web3; using GetAccountOrdersFunction = Managing.ABI.GmxV2.Reader.ContractDefinition.GetAccountOrdersFunction; using GetAccountOrdersOutputDTO = Managing.ABI.GmxV2.Reader.ContractDefinition.GetAccountOrdersOutputDTO; using GetAccountPositionInfoListFunction = Managing.ABI.GmxV2.SyntheticsReader.ContractDefinition.GetAccountPositionInfoListFunction; using GetAccountPositionInfoListOutputDTO = Managing.ABI.GmxV2.SyntheticsReader.ContractDefinition.GetAccountPositionInfoListOutputDTO; using GetMarketInfoFunction = Managing.ABI.GmxV2.Reader.ContractDefinition.GetMarketInfoFunction; using GetMarketInfoOutputDTO = Managing.ABI.GmxV2.Reader.ContractDefinition.GetMarketInfoOutputDTO; using GetMarketsFunction = Managing.ABI.GmxV2.Reader.ContractDefinition.GetMarketsFunction; using GetMarketsOutputDTO = Managing.ABI.GmxV2.Reader.ContractDefinition.GetMarketsOutputDTO; using GetMarketTokenPriceFunction = Managing.ABI.GmxV2.Reader.ContractDefinition.GetMarketTokenPriceFunction; using GetMarketTokenPriceOutputDTO = Managing.ABI.GmxV2.Reader.ContractDefinition.GetMarketTokenPriceOutputDTO; using MarketPrices = Managing.ABI.GmxV2.Reader.ContractDefinition.MarketPrices; namespace Managing.Infrastructure.Evm.Services.Gmx; public class GmxV2Service { private readonly GmxV2Encoder _encoder = new GmxV2Encoder(); private readonly BigInteger _defaultCount = new BigInteger(1000); private readonly OracleKeeperService _oracleKeeperService = new OracleKeeperService(); public GmxV2Service() { } public async Task GetMarkets(Web3 web3) { var reader = new ReaderService(web3, Arbitrum.AddressV2.Reader); var result = await reader.GetMarketsQueryAsync(Arbitrum.AddressV2.DataStore, 0, 100); return result; } public async Task> GetMarketsAsync(Web3 web3) { var getMarketCallData = new GetMarketsFunction { DataStore = Arbitrum.AddressV2.DataStore, Start = 0, End = 10 }.GetCallData(); var call1 = new Call { Target = Arbitrum.AddressV2.Reader, CallData = getMarketCallData }; var aggregateFunction = new AggregateFunction(); aggregateFunction.Calls = new List(); aggregateFunction.Calls.Add(call1); var queryHandler = web3.Eth.GetContractQueryHandler(); var returnCalls = await queryHandler .QueryDeserializingToObjectAsync(aggregateFunction, Arbitrum.AddressV2.Multicall) .ConfigureAwait(false); var markets = new GetMarketsOutputDTO().DecodeOutput(returnCalls.ReturnData[0].ToHex()); var result = new List(); foreach (var market in markets.ReturnValue1) { try { result.Add(GmxMappers.MapInfos(market)); } catch (Exception e) { Console.WriteLine(e); } } return result; } public async Task GetMarketInfo(Web3 web3) { var reader = new ReaderService(web3, Arbitrum.AddressV2.Reader); var market = (await GetMarketsAsync(web3)).First(); var tokensData = await GetTokensData(web3); var marketPrices = GmxHelpers.GetContractMarketPrices(tokensData, market); var response = await reader.GetMarketInfoQueryAsync(Arbitrum.AddressV2.DataStore, marketPrices, market.MarketToken); // Get hashed key to call datastore var longInterestUsingLongTokenCallData = new GetUintFunction() { Key = _encoder.HashData([ ("bytes32", _encoder.HashString(Constants.GMX.OPEN_INTEREST)), ("address", market.MarketToken), ("address", market.LongToken), ("bool", true) ]) }.GetCallData(); var longInterestUsingLongTokenCall = new Call { Target = Arbitrum.AddressV2.DataStore, CallData = longInterestUsingLongTokenCallData }; var longInterestUsingShortTokenCallData = new GetUintFunction() { Key = _encoder.HashData([ ("bytes32", _encoder.HashString(Constants.GMX.OPEN_INTEREST)), ("address", market.MarketToken), ("address", market.ShortToken), ("bool", true) ]) }.GetCallData(); var longInterestUsingShortTokenCall = new Call { Target = Arbitrum.AddressV2.DataStore, CallData = longInterestUsingShortTokenCallData }; var shortInterestUsingLongTokenCallData = new GetUintFunction() { Key = _encoder.HashData([ ("bytes32", _encoder.HashString(Constants.GMX.OPEN_INTEREST)), ("address", market.MarketToken), ("address", market.LongToken), ("bool", false) ]) }.GetCallData(); var shortInterestUsingLongTokenCall = new Call { Target = Arbitrum.AddressV2.DataStore, CallData = shortInterestUsingLongTokenCallData }; var shortInterestUsingShortTokenCallData = new GetUintFunction() { Key = _encoder.HashData([ ("bytes32", _encoder.HashString(Constants.GMX.OPEN_INTEREST)), ("address", market.MarketToken), ("address", market.ShortToken), ("bool", false) ]) }.GetCallData(); var shortInterestUsingShortTokenCall = new Call { Target = Arbitrum.AddressV2.DataStore, CallData = shortInterestUsingShortTokenCallData }; var dataStoreMulticall = new AggregateFunction(); dataStoreMulticall.Calls = new List(); dataStoreMulticall.Calls.Add(longInterestUsingLongTokenCall); dataStoreMulticall.Calls.Add(longInterestUsingShortTokenCall); dataStoreMulticall.Calls.Add(shortInterestUsingLongTokenCall); dataStoreMulticall.Calls.Add(shortInterestUsingShortTokenCall); var queryHandler = web3.Eth.GetContractQueryHandler(); var returnCalls = await queryHandler .QueryDeserializingToObjectAsync(dataStoreMulticall, Arbitrum.AddressV2.Multicall) .ConfigureAwait(false); var marketInfos = GmxMappers.Map(response.ReturnValue1); var result = new GmxMarketInfo() { Market = marketInfos, Infos = DecodeFundingRateOutput(returnCalls, marketInfos.IsSameCollaterals) }; return result; } public async Task GetMarketTokenPrice(Web3 web3, bool maximize = true) { var reader = new ReaderService(web3, Arbitrum.AddressV2.Reader); var market = (await GetMarketsAsync(web3)).First(); var tokensData = await GetTokensData(web3); var marketPrices = GmxHelpers.GetContractMarketPrices(tokensData, market); var marketProps = new MarketsProps() { MarketToken = market.MarketToken, IndexToken = market.IndexToken, LongToken = market.LongToken, ShortToken = market.ShortToken }; var response = await reader.GetMarketTokenPriceQueryAsync(Arbitrum.AddressV2.DataStore, marketProps, marketPrices.IndexTokenPrice, marketPrices.LongTokenPrice, marketPrices.ShortTokenPrice, _encoder.HashString(Constants.GMX.MAX_PNL_FACTOR_FOR_TRADERS), maximize); return GmxMappers.Map(response); } public async Task GetIsMarketDisabled(Web3 web3) { var dataStoreService = new DataStoreService(web3, Arbitrum.AddressV2.DataStore); var market = (await GetMarketsAsync(web3)).First(); var hashString = _encoder.HashString(Constants.GMX.MARKET_DISABLED_KEY); var parameters = new List<(string dataType, object dataValues)> { ("bytes32", hashString), ("address", market.MarketToken) }; var response = await dataStoreService.GetBoolQueryAsync(_encoder.HashData(parameters)); return response; } public async Task GetLongInterestAmount(Web3 web3) { var dataStoreService = new DataStoreService(web3, Arbitrum.AddressV2.DataStore); var market = (await GetMarketsAsync(web3)).First(); var parameters = new List<(string dataType, object dataValues)> { ("bytes32", _encoder.HashString(Constants.GMX.OPEN_INTEREST)), ("address", market.MarketToken), ("address", market.LongToken), ("bool", true) }; var response = await dataStoreService.GetUintQueryAsync(_encoder.HashData(parameters)); return response; } public async Task> GetMarketInfosAsync(Web3 web3) { var markets = await GetMarketsAsync(web3); var readerResult = new List(); var readerCalls = new List(); var datastoreCalls = new List(); markets = markets.Where(m => !m.IsSpotOnly).ToList(); var tokensData = await GetTokensData(web3); foreach (var market in markets) { var marketPrices = GmxHelpers.GetContractMarketPrices(tokensData, market); var marketProps = new MarketsProps() { MarketToken = market.MarketToken, IndexToken = market.IndexToken, LongToken = market.LongToken, ShortToken = market.ShortToken }; var getMarketInfoCallData = new GetMarketInfoFunction { DataStore = Arbitrum.AddressV2.DataStore, Prices = marketPrices, MarketKey = market.MarketToken }.GetCallData(); var getMarketInfoCall = new Call { Target = Arbitrum.AddressV2.Reader, CallData = getMarketInfoCallData }; var getMarketTokenPriceCallData = new GetMarketTokenPriceFunction { DataStore = Arbitrum.AddressV2.DataStore, Market = marketProps, IndexTokenPrice = marketPrices.IndexTokenPrice, LongTokenPrice = marketPrices.LongTokenPrice, ShortTokenPrice = marketPrices.ShortTokenPrice, PnlFactorType = _encoder.HashString(Constants.GMX.MAX_PNL_FACTOR_FOR_TRADERS), Maximize = true }.GetCallData(); var getMarketTokenPriceCall = new Call { Target = Arbitrum.AddressV2.Reader, CallData = getMarketTokenPriceCallData }; var getMarketTokenPriceMinCallData = new GetMarketTokenPriceFunction { DataStore = Arbitrum.AddressV2.DataStore, Market = marketProps, IndexTokenPrice = marketPrices.IndexTokenPrice, LongTokenPrice = marketPrices.LongTokenPrice, ShortTokenPrice = marketPrices.ShortTokenPrice, PnlFactorType = _encoder.HashString(Constants.GMX.MAX_PNL_FACTOR_FOR_TRADERS), Maximize = false }.GetCallData(); var getMarketTokenPriceMinCall = new Call { Target = Arbitrum.AddressV2.Reader, CallData = getMarketTokenPriceMinCallData }; readerCalls.Add(getMarketInfoCall); readerCalls.Add(getMarketTokenPriceCall); readerCalls.Add(getMarketTokenPriceMinCall); var longInterestUsingLongTokenCallData = new GetUintFunction { Key = _encoder.HashData([ ("bytes32", _encoder.HashString(Constants.GMX.OPEN_INTEREST)), ("address", market.MarketToken), ("address", market.LongToken), ("bool", true) ]) }.GetCallData(); var longInterestUsingLongTokenCall = new Call { Target = Arbitrum.AddressV2.DataStore, CallData = longInterestUsingLongTokenCallData }; var longInterestUsingShortTokenCallData = new GetUintFunction { Key = _encoder.HashData([ ("bytes32", _encoder.HashString(Constants.GMX.OPEN_INTEREST)), ("address", market.MarketToken), ("address", market.ShortToken), ("bool", true) ]) }.GetCallData(); var longInterestUsingShortTokenCall = new Call { Target = Arbitrum.AddressV2.DataStore, CallData = longInterestUsingShortTokenCallData }; var shortInterestUsingLongTokenCallData = new GetUintFunction { Key = _encoder.HashData([ ("bytes32", _encoder.HashString(Constants.GMX.OPEN_INTEREST)), ("address", market.MarketToken), ("address", market.LongToken), ("bool", false) ]) }.GetCallData(); var shortInterestUsingLongTokenCall = new Call { Target = Arbitrum.AddressV2.DataStore, CallData = shortInterestUsingLongTokenCallData }; var shortInterestUsingShortTokenCallData = new GetUintFunction { Key = _encoder.HashData([ ("bytes32", _encoder.HashString(Constants.GMX.OPEN_INTEREST)), ("address", market.MarketToken), ("address", market.ShortToken), ("bool", false) ]) }.GetCallData(); var shortInterestUsingShortTokenCall = new Call { Target = Arbitrum.AddressV2.DataStore, CallData = shortInterestUsingShortTokenCallData }; datastoreCalls.Add(longInterestUsingLongTokenCall); datastoreCalls.Add(longInterestUsingShortTokenCall); datastoreCalls.Add(shortInterestUsingLongTokenCall); datastoreCalls.Add(shortInterestUsingShortTokenCall); } var readerMulticall = new AggregateFunction { Calls = readerCalls }; var datastoreMulticall = new AggregateFunction { Calls = datastoreCalls }; var queryHandler = web3.Eth.GetContractQueryHandler(); var readerCallResults = await queryHandler .QueryDeserializingToObjectAsync(readerMulticall, Arbitrum.AddressV2.Multicall) .ConfigureAwait(false); var datastoreCallResults = await queryHandler .QueryDeserializingToObjectAsync(datastoreMulticall, Arbitrum.AddressV2.Multicall) .ConfigureAwait(false); int readerCallIndex = 0; int datastoreCallIndex = 0; foreach (var market in markets) { var marketInfo = new GetMarketInfoOutputDTO().DecodeOutput(readerCallResults.ReturnData[readerCallIndex++].ToHex()); var marketTokenPriceMax = new GetMarketTokenPriceOutputDTO().DecodeOutput(readerCallResults.ReturnData[readerCallIndex++] .ToHex()); var marketTokenPriceMin = new GetMarketTokenPriceOutputDTO().DecodeOutput(readerCallResults.ReturnData[readerCallIndex++] .ToHex()); var longInterestUsingLongToken = new GetUintOutputDTO() .DecodeOutput(datastoreCallResults.ReturnData[datastoreCallIndex++].ToHex()).ReturnValue1; var longInterestUsingShortToken = new GetUintOutputDTO() .DecodeOutput(datastoreCallResults.ReturnData[datastoreCallIndex++].ToHex()).ReturnValue1; var shortInterestUsingLongToken = new GetUintOutputDTO() .DecodeOutput(datastoreCallResults.ReturnData[datastoreCallIndex++].ToHex()).ReturnValue1; var shortInterestUsingShortToken = new GetUintOutputDTO() .DecodeOutput(datastoreCallResults.ReturnData[datastoreCallIndex++].ToHex()).ReturnValue1; var marketInfos = GmxMappers.Map(marketInfo.ReturnValue1); readerResult.Add(new GmxMarketInfo { Market = marketInfos, Infos = new GmxMarketInfos { LongInterestUsd = longInterestUsingLongToken + longInterestUsingShortToken, ShortInterestUsd = shortInterestUsingLongToken + shortInterestUsingShortToken }, MarketTokenPriceMax = GmxMappers.Map(marketTokenPriceMax), MarketTokenPriceMin = GmxMappers.Map(marketTokenPriceMin) }); } return readerResult; } private GmxMarketInfos DecodeFundingRateOutput(AggregateOutputDTO results, bool isSameCollaterals) { var marketDivisor = new BigInteger(isSameCollaterals ? 2 : 1); var longInterestUsingLongToken = new GetUintOutputDTO().DecodeOutput(results.ReturnData[0].ToHex()).ReturnValue1 / marketDivisor; var longInterestUsingShortToken = new GetUintOutputDTO().DecodeOutput(results.ReturnData[1].ToHex()).ReturnValue1 / marketDivisor; var shortInterestUsingLongToken = new GetUintOutputDTO().DecodeOutput(results.ReturnData[2].ToHex()).ReturnValue1 / marketDivisor; var shortInterestUsingShortToken = new GetUintOutputDTO().DecodeOutput(results.ReturnData[3].ToHex()).ReturnValue1 / marketDivisor; var longInterestUsd = longInterestUsingLongToken + longInterestUsingShortToken; var shortInterestUsd = shortInterestUsingLongToken + shortInterestUsingShortToken; var marketInfos = new GmxMarketInfos() { LongInterestUsd = longInterestUsd, ShortInterestUsd = shortInterestUsd }; return marketInfos; } public async Task> GetTokensData(Web3 web3, bool fastCall = false) { var tokens = TokenV2Service.GetTokens().ToList(); var balances = fastCall ? null : await GetTokenBalances(web3, tokens); var prices = await GetTokenPrices(web3, tokens); var result = new List(); foreach (var token in tokens) { try { BigInteger? balance = null; if (balances?.Balances != null && balances.Balances.ContainsKey(token.Address)) { balance = balances.Balances[token.Address]; } var price = prices[token.Address].Price; var data = new GmxTokenData() { Address = token.Address, Name = token.Name, Symbol = token.Symbol, Decimals = token.Decimals, Price = price, Balance = balance }; result.Add(data); } catch (Exception e) { Console.WriteLine(e); throw; } } return result; } private async Task> GetTokenPrices(Web3 web3, List tokens) { var result = new Dictionary(); var tickersPrices = await _oracleKeeperService.GetTickers(); foreach (var token in tokens) { var price = tickersPrices.FirstOrDefault(t => GmxV2Helpers.SameAddress(t.TokenAddress, token.Address)); if (price == null && GmxV2Helpers.SameAddress(token.Address, Arbitrum.Address.Zero)) { price = tickersPrices.FirstOrDefault(t => GmxV2Helpers.SameAddress(t.TokenAddress, Constants.GMX.TokenAddress.WETH)); } result.Add(token.Address, new GmxTokenPriceData() { Price = new MarketPrice() { Min = price != null ? BigInteger.Parse(price.MinPrice) : BigInteger.Zero, Max = price != null ? BigInteger.Parse(price.MaxPrice) : BigInteger.Zero, }, Balance = 0, TotalSupply = 0 }); } return result; } public async Task GetTokenBalances(Web3 web3, List tokens, string? account = null) { var result = new GmxTokenBalances(); result.Balances = new Dictionary(); var aggregateFunction = new AggregateFunction(); aggregateFunction.Calls = new List(); var index = 0; foreach (var token in tokens) { if (token.IsNative) { var nativeBalanceCallData = new GetEthBalanceFunction() { Addr = account ?? AddressUtil.ZERO_ADDRESS }.GetCallData(); aggregateFunction.Calls.Add(new Call { Target = Arbitrum.AddressV2.Multicall, CallData = nativeBalanceCallData }); } else { var balanceCallData = new BalanceOfFunction() { Owner = account ?? GmxHelpers.GetRandomAddress() }.GetCallData(); aggregateFunction.Calls.Add(new Call { Target = token.Address, CallData = balanceCallData }); } } var queryHandler = web3.Eth.GetContractQueryHandler(); var returnCalls = await queryHandler .QueryDeserializingToObjectAsync(aggregateFunction, Arbitrum.AddressV2.Multicall) .ConfigureAwait(false); // var balances = new BalanceOfOutputDTOBase().DecodeOutput(returnCalls.ReturnData[0].ToHex()); // result.Balances.Add(token.Address, balances.Balance); foreach (var callResult in returnCalls.ReturnData) { var balance = new BalanceOfOutputDTOBase().DecodeOutput(callResult.ToHex()); result.Balances.Add(tokens[index].Address, balance.Balance); index++; } return result; } public async Task> GetFundingRate(Web3 web3) { var market = await GetMarketInfo(web3); return GmxMappers.Map(market); } public async Task> GetFundingRates(Web3 web3) { var fundingRates = new List(); var marketDatas = await GetMarketInfosAsync(web3); foreach (var gmxMarketInfo in marketDatas) { if (!Constants.GMX.Config.DeltaNeutralTickers.Contains(GmxHelpers.GetTicker(gmxMarketInfo.Market.Symbol))) continue; var rates = GmxMappers.Map(gmxMarketInfo); fundingRates.AddRange(rates); } return fundingRates; } public async Task> GetOrders(Web3 web3, string publicAddress, Enums.Ticker ticker) { var marketAddress = GmxV2Helpers.GetMarketAddress(ticker); var orders = await GetAllOrders(web3, publicAddress); return orders.Where(o => o.MarketAddress == marketAddress).ToList(); } private async Task> GetAllOrders(Web3 web3, string publicAddress) { // Call Datastore and reader to construct open orders list var aggregateFunction = new AggregateFunction(); aggregateFunction.Calls = new List(); var countCallData = new GetBytes32CountFunction { SetKey = _encoder.HashData([ ("bytes32", _encoder.HashString(Constants.GMX.ACCOUNT_ORDER_LIST_KEY)), ("address", publicAddress), ]) }.GetCallData(); var countCall = new Call { Target = Arbitrum.AddressV2.DataStore, CallData = countCallData }; var keysCallData = new GetBytes32ValuesAtFunction { SetKey = _encoder.HashData([ ("bytes32", _encoder.HashString(Constants.GMX.ACCOUNT_ORDER_LIST_KEY)), ("address", publicAddress), ]), Start = 0, End = _defaultCount }.GetCallData(); var keysCall = new Call { Target = Arbitrum.AddressV2.DataStore, CallData = keysCallData }; aggregateFunction.Calls.Add(countCall); aggregateFunction.Calls.Add(keysCall); var orderCallData = new GetAccountOrdersFunction { DataStore = Arbitrum.AddressV2.DataStore, Account = publicAddress, Start = 0, End = _defaultCount }.GetCallData(); var orderCall = new Call { Target = Arbitrum.AddressV2.Reader, CallData = orderCallData }; aggregateFunction.Calls.Add(orderCall); var queryHandler = web3.Eth.GetContractQueryHandler(); var returnCalls = await queryHandler .QueryDeserializingToObjectAsync(aggregateFunction, Arbitrum.AddressV2.Multicall) .ConfigureAwait(false); var count = new GetBytes32CountOutputDTO().DecodeOutput(returnCalls.ReturnData[0].ToHex()).ReturnValue1; var keys = new GetBytes32ValuesAtOutputDTO().DecodeOutput(returnCalls.ReturnData[1].ToHex()).ReturnValue1; var orders = new GetAccountOrdersOutputDTO().DecodeOutput(returnCalls.ReturnData[2].ToHex()).ReturnValue1; var result = new List(); for (int i = 0; i < count; i++) { var key = keys[i]; var order = orders[i]; var date = DateHelpers.GetFromUnixTimestamp((int)order.Numbers.UpdatedAtBlock); var orderData = new GmxV2Order() { Key = key, Account = order.Addresses.Account, Receiver = order.Addresses.Receiver, CallbackContract = order.Addresses.CallbackContract, MarketAddress = order.Addresses.Market, InitialCollateralTokenAddress = order.Addresses.InitialCollateralToken, SwapPath = order.Addresses.SwapPath, SizeDeltaUsd = order.Numbers.SizeDeltaUsd, InitialCollateralDeltaAmount = order.Numbers.InitialCollateralDeltaAmount, ContractTriggerPrice = order.Numbers.TriggerPrice, ContractAcceptablePrice = order.Numbers.AcceptablePrice, ExecutionFee = order.Numbers.ExecutionFee, CallbackGasLimit = order.Numbers.CallbackGasLimit, MinOutputAmount = order.Numbers.MinOutputAmount, UpdatedAtBlock = order.Numbers.UpdatedAtBlock, IsLong = order.Flags.IsLong, ShouldUnwrapNativeToken = order.Flags.ShouldUnwrapNativeToken, IsFrozen = order.Flags.IsFrozen, OrderType = MiscExtensions.ParseEnum(order.Numbers.OrderType.ToString()), DecreasePositionSwapType = MiscExtensions.ParseEnum(order.Numbers.DecreasePositionSwapType .ToString()), Data = order, Date = date }; result.Add(orderData); } return result; } public async Task CancelOrders(Web3 web3, string accountKey, Enums.Ticker ticker) { var orders = await new GmxV2Service().GetOrders(web3, accountKey, ticker); if (orders.Count == 0) { return true; } // Somehow, it's only possible to cancel one order at a time var results = new List(); foreach (var order in orders) { var multicallFunction = new MulticallFunction { Data = [] }; var cancelOrderCallData = new CancelOrderFunction { Key = order.Key }; multicallFunction.Data.Add(cancelOrderCallData.GetCallData()); var queryHandler = web3.Eth.GetContractTransactionHandler(); try { var returnCalls = await queryHandler.SendRequestAndWaitForReceiptAsync(Arbitrum.AddressV2.ExchangeRouter, multicallFunction); results.Add(returnCalls.Status.Value == 1); } catch (Exception e) { results.Add(false); } } return results.All(r => r) && results.Count == orders.Count; } public async Task IncreasePosition(Web3 web3, string accountKey, Enums.Ticker ticker, Enums.TradeDirection direction, decimal price, decimal quantity, decimal? leverage) { var defaultLeverage = leverage ?? 1; var marketAddress = GmxV2Helpers.GetMarketAddress(ticker); var fromToken = TokenV2Service.GetByTicker(ticker); var multiCallFunction = new MulticallFunction(); multiCallFunction.Data = new List(); var executionFee = await GetExecutionFee(web3, GmxV2Enums.TradeType.Increase); var usdcToken = TokenV2Service.TOKENS.First(t => t.Symbol == Enums.Ticker.USDC.ToString()); var collateral = price * quantity; var initialCollateralAmount = GmxV2Helpers.ParseValue(collateral, usdcToken.Decimals); var triggerPrice = GmxV2Helpers.ConvertToContractPrice(price, fromToken.Decimals, true); var sizeDeltaUsd = GmxV2Helpers.ParseValue(collateral * defaultLeverage, 30); var acceptablePrice = GmxV2Helpers.GetAcceptablePrice(price, direction == Enums.TradeDirection.Long); var totalWntAmount = executionFee.FeeAmount; var sendWntFunction = new SendWntFunction { Receiver = Arbitrum.AddressV2.OrderVault, Amount = totalWntAmount }; multiCallFunction.Data.Add(sendWntFunction.GetCallData()); var sendTokenCallData = new SendTokensFunction { Token = usdcToken.Address, Receiver = Arbitrum.AddressV2.OrderVault, Amount = initialCollateralAmount }; multiCallFunction.Data.Add(sendTokenCallData.GetCallData()); var createOrderCallData = new CreateOrderFunction { Params = new CreateOrderParams { Addresses = new CreateOrderParamsAddresses { CallbackContract = Arbitrum.Address.Zero, CancellationReceiver = Arbitrum.Address.Zero, InitialCollateralToken = usdcToken.Address, Market = marketAddress, Receiver = accountKey, SwapPath = new List(), UiFeeReceiver = Arbitrum.Address.Zero }, AutoCancel = false, IsLong = direction == Enums.TradeDirection.Long, DecreasePositionSwapType = 0, Numbers = new CreateOrderParamsNumbers { AcceptablePrice = acceptablePrice, CallbackGasLimit = 0, ExecutionFee = executionFee.FeeAmount, InitialCollateralDeltaAmount = initialCollateralAmount, // Should be 0 somehow SizeDeltaUsd = sizeDeltaUsd, TriggerPrice = triggerPrice, MinOutputAmount = 0, }, OrderType = 3, ReferralCode = new byte[32], ShouldUnwrapNativeToken = false } }; multiCallFunction.Data.Add(createOrderCallData.GetCallData()); multiCallFunction.Gas = new HexBigInteger(8_000_000); var gasPriceData = await GetCurrentGasPrice(web3); multiCallFunction.GasPrice = new HexBigInteger(gasPriceData); multiCallFunction.AmountToSend = totalWntAmount; try { var exchangeRouterService = new ExchangeRouterService(web3, Arbitrum.AddressV2.ExchangeRouter); var receipt = await exchangeRouterService.MulticallRequestAndWaitForReceiptAsync(multiCallFunction); var trade = new Trade(DateTime.UtcNow, direction, Enums.TradeStatus.Requested, Enums.TradeType.Limit, ticker, quantity, price, leverage, receipt.TransactionHash, ""); return trade; } catch (Exception e) { Console.WriteLine($"Error increasing position: {e.Message}"); throw; } } public async Task DecreasePosition( Web3 web3, string accountAddress, Enums.Ticker ticker, Enums.TradeDirection direction, decimal closePrice, decimal quantity, decimal leverage) { var trade = new Trade(DateTime.UtcNow, direction, Enums.TradeStatus.Cancelled, Enums.TradeType.StopMarket, ticker, quantity, closePrice, leverage, "", ""); var marketAddress = GmxV2Helpers.GetMarketAddress(ticker); var currentPosition = (await GetGmxPositionsV2(web3, accountAddress)).FirstOrDefault(p => GmxV2Helpers.SameAddress(marketAddress, p.MarketAddress)); if (currentPosition == null || currentPosition.SizeInUsd == 0) return trade; // var collateralToken = TokenV2Service.TOKENS.First(t => t.Symbol == "USDC"); var executionFee = await GetExecutionFee(web3, GmxV2Enums.TradeType.Decrease); var acceptablePrice = GmxV2Helpers.GetAcceptablePrice(closePrice, direction == Enums.TradeDirection.Long); // var collateral = closePrice * quantity; // var initialCollateralAmount = GmxV2Helpers.ParseValue(collateral, collateralToken.Decimals); var quantityLeveraged = quantity * leverage; // var sizeDeltaUsd = GmxV2Helpers.ParseValue(quantityLeveraged * closePrice, 0); var sendWntFunction = new SendWntFunction { Receiver = Arbitrum.AddressV2.OrderVault, Amount = executionFee.FeeAmount }.GetCallData(); var createOrderFunction = new CreateOrderFunction { Params = new CreateOrderParams { Addresses = new CreateOrderParamsAddresses { Market = marketAddress, InitialCollateralToken = currentPosition.CollateralTokenAddress, CancellationReceiver = Arbitrum.Address.Zero, Receiver = accountAddress, CallbackContract = Arbitrum.Address.Zero, SwapPath = new List(), UiFeeReceiver = Arbitrum.Address.Zero }, Numbers = new CreateOrderParamsNumbers { SizeDeltaUsd = currentPosition.SizeInUsd, InitialCollateralDeltaAmount = currentPosition.CollateralAmount, // No collateral withdrawal TriggerPrice = 0, AcceptablePrice = acceptablePrice, ExecutionFee = executionFee.FeeAmount, MinOutputAmount = 0, ValidFromTime = 0, CallbackGasLimit = 0 }, OrderType = 4, DecreasePositionSwapType = 1, IsLong = currentPosition.IsLong, ShouldUnwrapNativeToken = false, ReferralCode = new byte[32] } }; // 5. Configure transaction var gasPriceData = await GetCurrentGasPrice(web3); var multicallFunction = new MulticallFunction { Data = new List { sendWntFunction, createOrderFunction.GetCallData() }, AmountToSend = executionFee.FeeAmount, Gas = new HexBigInteger(7_000_000), GasPrice = new HexBigInteger(gasPriceData) }; try { // 7. Execute transaction var exchangeRouterService = new ExchangeRouterService(web3, Arbitrum.AddressV2.ExchangeRouter); var receipt = await exchangeRouterService.MulticallRequestAndWaitForReceiptAsync( multicallFunction ); return new Trade( DateTime.UtcNow, direction, Enums.TradeStatus.Requested, Enums.TradeType.Market, ticker, quantity, closePrice, leverage, receipt.TransactionHash, "" ); } catch (Exception ex) { Console.WriteLine($"Decrease position failed: {ex.Message}"); trade.SetMessage(ex.Message); return trade; } } // V2-specific helper methods private (BigInteger triggerPrice, BigInteger acceptablePrice) GetDecreaseOrderPrices( decimal price, bool isLong, int priceDecimals, decimal slippageBps) { var slippageMultiplier = isLong ? 1m - (slippageBps / 10000m) : 1m + (slippageBps / 10000m); return ( GmxV2Helpers.ConvertToContractPrice(price, priceDecimals), GmxV2Helpers.ConvertToContractPrice(price * slippageMultiplier, 30) ); } private async Task GetCurrentGasPrice(Web3 web3) { var gasPrice = await web3.Eth.GasPrice.SendRequestAsync(); return new HexBigInteger(gasPrice.Value * 115 / 100); // Add 15% buffer } public async Task GetGasLimit(Web3 web3) { var aggregateFunction = new AggregateFunction(); aggregateFunction.Calls = new List(); var depositSingleTokenCall = new Call { Target = Arbitrum.AddressV2.DataStore, CallData = new GetUintFunction { Key = _encoder.HashData([ ("bytes32", _encoder.HashString(Constants.GMX.DEPOSIT_GAS_LIMIT_KEY)), ("bool", true) ]) }.GetCallData() }; var depositMultiTokenCall = new Call { Target = Arbitrum.AddressV2.DataStore, CallData = new GetUintFunction { Key = _encoder.HashData([ ("bytes32", _encoder.HashString(Constants.GMX.DEPOSIT_GAS_LIMIT_KEY)), ("bool", false) ]) }.GetCallData() }; var withdrawalMultiTokenCall = new Call { Target = Arbitrum.AddressV2.DataStore, CallData = new GetUintFunction { Key = _encoder.HashData([ ("bytes32", _encoder.HashString(Constants.GMX.WITHDRAWAL_GAS_LIMIT_KEY)), ]) }.GetCallData() }; var shiftCall = new Call { Target = Arbitrum.AddressV2.DataStore, CallData = new GetUintFunction { Key = _encoder.HashString(Constants.GMX.SHIFT_GAS_LIMIT_KEY) }.GetCallData() }; var singleSwapCall = new Call { Target = Arbitrum.AddressV2.DataStore, CallData = new GetUintFunction { Key = _encoder.HashString(Constants.GMX.SINGLE_SWAP_GAS_LIMIT_KEY) }.GetCallData() }; var swapOrderCall = new Call { Target = Arbitrum.AddressV2.DataStore, CallData = new GetUintFunction { Key = _encoder.HashString(Constants.GMX.SWAP_ORDER_GAS_LIMIT_KEY) }.GetCallData() }; var increaseOrderCall = new Call { Target = Arbitrum.AddressV2.DataStore, CallData = new GetUintFunction { Key = _encoder.HashString(Constants.GMX.INCREASE_ORDER_GAS_LIMIT_KEY) }.GetCallData() }; var decreaseOrderCall = new Call { Target = Arbitrum.AddressV2.DataStore, CallData = new GetUintFunction { Key = _encoder.HashString(Constants.GMX.DECREASE_ORDER_GAS_LIMIT_KEY) }.GetCallData() }; var estimatedGasFeeBaseAmountCall = new Call { Target = Arbitrum.AddressV2.DataStore, CallData = new GetUintFunction { Key = _encoder.HashString(Constants.GMX.ESTIMATED_GAS_FEE_BASE_AMOUNT) }.GetCallData() }; var estimatedGasFeePerOraclePriceCall = new Call { Target = Arbitrum.AddressV2.DataStore, CallData = new GetUintFunction { Key = _encoder.HashString(Constants.GMX.ESTIMATED_GAS_FEE_PER_ORACLE_PRICE) }.GetCallData() }; var estimatedFeeMultiplierFactorCall = new Call { Target = Arbitrum.AddressV2.DataStore, CallData = new GetUintFunction { Key = _encoder.HashString(Constants.GMX.ESTIMATED_GAS_FEE_MULTIPLIER_FACTOR) }.GetCallData() }; aggregateFunction.Calls.Add(depositSingleTokenCall); aggregateFunction.Calls.Add(depositMultiTokenCall); aggregateFunction.Calls.Add(withdrawalMultiTokenCall); aggregateFunction.Calls.Add(shiftCall); aggregateFunction.Calls.Add(singleSwapCall); aggregateFunction.Calls.Add(swapOrderCall); aggregateFunction.Calls.Add(increaseOrderCall); aggregateFunction.Calls.Add(decreaseOrderCall); aggregateFunction.Calls.Add(estimatedGasFeeBaseAmountCall); aggregateFunction.Calls.Add(estimatedGasFeePerOraclePriceCall); aggregateFunction.Calls.Add(estimatedFeeMultiplierFactorCall); var queryHandler = web3.Eth.GetContractQueryHandler(); var returnCalls = await queryHandler .QueryDeserializingToObjectAsync(aggregateFunction, Arbitrum.AddressV2.Multicall) .ConfigureAwait(false); var gasLimit = new GmxV2GasLimit(); gasLimit.DepositSingleToken = new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[0].ToHex()).ReturnValue1; gasLimit.DepositMultiToken = new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[1].ToHex()).ReturnValue1; gasLimit.WithdrawalMultiToken = new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[2].ToHex()).ReturnValue1; gasLimit.Shift = new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[3].ToHex()).ReturnValue1; gasLimit.SingleSwap = new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[4].ToHex()).ReturnValue1; gasLimit.SwapOrder = new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[5].ToHex()).ReturnValue1; gasLimit.IncreaseOrder = new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[6].ToHex()).ReturnValue1; gasLimit.DecreaseOrder = new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[7].ToHex()).ReturnValue1; gasLimit.EstimatedGasFeeBaseAmount = new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[8].ToHex()).ReturnValue1; gasLimit.EstimatedGasFeePerOraclePrice = new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[9].ToHex()).ReturnValue1; gasLimit.EstimatedFeeMultiplierFactor = new GetUintOutputDTO().DecodeOutput(returnCalls.ReturnData[10].ToHex()).ReturnValue1; return gasLimit; } public async Task GetExecutionFee( Web3 web3, GmxV2Enums.TradeType tradeType, int swapPathLength = 0) { const int PRECISION = 30; // 1. Get gas configuration from chain var gasLimits = await GetGasLimits(web3); var gasPrice = await web3.Eth.GasPrice.SendRequestAsync(); var tokensData = await GetTokensData(web3); // 2. Calculate oracle price count (matches SDK logic) var oraclePriceCount = (swapPathLength > 0 ? 2 : 1) * (swapPathLength + 1); // 3. Get base gas limit with oracle cost var operationGasLimit = tradeType switch { GmxV2Enums.TradeType.Increase => 6_500_000, GmxV2Enums.TradeType.Decrease => 5_500_000, _ => throw new ArgumentException("Invalid trade type") }; var feeAmount = operationGasLimit * gasPrice.Value; // 5. Apply multiplier with precise integer math var applyFactor = (operationGasLimit * gasLimits.EstimatedFeeMultiplierFactor) / BigInteger.Pow(10, PRECISION); // 7. Get native token data var nativeToken = tokensData.First(t => t.Symbol.Equals("ETH", StringComparison.OrdinalIgnoreCase) || t.Symbol.Equals("WETH", StringComparison.OrdinalIgnoreCase)); return new GmxV2ExecutionFee { FeeAmount = feeAmount, // FeeUsd = ConvertToUsd(feeAmount, nativeToken.Decimals, nativeToken.Price.Min), NativeToken = nativeToken }; } // Helper methods private BigInteger ConvertToUsd(BigInteger feeAmount, int decimals, decimal tokenPrice) { var tokenValue = (decimal)feeAmount / (decimal)BigInteger.Pow(10, decimals); return (BigInteger)(tokenValue * tokenPrice); } private async Task GetGasLimits(Web3 web3) { // Implement chain-specific gas limit configuration return new GmxV2GasLimit { EstimatedGasFeeBaseAmount = 200_000, EstimatedGasFeePerOraclePrice = 50_000, EstimatedFeeMultiplierFactor = BigInteger.Parse("1000000000000000000") // 1e18 }; } // Helper methods private BigInteger EstimateOrderOraclePriceCount(int swapPathLength) { // Matches SDK's estimateOrderOraclePriceCount return (swapPathLength > 0 ? 2 : 1) * (swapPathLength + 1); } private async Task GetGasPriceInWei(Web3 web3) { var gasPrice = await web3.Eth.GasPrice.SendRequestAsync(); return gasPrice.Value; // Already in wei } private BigInteger GetOperationGasLimit(GmxV2Enums.TradeType tradeType) { // Implement based on GMX's internal logic: // - Increase: 6_500_000 // - Decrease: 7_000_000 // - Swap: 8_000_000 return tradeType switch { GmxV2Enums.TradeType.Increase => 6_500_000, GmxV2Enums.TradeType.Decrease => 7_000_000, GmxV2Enums.TradeType.Swap => 8_000_000, _ => throw new ArgumentException("Invalid trade type") }; } public BigInteger GetEstimatedGasLimit(GmxV2GasLimit gasLimit, int swapCount, GmxV2Enums.TradeType tradeType) { switch (tradeType) { case GmxV2Enums.TradeType.Swap: break; case GmxV2Enums.TradeType.Increase: // To handle collateral swap must implement get swap path return EstimateExecuteIncreaseOrderGasLimit(gasLimit, swapCount); case GmxV2Enums.TradeType.Decrease: return EstimateExecuteDecreaseOrderGasLimit(gasLimit, swapCount); break; default: throw new ArgumentOutOfRangeException(nameof(tradeType), tradeType, null); } return new BigInteger(); } private BigInteger EstimateExecuteIncreaseOrderGasLimit(GmxV2GasLimit gasLimit, int? swapCount = null, BigInteger? callbackGasLimit = null) { var gasPerSwap = gasLimit.SingleSwap; var swapsCount = new BigInteger(swapCount ?? 0); var totalGasLimit = gasLimit.IncreaseOrder + gasPerSwap * swapsCount + (callbackGasLimit ?? BigInteger.Zero); return totalGasLimit; } private BigInteger EstimateExecuteDecreaseOrderGasLimit(GmxV2GasLimit gasLimit, int swapCount, BigInteger? callbackGasLimit = null, GmxV2Enums.DecreasePositionSwapType? decreaseSwapType = null) { var gasPerSwap = gasLimit.SingleSwap; var swapsCount = new BigInteger(swapCount); if (decreaseSwapType != GmxV2Enums.DecreasePositionSwapType.NoSwap) { swapsCount += 1; } var totalGasLimit = gasLimit.DecreaseOrder + gasPerSwap * swapsCount + (callbackGasLimit ?? BigInteger.Zero); return totalGasLimit; } public async Task GetTrade(Web3 web3, string publicAddress, Enums.Ticker ticker) { var position = await GetGmxPositionsV2(web3, publicAddress); var positionsPerTicker = position.First(p => GmxV2Helpers.SameAddress(p.MarketAddress, GmxV2Helpers.GetMarketAddress(ticker))); return GmxV2Mappers.Map(positionsPerTicker, ticker); } private async Task> GetGmxPositionsV2(Web3 web3, string account) { var marketsInfoData = await GetMarketInfosAsync(web3); var tokensData = await GetTokensData(web3, true); var (marketKeys, marketPrices, allPositionsKeys) = GetKeysAndPrices(marketsInfoData, tokensData, account); var getAccountPositionInfoListFunction = new GetAccountPositionInfoListFunction { DataStore = Arbitrum.AddressV2.DataStore, ReferralStorage = Arbitrum.AddressV2.ReferralStorage, Account = account, Markets = marketKeys, MarketPrices = GmxV2Mappers.Map(marketPrices), UiFeeReceiver = Arbitrum.Address.Zero, Start = new BigInteger(0), End = new BigInteger(1000) }.GetCallData(); var getPositionCall = new Call() { Target = Arbitrum.AddressV2.SyntheticsReader, CallData = getAccountPositionInfoListFunction }; var aggregateFunction = new AggregateFunction(); aggregateFunction.Calls = new List(); aggregateFunction.Calls.Add(getPositionCall); var queryHandler = web3.Eth.GetContractQueryHandler(); var returnCalls = await queryHandler .QueryDeserializingToObjectAsync(aggregateFunction, Arbitrum.AddressV2.Multicall) .ConfigureAwait(false); var hexResult = returnCalls.ReturnData[0].ToHex(); var positions = new GetAccountPositionInfoListOutputDTO().DecodeOutput(hexResult).ReturnValue1; var result = new List(); foreach (var positionInfo in positions) { var position = positionInfo.Position; var fees = positionInfo.Fees; var addresses = position.Addresses; var numbers = position.Numbers; var flags = position.Flags; var marketInfo = marketsInfoData.First(m => GmxV2Helpers.SameAddress(m.Market.MarketToken, addresses.Market)); var tokenData = TokenV2Service.GetByTicker(Enums.Ticker.USDC); if (numbers.IncreasedAtTime == 0) { continue; } var positionKey = GmxV2Helpers.GetPositionKey(addresses.Account, addresses.Market, addresses.CollateralToken, flags.IsLong); var contractPositionKey = _encoder.HashData([ ("address", addresses.Account), ("address", addresses.Market), ("address", addresses.CollateralToken), ("bool", flags.IsLong) ]); var gmxPosition = new GmxV2Position { Key = positionKey, ContractKey = contractPositionKey.ToString(), Account = addresses.Account, MarketAddress = addresses.Market, CollateralTokenAddress = addresses.CollateralToken, SizeInUsd = numbers.SizeInUsd, SizeInTokens = numbers.SizeInTokens, CollateralAmount = numbers.CollateralAmount, IncreasedAtTime = numbers.IncreasedAtTime, DecreasedAtTime = numbers.DecreasedAtTime, IsLong = flags.IsLong, PendingBorrowingFeesUsd = fees.Borrowing.BorrowingFeeUsd, FundingFeeAmount = fees.Funding.FundingFeeAmount, ClaimableLongTokenAmount = fees.Funding.ClaimableLongTokenAmount, ClaimableShortTokenAmount = fees.Funding.ClaimableShortTokenAmount, MarketInfo = marketInfo, TokenData = tokenData }; result.Add(gmxPosition); } return result; } private (List marketKeys, List marketPrices, List allPositionsKeys) GetKeysAndPrices( List marketsInfoData, List tokensData, string account) { var values = ( marketKeys: new List(), marketPrices: new List(), allPositionsKeys: new List() ); if (string.IsNullOrEmpty(account) || marketsInfoData == null || tokensData == null) { return values; } foreach (var market in marketsInfoData) { var marketPrices = GmxHelpers.GetContractMarketPrices(tokensData, market.Market); if (marketPrices == null || market.Market.IsSpotOnly) { continue; } values.marketKeys.Add(market.Market.MarketToken); values.marketPrices.Add(marketPrices); var collaterals = market.Market.IsSameCollaterals ? new List { market.Market.LongToken } : new List { market.Market.LongToken, market.Market.ShortToken }; foreach (var collateralAddress in collaterals) { foreach (var isLong in new[] { true, false }) { var positionKey = GetPositionKeyV2(account, market.Market.MarketToken, collateralAddress, isLong); values.allPositionsKeys.Add(positionKey); } } } return values; } // Helper Methods private static string GetPositionKeyV2(string account, string market, string collateralToken, bool isLong) { return $"{account}-{market}-{collateralToken}-{(isLong ? "true" : "false")}"; } }