From e18fb6a4ce884aed834ecb8cc80b6fb372404dee Mon Sep 17 00:00:00 2001 From: Zakhar Petukhov Date: Fri, 25 Oct 2024 12:57:31 +0800 Subject: [PATCH] create a rates for LP jettons --- pkg/rates/market.go | 151 ++++++++++++++++++++++++++++++++++++++-- pkg/rates/rates_test.go | 22 +++--- 2 files changed, 157 insertions(+), 16 deletions(-) diff --git a/pkg/rates/market.go b/pkg/rates/market.go index 5db027bc..99c92157 100644 --- a/pkg/rates/market.go +++ b/pkg/rates/market.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "math" + "math/big" "net/http" "sort" "strconv" @@ -47,6 +48,14 @@ type Asset struct { HoldersCount int } +// LpAsset represents a liquidity provider asset that holds a collection of assets in a pool +type LpAsset struct { + Account ton.AccountID + Decimals int + TotalSupply *big.Int // The total supply of the liquidity provider asset + Assets []Asset // A slice of Asset included in the liquidity pool +} + // DeDustAssets represents a collection of assets from the DeDust platform, including their stability status type DeDustAssets struct { Assets []Asset @@ -429,7 +438,32 @@ func convertedStonFiPoolResponse(pools map[ton.AccountID]float64, respBody io.Re } return firstAsset, secondAsset, nil } - + parseLpAsset := func(record []string, firstAsset, secondAsset Asset) (LpAsset, error) { + lpAsset, err := ton.ParseAccountID(record[8]) + if err != nil { + return LpAsset{}, err + } + if record[9] == "0" { + return LpAsset{}, fmt.Errorf("unknown total supply") + } + totalSupply, ok := new(big.Int).SetString(record[9], 10) + if !ok { + return LpAsset{}, fmt.Errorf("failed to parse total supply") + } + if totalSupply.Cmp(big.NewInt(int64(defaultMinReserve))) < 0 { + return LpAsset{}, fmt.Errorf("the total supply is less than the minimum value") + } + decimals, err := strconv.Atoi(record[10]) + if err != nil { + return LpAsset{}, err + } + return LpAsset{ + Account: lpAsset, + Decimals: decimals, + TotalSupply: totalSupply, + Assets: []Asset{firstAsset, secondAsset}, + }, nil + } actualAssets := make(map[ton.AccountID][]Asset) // Update the assets with the largest reserves updateActualAssets := func(mainAsset Asset, firstAsset, secondAsset Asset) { @@ -439,7 +473,14 @@ func convertedStonFiPoolResponse(pools map[ton.AccountID]float64, respBody io.Re return } for idx, asset := range assets { - if asset.Account == mainAsset.Account && asset.Reserve < mainAsset.Reserve { + assetReserveNormalised := asset.Reserve + mainAssetReserveNormalised := mainAsset.Reserve + // Adjust the reserves of assets considering their decimal places + if asset.Decimals != mainAsset.Decimals { + assetReserveNormalised = asset.Reserve / math.Pow10(asset.Decimals) + mainAssetReserveNormalised = mainAsset.Reserve / math.Pow10(mainAsset.Decimals) + } + if asset.Account == mainAsset.Account && assetReserveNormalised < mainAssetReserveNormalised { if idx == 0 { actualAssets[mainAsset.Account] = []Asset{firstAsset, secondAsset} } else { @@ -448,8 +489,9 @@ func convertedStonFiPoolResponse(pools map[ton.AccountID]float64, respBody io.Re } } } + actualLpAssets := make(map[ton.AccountID]LpAsset) for idx, record := range records { - if idx == 0 || len(record) < 8 { // Skip headers + if idx == 0 || len(record) < 10 { // Skip headers continue } firstAsset, secondAsset, err := parseAssets(record) @@ -469,6 +511,10 @@ func convertedStonFiPoolResponse(pools map[ton.AccountID]float64, respBody io.Re (isNotPTon(secondAsset.Account) && secondAsset.HoldersCount < defaultMinHoldersCount) { continue } + lpAsset, err := parseLpAsset(record, firstAsset, secondAsset) + if err == nil { + actualLpAssets[lpAsset.Account] = lpAsset + } updateActualAssets(firstAsset, firstAsset, secondAsset) updateActualAssets(secondAsset, firstAsset, secondAsset) } @@ -481,6 +527,16 @@ func convertedStonFiPoolResponse(pools map[ton.AccountID]float64, respBody io.Re pools[accountID] = price } } + for _, asset := range actualLpAssets { + if _, ok := pools[asset.Account]; ok { + continue + } + price := calculateLpAssetPrice(asset, pools) + if price == 0 { + continue + } + pools[asset.Account] = price + } return pools, nil } @@ -576,6 +632,32 @@ func convertedDeDustPoolResponse(pools map[ton.AccountID]float64, respBody io.Re } return DeDustAssets{Assets: []Asset{firstAsset, secondAsset}, IsStable: isStable}, nil } + parseLpAsset := func(record []string, firstAsset, secondAsset Asset) (LpAsset, error) { + lpAsset, err := ton.ParseAccountID(record[11]) + if err != nil { + return LpAsset{}, err + } + if record[12] == "0" { + return LpAsset{}, fmt.Errorf("unknown total supply") + } + totalSupply, ok := new(big.Int).SetString(record[12], 10) + if !ok { + return LpAsset{}, fmt.Errorf("failed to parse total supply") + } + if totalSupply.Cmp(big.NewInt(int64(defaultMinReserve))) < 0 { + return LpAsset{}, fmt.Errorf("the total supply is less than the minimum value") + } + decimals, err := strconv.Atoi(record[13]) + if err != nil { + return LpAsset{}, err + } + return LpAsset{ + Account: lpAsset, + Decimals: decimals, + TotalSupply: totalSupply, + Assets: []Asset{firstAsset, secondAsset}, + }, nil + } actualAssets := make(map[ton.AccountID]DeDustAssets) // Update the assets with the largest reserves updateActualAssets := func(mainAsset Asset, deDustAssets DeDustAssets) { @@ -586,7 +668,14 @@ func convertedDeDustPoolResponse(pools map[ton.AccountID]float64, respBody io.Re return } for idx, asset := range assets.Assets { - if asset.Account == mainAsset.Account && asset.Reserve < mainAsset.Reserve { + assetReserveNormalised := asset.Reserve + mainAssetReserveNormalised := mainAsset.Reserve + // Adjust the reserves of assets considering their decimal places + if asset.Decimals != mainAsset.Decimals { + assetReserveNormalised = asset.Reserve / math.Pow10(asset.Decimals) + mainAssetReserveNormalised = mainAsset.Reserve / math.Pow10(mainAsset.Decimals) + } + if asset.Account == mainAsset.Account && assetReserveNormalised < mainAssetReserveNormalised { if idx == 0 { actualAssets[mainAsset.Account] = DeDustAssets{Assets: []Asset{firstAsset, secondAsset}, IsStable: deDustAssets.IsStable} } else { @@ -595,8 +684,9 @@ func convertedDeDustPoolResponse(pools map[ton.AccountID]float64, respBody io.Re } } } + actualLpAssets := make(map[ton.AccountID]LpAsset) for idx, record := range records { - if idx == 0 || len(record) < 10 { // Skip headers + if idx == 0 || len(record) < 14 { // Skip headers continue } assets, err := parseAssets(record) @@ -608,6 +698,10 @@ func convertedDeDustPoolResponse(pools map[ton.AccountID]float64, respBody io.Re if firstAsset.Reserve == 0 || secondAsset.Reserve == 0 { continue } + lpAsset, err := parseLpAsset(record, firstAsset, secondAsset) + if err == nil { + actualLpAssets[lpAsset.Account] = lpAsset + } updateActualAssets(firstAsset, assets) updateActualAssets(secondAsset, assets) } @@ -620,10 +714,57 @@ func convertedDeDustPoolResponse(pools map[ton.AccountID]float64, respBody io.Re pools[accountID] = price } } + for _, asset := range actualLpAssets { + if _, ok := pools[asset.Account]; ok { + continue + } + price := calculateLpAssetPrice(asset, pools) + if price == 0 { + continue + } + pools[asset.Account] = price + } return pools, nil } +func calculateLpAssetPrice(asset LpAsset, pools map[ton.AccountID]float64) float64 { + firstAsset := asset.Assets[0] + secondAsset := asset.Assets[1] + firstAssetPrice, ok := pools[firstAsset.Account] + if !ok { + return 0 + } + secondAssetPrice, ok := pools[secondAsset.Account] + if !ok { + return 0 + } + // Adjust the reserves of assets considering their decimal places + firstAssetAdjustedReserve := new(big.Float).Quo( + big.NewFloat(firstAsset.Reserve), + new(big.Float).SetFloat64(math.Pow(10, float64(firstAsset.Decimals))), + ) + secondAssetAdjustedReserve := new(big.Float).Quo( + big.NewFloat(secondAsset.Reserve), + new(big.Float).SetFloat64(math.Pow(10, float64(secondAsset.Decimals))), + ) + // Calculate the total value of the jetton by summing the values of both assets + totalValue := new(big.Float).Add( + new(big.Float).Mul(firstAssetAdjustedReserve, big.NewFloat(firstAssetPrice)), + new(big.Float).Mul(secondAssetAdjustedReserve, big.NewFloat(secondAssetPrice)), + ) + // Adjust the total supply for the asset decimals + totalSupplyAdjusted := new(big.Float).Quo( + new(big.Float).SetInt(asset.TotalSupply), + new(big.Float).SetFloat64(math.Pow(10, float64(asset.Decimals))), + ) + // Calculate the price of the jetton by dividing the total value by the adjusted total supply of tokens + price := new(big.Float).Quo(totalValue, totalSupplyAdjusted) + + convertedPrice, _ := price.Float64() + return convertedPrice +} + func calculatePoolPrice(firstAsset, secondAsset Asset, pools map[ton.AccountID]float64, isStable bool) (ton.AccountID, float64) { priceFirst, okFirst := pools[firstAsset.Account] priceSecond, okSecond := pools[secondAsset.Account] diff --git a/pkg/rates/rates_test.go b/pkg/rates/rates_test.go index 24e00358..6a908198 100644 --- a/pkg/rates/rates_test.go +++ b/pkg/rates/rates_test.go @@ -22,10 +22,10 @@ func TestCalculateJettonPriceFromStonFiPool(t *testing.T) { { name: "Successful calculate Scaleton and USD₮ jettons", csv: ` -asset_0_account_id,asset_1_account_id,asset_0_reserve,asset_1_reserve,asset_0_metadata,asset_1_metadata,asset_0_holders,asset_1_holders -0:8cdc1d7640ad5ee326527fc1ad0514f468b30dc84b0173f0e155f451b4e11f7c,0:65aac9b5e380eae928db3c8e238d9bc0d61a9320fdc2bc7a2f6c87d6fedf9208,356773586306,572083446808,"{""decimals"":""9"",""name"":""Proxy TON"",""symbol"":""pTON""}","{""name"":""Scaleton"",""symbol"":""SCALE""}",52,17245 -0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe,0:8cdc1d7640ad5ee326527fc1ad0514f468b30dc84b0173f0e155f451b4e11f7c,54581198678395,9745288354931876,"{""decimals"":""6"",""name"":""Tether USD"",""symbol"":""USD₮""}","{""decimals"":""9"",""name"":""Proxy TON"",""symbol"":""pTON""}",1038000,52 -0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe,0:afc49cb8786f21c87045b19ede78fc6b46c51048513f8e9a6d44060199c1bf0c,996119000168,921942515487299500,"{""decimals"":""6"",""name"":""Tether USD"",""symbol"":""USD₮""}","{""decimals"":""9"",""name"":""Dogs"",""symbol"":""DOGS""}",1050066,881834`, +asset_0_account_id,asset_1_account_id,asset_0_reserve,asset_1_reserve,asset_0_metadata,asset_1_metadata,asset_0_holders,asset_1_holders,lp_jetton,total_supply,lp_jetton_decimals +0:8cdc1d7640ad5ee326527fc1ad0514f468b30dc84b0173f0e155f451b4e11f7c,0:65aac9b5e380eae928db3c8e238d9bc0d61a9320fdc2bc7a2f6c87d6fedf9208,356773586306,572083446808,"{""decimals"":""9"",""name"":""Proxy TON"",""symbol"":""pTON""}","{""name"":""Scaleton"",""symbol"":""SCALE""}",52,17245,0:4d70707f7f62d432157dd8f1a90ce7421b34bcb2ecc4390469181bc575e4739f,0,9 +0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe,0:8cdc1d7640ad5ee326527fc1ad0514f468b30dc84b0173f0e155f451b4e11f7c,54581198678395,9745288354931876,"{""decimals"":""6"",""name"":""Tether USD"",""symbol"":""USD₮""}","{""decimals"":""9"",""name"":""Proxy TON"",""symbol"":""pTON""}",1038000,52,0:4d70707f7f62d432157dd8f1a90ce7421b34bcb2ecc4390469181bc575e4739f,0,9 +0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe,0:afc49cb8786f21c87045b19ede78fc6b46c51048513f8e9a6d44060199c1bf0c,996119000168,921942515487299500,"{""decimals"":""6"",""name"":""Tether USD"",""symbol"":""USD₮""}","{""decimals"":""9"",""name"":""Dogs"",""symbol"":""DOGS""}",1050066,881834,0:4d70707f7f62d432157dd8f1a90ce7421b34bcb2ecc4390469181bc575e4739f,0,9`, expected: map[ton.AccountID]float64{ ton.MustParseAccountID("0:8cdc1d7640ad5ee326527fc1ad0514f468b30dc84b0173f0e155f451b4e11f7c"): 1, // Default pTonV1 price ton.MustParseAccountID("0:671963027f7f85659ab55b821671688601cdcf1ee674fc7fbbb1a776a18d34a3"): 1, // Default pTonV2 price @@ -39,8 +39,8 @@ asset_0_account_id,asset_1_account_id,asset_0_reserve,asset_1_reserve,asset_0_me { name: "Failed calculate Scaleton (insufficient holders)", csv: ` -asset_0_account_id,asset_1_account_id,asset_0_reserve,asset_1_reserve,asset_0_metadata,asset_1_metadata,asset_0_holders,asset_1_holders -0:8cdc1d7640ad5ee326527fc1ad0514f468b30dc84b0173f0e155f451b4e11f7c,0:65aac9b5e380eae928db3c8e238d9bc0d61a9320fdc2bc7a2f6c87d6fedf9208,356773586306,572083446808,"{""decimals"":""9"",""name"":""Proxy TON"",""symbol"":""pTON""}","{""name"":""Scaleton"",""symbol"":""SCALE""}",52,10 +asset_0_account_id,asset_1_account_id,asset_0_reserve,asset_1_reserve,asset_0_metadata,asset_1_metadata,asset_0_holders,asset_1_holders,lp_jetton,total_supply,lp_jetton_decimals +0:8cdc1d7640ad5ee326527fc1ad0514f468b30dc84b0173f0e155f451b4e11f7c,0:65aac9b5e380eae928db3c8e238d9bc0d61a9320fdc2bc7a2f6c87d6fedf9208,356773586306,572083446808,"{""decimals"":""9"",""name"":""Proxy TON"",""symbol"":""pTON""}","{""name"":""Scaleton"",""symbol"":""SCALE""}",52,10,0:4d70707f7f62d432157dd8f1a90ce7421b34bcb2ecc4390469181bc575e4739f,0,9 `, expected: map[ton.AccountID]float64{ ton.MustParseAccountID("0:8cdc1d7640ad5ee326527fc1ad0514f468b30dc84b0173f0e155f451b4e11f7c"): 1, // Default pTonV1 price @@ -83,11 +83,11 @@ func TestCalculateJettonPriceFromDeDustPool(t *testing.T) { { name: "Successful calculate", csv: ` -asset_0_account_id,asset_1_account_id,asset_0_native,asset_1_native,asset_0_reserve,asset_1_reserve,asset_0_metadata,asset_1_metadata,is_stable,asset_0_holders,asset_1_holders -NULL,0:022d70f08add35b2d8aa2bd16f622268d7996e5737c3e7353cbb00d2aba257c5,true,false,100171974809,1787220634679,NULL,"{""decimals"":""8"",""name"":""Spintria"",""symbol"":""SP""}",false,0,3084 -NULL,0:48cef1de34697508200b8026bf882f8e88aff894586cfd304ab513633fa7e2d3,true,false,22004762576054,4171862045823,NULL,"{""decimals"":""9"",""name"":""AI Coin"",""symbol"":""AIC""}",false,0,1239 -0:48cef1de34697508200b8026bf882f8e88aff894586cfd304ab513633fa7e2d3,0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe,false,false,468457277157,13287924673,"{""decimals"":""9"",""name"":""AI Coin"",""symbol"":""AIC""}","{""decimals"":""6"",""name"":""Tether USD"",""symbol"":""USD₮""}",false,1239,1039987 -NULL,0:cf76af318c0872b58a9f1925fc29c156211782b9fb01f56760d292e56123bf87,true,false,5406255533839,3293533372962,NULL,"{""decimals"":""9"",""name"":""Hipo Staked TON"",""symbol"":""hTON""}",true,0,2181`, +asset_0_account_id,asset_1_account_id,asset_0_native,asset_1_native,asset_0_reserve,asset_1_reserve,asset_0_metadata,asset_1_metadata,is_stable,asset_0_holders,asset_1_holders,lp_jetton,total_supply,lp_jetton_decimals +NULL,0:022d70f08add35b2d8aa2bd16f622268d7996e5737c3e7353cbb00d2aba257c5,true,false,100171974809,1787220634679,NULL,"{""decimals"":""8"",""name"":""Spintria"",""symbol"":""SP""}",false,0,3084,0:4d70707f7f62d432157dd8f1a90ce7421b34bcb2ecc4390469181bc575e4739f,0,9 +NULL,0:48cef1de34697508200b8026bf882f8e88aff894586cfd304ab513633fa7e2d3,true,false,22004762576054,4171862045823,NULL,"{""decimals"":""9"",""name"":""AI Coin"",""symbol"":""AIC""}",false,0,1239,0:4d70707f7f62d432157dd8f1a90ce7421b34bcb2ecc4390469181bc575e4739f,0,9 +0:48cef1de34697508200b8026bf882f8e88aff894586cfd304ab513633fa7e2d3,0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe,false,false,468457277157,13287924673,"{""decimals"":""9"",""name"":""AI Coin"",""symbol"":""AIC""}","{""decimals"":""6"",""name"":""Tether USD"",""symbol"":""USD₮""}",false,1239,1039987,0:4d70707f7f62d432157dd8f1a90ce7421b34bcb2ecc4390469181bc575e4739f,0,9 +NULL,0:cf76af318c0872b58a9f1925fc29c156211782b9fb01f56760d292e56123bf87,true,false,5406255533839,3293533372962,NULL,"{""decimals"":""9"",""name"":""Hipo Staked TON"",""symbol"":""hTON""}",true,0,2181,0:4d70707f7f62d432157dd8f1a90ce7421b34bcb2ecc4390469181bc575e4739f,0,9`, expected: map[ton.AccountID]float64{ ton.MustParseAccountID("0:0000000000000000000000000000000000000000000000000000000000000000"): 1, // Default TON price ton.MustParseAccountID("0:022d70f08add35b2d8aa2bd16f622268d7996e5737c3e7353cbb00d2aba257c5"): 0.005604902543383612,