Gmx v2 - Funding rates (#6)

* Setup GMX v2

* Add get markets

* Map token with service

* Add get market info data

* Add get markets

* Add get market token prices

* Get markets infos multicall

* Try call datastore

* Add some tests to figure out why datastore call dont work

* Update funding rates

* clean
This commit is contained in:
Oda
2024-08-17 06:50:18 +07:00
committed by GitHub
parent b4087753c7
commit 68aa7fff5d
75 changed files with 8979 additions and 608 deletions

View File

@@ -0,0 +1,531 @@
using System.Numerics;
using Managing.ABI.GmxV2.DataStore;
using Managing.ABI.GmxV2.DataStore.ContractDefinition;
using Managing.ABI.GmxV2.Multicall3.ContractDefinition;
using Managing.ABI.GmxV2.Reader;
using Managing.ABI.GmxV2.Reader.ContractDefinition;
using Managing.Common;
using Managing.Domain.Statistics;
using Managing.Infrastructure.Evm.Models.Gmx.v2;
using Managing.Infrastructure.Evm.Referentials;
using Managing.Infrastructure.Evm.Services.Gmx;
using Nethereum.Contracts;
using Nethereum.Contracts.Standards.ERC20.ContractDefinition;
using Nethereum.Hex.HexConvertors.Extensions;
using Nethereum.Web3;
public class GmxV2Service
{
private readonly GmxV2Helpers _helpers = new GmxV2Helpers();
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 = 100
}.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 marketKeys = GmxKeysService.GetMarketKeys(market.MarketToken);
var longInterestUsingLongTokenCallData = new GetUintFunction()
{
Key = marketKeys.LongInterestUsingLongToken.HexToByteArray()
}.GetCallData();
var longInterestUsingLongTokenCall = new Call
{
Target = Arbitrum.AddressV2.DataStore,
CallData = longInterestUsingLongTokenCallData
};
var longInterestUsingShortTokenCallData = new GetUintFunction()
{
Key = marketKeys.LongInterestUsingShortToken.HexToByteArray()
}.GetCallData();
var longInterestUsingShortTokenCall = new Call
{
Target = Arbitrum.AddressV2.DataStore,
CallData = longInterestUsingShortTokenCallData
};
var shortInterestUsingLongTokenCallData = new GetUintFunction()
{
Key = marketKeys.ShortInterestUsingLongToken.HexToByteArray()
}.GetCallData();
var shortInterestUsingLongTokenCall = new Call
{
Target = Arbitrum.AddressV2.DataStore,
CallData = shortInterestUsingLongTokenCallData
};
var shortInterestUsingShortTokenCallData = new GetUintFunction()
{
Key = marketKeys.ShortInterestUsingShortToken.HexToByteArray()
}.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,
_helpers.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 = _helpers.HashString(Constants.GMX.MARKET_DISABLED_KEY);
var response = await dataStoreService.GetBoolQueryAsync(_helpers.HashData(
new[] { "bytes32", "address" },
new object[] { hashString, market.MarketToken }));
return response;
}
public async Task<BigInteger> GetLongInterestAmount(Web3 web3)
{
var dataStoreService = new DataStoreService(web3, Arbitrum.AddressV2.DataStore);
var market = (await GetMarketsAsync(web3)).First();
var keys = GmxKeysService.GetMarketKeys(market.MarketToken);
var response = await dataStoreService.GetUintQueryAsync(keys.LongInterestUsingLongToken.HexToByteArray());
return response;
}
public async Task<List<GmxMarketInfo>> GetMarketInfosAsync(Web3 web3)
{
var markets = await GetMarketsAsync(web3);
var readerResult = new List<GmxMarketInfo>();
foreach (var market in markets)
{
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 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 = _helpers.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 = _helpers.HashString(Constants.GMX.MAX_PNL_FACTOR_FOR_TRADERS),
Maximize = false
}.GetCallData();
var getMarketTokenPriceMinCall = new Call
{
Target = Arbitrum.AddressV2.Reader,
CallData = getMarketTokenPriceMinCallData
};
var readerMulticall = new AggregateFunction();
readerMulticall.Calls = new List<Call>();
readerMulticall.Calls.Add(getMarketInfoCall);
readerMulticall.Calls.Add(getMarketTokenPriceCall);
readerMulticall.Calls.Add(getMarketTokenPriceMinCall);
var queryHandler = web3.Eth.GetContractQueryHandler<AggregateFunction>();
var readerCallResults = await queryHandler
.QueryDeserializingToObjectAsync<AggregateOutputDTO>(readerMulticall, Arbitrum.AddressV2.Multicall)
.ConfigureAwait(false);
var marketInfo = new GetMarketInfoOutputDTO().DecodeOutput(readerCallResults.ReturnData[0].ToHex());
var marketTokenPriceMax =
new GetMarketTokenPriceOutputDTO().DecodeOutput(readerCallResults.ReturnData[1].ToHex());
var marketTokenPriceMin =
new GetMarketTokenPriceOutputDTO().DecodeOutput(readerCallResults.ReturnData[2].ToHex());
// Get hashed key to call datastore
var marketKeys = GmxKeysService.GetMarketKeys(market.MarketToken);
if (marketKeys == null)
continue;
var longInterestUsingLongTokenCallData = new GetUintFunction()
{
Key = marketKeys.LongInterestUsingLongToken.HexToByteArray()
}.GetCallData();
var longInterestUsingLongTokenCall = new Call
{
Target = Arbitrum.AddressV2.DataStore,
CallData = longInterestUsingLongTokenCallData
};
var longInterestUsingShortTokenCallData = new GetUintFunction()
{
Key = marketKeys.LongInterestUsingShortToken.HexToByteArray()
}.GetCallData();
var longInterestUsingShortTokenCall = new Call
{
Target = Arbitrum.AddressV2.DataStore,
CallData = longInterestUsingShortTokenCallData
};
var shortInterestUsingLongTokenCallData = new GetUintFunction()
{
Key = marketKeys.ShortInterestUsingLongToken.HexToByteArray()
}.GetCallData();
var shortInterestUsingLongTokenCall = new Call
{
Target = Arbitrum.AddressV2.DataStore,
CallData = shortInterestUsingLongTokenCallData
};
var shortInterestUsingShortTokenCallData = new GetUintFunction()
{
Key = marketKeys.ShortInterestUsingShortToken.HexToByteArray()
}.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);
// Call datastore to get market info
var dataStoreQueryHandler = web3.Eth.GetContractQueryHandler<AggregateFunction>();
var dataStoreCallResults = await dataStoreQueryHandler
.QueryDeserializingToObjectAsync<AggregateOutputDTO>(dataStoreMulticall, Arbitrum.AddressV2.Multicall)
.ConfigureAwait(false);
var marketInfos = GmxMappers.Map(marketInfo.ReturnValue1);
readerResult.Add(new GmxMarketInfo()
{
Market = marketInfos,
Infos = DecodeFundingRateOutput(dataStoreCallResults, marketInfos.IsSameCollaterals),
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)
{
var tokens = TokenV2Service.GetTokens().ToList();
var balances = await GetTokenBalances(web3, tokens);
var prices = GetTokenPrices(web3, tokens);
var result = new List<GmxTokenData>();
foreach (var token in tokens)
{
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);
}
return result;
}
private Dictionary<string, GmxTokenPriceData> GetTokenPrices(Web3 web3, List<GmxToken> tokens)
{
var result = new Dictionary<string, GmxTokenPriceData>();
foreach (var token in tokens)
{
// TODO fetch token price from oracle
result.Add(token.Address, new GmxTokenPriceData()
{
Price = new MarketPrice()
{
Min = 0,
Max = 0
},
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>();
foreach (var token in tokens)
{
if (token.IsNative)
{
var nativeBalanceCallData = new GetEthBalanceFunction()
{
Addr = account ?? Nethereum.Util.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);
}
return result;
}
public async Task<List<FundingRate>> GetFundingRate(Web3 web3)
{
var market = await GetMarketInfo(web3);
return Map(market);
}
private List<FundingRate> Map(GmxMarketInfo market)
{
var longApr = GetFundingFactorPerPeriod(market, true, Periods[Enums.Timeframe.OneHour]) * 100;
var shortApr = GetFundingFactorPerPeriod(market, false, Periods[Enums.Timeframe.OneHour]) * 100;
var longFundingRate = new FundingRate()
{
Ticker = GmxHelpers.GetTicker(market.Market.Symbol),
Rate = GmxHelpers.FormatAmount(longApr, 30, 4),
Direction = Enums.TradeDirection.Long
};
var shortFundingRate = new FundingRate()
{
Ticker = GmxHelpers.GetTicker(market.Market.Symbol),
Rate = GmxHelpers.FormatAmount(shortApr, 30, 4),
Direction = Enums.TradeDirection.Short
};
return new List<FundingRate>() { longFundingRate, shortFundingRate };
}
public async Task<List<FundingRate>> GetFundingRates(Web3 web3)
{
var fundingRates = new List<FundingRate>();
var marketDatas = await GetMarketInfosAsync(web3);
foreach (var gmxMarketInfo in marketDatas)
{
var rates = Map(gmxMarketInfo);
fundingRates.AddRange(rates);
}
return fundingRates;
}
private BigInteger GetFundingFactorPerPeriod(GmxMarketInfo marketInfo, bool isLong, int periodInSeconds)
{
var fundingFactorPerSecond = marketInfo.Market.FundingFactorPerSecond;
var longsPayShorts = marketInfo.Market.LongsPayShorts;
var longInterestUsd = marketInfo.Infos.LongInterestUsd;
var shortInterestUsd = marketInfo.Infos.ShortInterestUsd;
var isLargerSide = isLong ? longsPayShorts : !longsPayShorts;
BigInteger factorPerSecond;
if (isLargerSide)
{
factorPerSecond = fundingFactorPerSecond * -1;
}
else
{
var largerInterestUsd = longsPayShorts ? longInterestUsd : shortInterestUsd;
var smallerInterestUsd = longsPayShorts ? shortInterestUsd : longInterestUsd;
var ratio = smallerInterestUsd > 0
? BigInteger.Multiply(largerInterestUsd, PRECISION) / smallerInterestUsd
: 0;
factorPerSecond = ApplyFactor(ratio, fundingFactorPerSecond);
}
return factorPerSecond * periodInSeconds;
}
private BigInteger ApplyFactor(BigInteger ratio, BigInteger fundingFactorPerSecond)
{
// Implement the logic for applying the factor based on the ratio
// This is a placeholder implementation
return BigInteger.Multiply(ratio, fundingFactorPerSecond) / PRECISION;
}
private static readonly BigInteger PRECISION = BigInteger.Pow(10, 18);
private Dictionary<Enums.Timeframe, int> Periods = new Dictionary<Enums.Timeframe, int>()
{
{ Enums.Timeframe.FiveMinutes, 300 },
{ Enums.Timeframe.FifteenMinutes, 900 },
{ Enums.Timeframe.OneHour, 3600 },
{ Enums.Timeframe.FourHour, 14400 },
{ Enums.Timeframe.OneDay, 86400 }
};
}