* Move PrivateKeys.cs * Update gitignore * Update gitignore * updt * Extract GmxServiceTests.cs * Refact * update todo * Update code * Fix hashdata * Replace static token hashed datas * Set allowance * Add get orders * Add get orders tests * Add ignore * add close orders * revert * Add get gas limit * Start increasePosition. Todo: Finish GetExecutionFee and estimateGas * little refact * Update gitignore * Fix namespaces and clean repo * Add tests samples * Add execution fee * Add increase position * Handle backtest on the frontend * Add tests * Update increase * Test increase * fix increase * Fix size * Start get position * Update get positions * Fix get position * Update rpc and trade mappers * Finish close position * Fix leverage
1375 lines
55 KiB
C#
1375 lines
55 KiB
C#
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<object> 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<List<GmxMarket>> 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<Call>();
|
|
aggregateFunction.Calls.Add(call1);
|
|
|
|
var queryHandler = web3.Eth.GetContractQueryHandler<AggregateFunction>();
|
|
var returnCalls = await queryHandler
|
|
.QueryDeserializingToObjectAsync<AggregateOutputDTO>(aggregateFunction, Arbitrum.AddressV2.Multicall)
|
|
.ConfigureAwait(false);
|
|
|
|
var markets = new GetMarketsOutputDTO().DecodeOutput(returnCalls.ReturnData[0].ToHex());
|
|
var result = new List<GmxMarket>();
|
|
foreach (var market in markets.ReturnValue1)
|
|
{
|
|
try
|
|
{
|
|
result.Add(GmxMappers.MapInfos(market));
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public async Task<GmxMarketInfo> 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<Call>();
|
|
dataStoreMulticall.Calls.Add(longInterestUsingLongTokenCall);
|
|
dataStoreMulticall.Calls.Add(longInterestUsingShortTokenCall);
|
|
dataStoreMulticall.Calls.Add(shortInterestUsingLongTokenCall);
|
|
dataStoreMulticall.Calls.Add(shortInterestUsingShortTokenCall);
|
|
var queryHandler = web3.Eth.GetContractQueryHandler<AggregateFunction>();
|
|
var returnCalls = await queryHandler
|
|
.QueryDeserializingToObjectAsync<AggregateOutputDTO>(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<GmxMarketTokenPrice> 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<bool> 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<BigInteger> 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<List<GmxMarketInfo>> GetMarketInfosAsync(Web3 web3)
|
|
{
|
|
var markets = await GetMarketsAsync(web3);
|
|
var readerResult = new List<GmxMarketInfo>();
|
|
var readerCalls = new List<Call>();
|
|
var datastoreCalls = new List<Call>();
|
|
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<AggregateFunction>();
|
|
var readerCallResults = await queryHandler
|
|
.QueryDeserializingToObjectAsync<AggregateOutputDTO>(readerMulticall, Arbitrum.AddressV2.Multicall)
|
|
.ConfigureAwait(false);
|
|
var datastoreCallResults = await queryHandler
|
|
.QueryDeserializingToObjectAsync<AggregateOutputDTO>(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<List<GmxTokenData>> 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<GmxTokenData>();
|
|
|
|
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<Dictionary<string, GmxTokenPriceData>> GetTokenPrices(Web3 web3, List<GmxToken> tokens)
|
|
{
|
|
var result = new Dictionary<string, GmxTokenPriceData>();
|
|
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<GmxTokenBalances> GetTokenBalances(Web3 web3, List<GmxToken> tokens, string? account = null)
|
|
{
|
|
var result = new GmxTokenBalances();
|
|
result.Balances = new Dictionary<string, BigInteger>();
|
|
var aggregateFunction = new AggregateFunction();
|
|
aggregateFunction.Calls = new List<Call>();
|
|
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<AggregateFunction>();
|
|
var returnCalls = await queryHandler
|
|
.QueryDeserializingToObjectAsync<AggregateOutputDTO>(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<List<FundingRate>> GetFundingRate(Web3 web3)
|
|
{
|
|
var market = await GetMarketInfo(web3);
|
|
return GmxMappers.Map(market);
|
|
}
|
|
|
|
public async Task<List<FundingRate>> GetFundingRates(Web3 web3)
|
|
{
|
|
var fundingRates = new List<FundingRate>();
|
|
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<List<GmxV2Order>> 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<List<GmxV2Order>> GetAllOrders(Web3 web3, string publicAddress)
|
|
{
|
|
// Call Datastore and reader to construct open orders list
|
|
var aggregateFunction = new AggregateFunction();
|
|
aggregateFunction.Calls = new List<Call>();
|
|
|
|
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<AggregateFunction>();
|
|
var returnCalls = await queryHandler
|
|
.QueryDeserializingToObjectAsync<AggregateOutputDTO>(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<GmxV2Order>();
|
|
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<GmxV2OrderType>(order.Numbers.OrderType.ToString()),
|
|
DecreasePositionSwapType =
|
|
MiscExtensions.ParseEnum<GmxV2DecreasePositionSwapType>(order.Numbers.DecreasePositionSwapType
|
|
.ToString()),
|
|
Data = order,
|
|
Date = date
|
|
};
|
|
result.Add(orderData);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public async Task<bool> 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<bool>();
|
|
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<MulticallFunction>();
|
|
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<Trade> 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<byte[]>();
|
|
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<string>(),
|
|
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<Trade> 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<string>(),
|
|
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<byte[]> { 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<HexBigInteger> GetCurrentGasPrice(Web3 web3)
|
|
{
|
|
var gasPrice = await web3.Eth.GasPrice.SendRequestAsync();
|
|
return new HexBigInteger(gasPrice.Value * 115 / 100); // Add 15% buffer
|
|
}
|
|
|
|
public async Task<GmxV2GasLimit> GetGasLimit(Web3 web3)
|
|
{
|
|
var aggregateFunction = new AggregateFunction();
|
|
aggregateFunction.Calls = new List<Call>();
|
|
|
|
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<AggregateFunction>();
|
|
var returnCalls = await queryHandler
|
|
.QueryDeserializingToObjectAsync<AggregateOutputDTO>(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<GmxV2ExecutionFee> 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<GmxV2GasLimit> 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<BigInteger> 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<Trade> 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<List<GmxV2Position>> 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<Call>();
|
|
aggregateFunction.Calls.Add(getPositionCall);
|
|
|
|
var queryHandler = web3.Eth.GetContractQueryHandler<AggregateFunction>();
|
|
var returnCalls = await queryHandler
|
|
.QueryDeserializingToObjectAsync<AggregateOutputDTO>(aggregateFunction, Arbitrum.AddressV2.Multicall)
|
|
.ConfigureAwait(false);
|
|
|
|
var hexResult = returnCalls.ReturnData[0].ToHex();
|
|
|
|
var positions = new GetAccountPositionInfoListOutputDTO().DecodeOutput(hexResult).ReturnValue1;
|
|
var result = new List<GmxV2Position>();
|
|
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<string> marketKeys, List<MarketPrices> marketPrices, List<string> allPositionsKeys) GetKeysAndPrices(
|
|
List<GmxMarketInfo> marketsInfoData, List<GmxTokenData> tokensData, string account)
|
|
{
|
|
var values = (
|
|
marketKeys: new List<string>(),
|
|
marketPrices: new List<MarketPrices>(),
|
|
allPositionsKeys: new List<string>()
|
|
);
|
|
|
|
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<string> { market.Market.LongToken }
|
|
: new List<string> { 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")}";
|
|
}
|
|
} |