diff --git a/api/openapi.json b/api/openapi.json index 113361d1c..6b2358c34 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -3158,6 +3158,114 @@ ], "type": "object" }, + "DefiAsset": { + "properties": { + "amount": { + "description": "amount in token units; negative for lending debt", + "example": 1000000000, + "format": "int64", + "type": "integer", + "x-js-format": "bigint" + }, + "jetton": { + "$ref": "#/components/schemas/JettonPreview" + }, + "nft": { + "$ref": "#/components/schemas/DefiNftPreview" + }, + "position": { + "description": "current position value in nanoTON", + "example": 2000000000, + "format": "int64", + "type": "integer", + "x-js-format": "bigint" + }, + "provider": { + "$ref": "#/components/schemas/DefiProvider" + }, + "type": { + "$ref": "#/components/schemas/DefiAssetType" + } + }, + "required": [ + "type", + "provider", + "amount", + "position" + ], + "type": "object" + }, + "DefiAssetType": { + "enum": [ + "liquid_staking", + "liquid_pool", + "staking", + "lending", + "farming" + ], + "type": "string" + }, + "DefiAssets": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/DefiAsset" + }, + "type": "array" + } + }, + "required": [ + "assets" + ], + "type": "object" + }, + "DefiNftPreview": { + "properties": { + "address": { + "example": "0:0BB5A9F69043EEBDDA5AD2E946EB953242BD8F603FE795D90698CEEC6BFC60A0", + "format": "address", + "type": "string" + }, + "collection": { + "example": "0:0BB5A9F69043EEBDDA5AD2E946EB953242BD8F603FE795D90698CEEC6BFC60A0", + "format": "address", + "type": "string" + }, + "token0": { + "$ref": "#/components/schemas/JettonPreview" + }, + "token1": { + "$ref": "#/components/schemas/JettonPreview" + } + }, + "required": [ + "address", + "collection" + ], + "type": "object" + }, + "DefiProvider": { + "properties": { + "image": { + "example": "https://tonstakers.com/logo.png", + "type": "string" + }, + "name": { + "example": "Tonstakers", + "type": "string" + }, + "url": { + "example": "https://app.tonstakers.com/", + "type": "string" + } + }, + "required": [ + "name", + "url", + "image" + ], + "type": "object" + }, "DepositStakeAction": { "description": "validator's participation in elections", "properties": { @@ -9674,6 +9782,35 @@ ] } }, + "/v2/defi/{account_id}/assets": { + "get": { + "description": "Get all DeFi assets (liquid staking, liquid pools, staking, lending, farming) for an account", + "operationId": "getAccountDefiAssets", + "parameters": [ + { + "$ref": "#/components/parameters/accountIDParameter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DefiAssets" + } + } + }, + "description": "defi assets" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "tags": [ + "DeFi" + ] + } + }, "/v2/dns/auctions": { "get": { "description": "Get all auctions", @@ -12874,6 +13011,9 @@ }, { "name": "Purchases" + }, + { + "name": "DeFi" } ] } diff --git a/api/openapi.yml b/api/openapi.yml index 31316b5cc..46432f239 100644 --- a/api/openapi.yml +++ b/api/openapi.yml @@ -85,6 +85,7 @@ tags: description: Additional documentation url: https://docs.tonconsole.com/tonapi/rest-api/extra-currency - name: Purchases + - name: DeFi paths: /v2/openapi.json: @@ -1832,6 +1833,24 @@ paths: 'default': $ref: '#/components/responses/Error' + /v2/defi/{account_id}/assets: + get: + description: Get all DeFi assets (liquid staking, liquid pools, staking, lending, farming) for an account + operationId: getAccountDefiAssets + tags: + - DeFi + parameters: + - $ref: '#/components/parameters/accountIDParameter' + responses: + '200': + description: defi assets + content: + application/json: + schema: + $ref: '#/components/schemas/DefiAssets' + 'default': + $ref: '#/components/responses/Error' + /v2/storage/providers: get: description: Get TON storage providers deployed to the blockchain. @@ -9017,6 +9036,90 @@ components: items: type: number format: double + DefiAssetType: + type: string + enum: + - liquid_staking + - liquid_pool + - staking + - lending + - farming + + DefiProvider: + type: object + required: + - name + - url + - image + properties: + name: + type: string + example: Tonstakers + url: + type: string + example: https://app.tonstakers.com/ + image: + type: string + example: https://tonstakers.com/logo.png + + DefiNftPreview: + type: object + required: + - address + - collection + properties: + address: + type: string + format: address + example: 0:0BB5A9F69043EEBDDA5AD2E946EB953242BD8F603FE795D90698CEEC6BFC60A0 + collection: + type: string + format: address + example: 0:0BB5A9F69043EEBDDA5AD2E946EB953242BD8F603FE795D90698CEEC6BFC60A0 + token0: + $ref: '#/components/schemas/JettonPreview' + token1: + $ref: '#/components/schemas/JettonPreview' + + DefiAsset: + type: object + required: + - type + - provider + - amount + - position + properties: + type: + $ref: '#/components/schemas/DefiAssetType' + jetton: + $ref: '#/components/schemas/JettonPreview' + nft: + $ref: '#/components/schemas/DefiNftPreview' + provider: + $ref: '#/components/schemas/DefiProvider' + amount: + type: integer + format: int64 + description: amount in token units; negative for lending debt + example: 1000000000 + x-js-format: bigint + position: + type: integer + format: int64 + description: current position value in nanoTON + example: 2000000000 + x-js-format: bigint + + DefiAssets: + type: object + required: + - assets + properties: + assets: + type: array + items: + $ref: '#/components/schemas/DefiAsset' + responses: Error: description: Some error during request processing diff --git a/pkg/api/defi_handlers.go b/pkg/api/defi_handlers.go new file mode 100644 index 000000000..09b308f04 --- /dev/null +++ b/pkg/api/defi_handlers.go @@ -0,0 +1,45 @@ +package api + +import ( + "context" + "net/http" + + "github.com/tonkeeper/opentonapi/pkg/defi" + "github.com/tonkeeper/opentonapi/pkg/oas" + "github.com/tonkeeper/tongo" +) + +func (h *Handler) defiDeps() defi.Deps { + return defi.Deps{ + Storage: h.storage, + Executor: h.executor, + Score: h.score, + Logger: h.logger, + ProxyURL: h.proxyURL, + JettonMeta: func(ctx context.Context, m tongo.AccountID) defi.NormalizedJettonMeta { + x := h.GetJettonNormalizedMetadata(ctx, m) + return defi.NormalizedJettonMeta{ + Name: x.Name, + Description: x.Description, + Image: x.Image, + Symbol: x.Symbol, + Decimals: x.Decimals, + Verification: x.Verification, + Social: x.Social, + Websites: x.Websites, + CustomPayloadApiUri: x.CustomPayloadApiUri, + PreviewImage: x.PreviewImage, + } + }, + } +} + +func (h *Handler) GetAccountDefiAssets(ctx context.Context, params oas.GetAccountDefiAssetsParams) (*oas.DefiAssets, error) { + account, err := tongo.ParseAddress(params.AccountID) + if err != nil { + return nil, toError(http.StatusBadRequest, err) + } + + todayRates, _, _, _, _ := h.getRates() + return defi.GetDefiAssets(ctx, h.defiDeps(), account.ID, todayRates), nil +} diff --git a/pkg/api/handler.go b/pkg/api/handler.go index ac204af9a..03fe60e0f 100644 --- a/pkg/api/handler.go +++ b/pkg/api/handler.go @@ -63,6 +63,8 @@ type Handler struct { ctxToDetails ctxToDetails tongoVersion int + proxyURL string + // mempoolEmulateIgnoreAccounts, we don't track pending transactions for this list of accounts. mempoolEmulateIgnoreAccounts map[tongo.AccountID]struct{} @@ -99,6 +101,7 @@ type Options struct { score scoreSource parallelTraceProcessing bool archiveLiteServers []config.LiteServer + proxyURL string } type Option func(o *Options) @@ -192,6 +195,12 @@ func WithArchiveLiteServers(s []config.LiteServer) Option { } } +func WithProxyURL(baseURL string) Option { + return func(o *Options) { + o.proxyURL = baseURL + } +} + func NewHandler(logger *zap.Logger, opts ...Option) (*Handler, error) { options := &Options{} for _, o := range opts { @@ -269,6 +278,7 @@ func NewHandler(logger *zap.Logger, opts ...Option) (*Handler, error) { ctxToDetails: options.ctxToDetails, gasless: options.gasless, score: options.score, + proxyURL: options.proxyURL, ratesSource: rates.InitCalculator(options.ratesSource), verifierSource: options.verifier, metaCache: metadataCache{ diff --git a/pkg/bath/bath_test.go b/pkg/bath/bath_test.go index 919120436..675e9f38d 100644 --- a/pkg/bath/bath_test.go +++ b/pkg/bath/bath_test.go @@ -91,6 +91,10 @@ func (m *mockInfoSource) DedustPools(ctx context.Context, contracts []tongo.Acco return map[tongo.AccountID]core.DedustPool{}, nil } +func (m *mockInfoSource) BidaskPools(_ context.Context) ([]tongo.AccountID, error) { + return nil, nil +} + var _ core.InformationSource = &mockInfoSource{} func isFocusedRun() bool { diff --git a/pkg/core/trace.go b/pkg/core/trace.go index 87bef7521..97caeebf4 100644 --- a/pkg/core/trace.go +++ b/pkg/core/trace.go @@ -309,6 +309,7 @@ type InformationSource interface { STONfiPools(ctx context.Context, poolIDs []STONfiPoolID) (map[tongo.AccountID]STONfiPool, error) DedustPools(ctx context.Context, contracts []tongo.AccountID) (map[tongo.AccountID]DedustPool, error) SubscriptionInfos(ctx context.Context, ids []SubscriptionID) (map[tongo.AccountID]SubscriptionInfo, error) + BidaskPools(ctx context.Context) ([]tongo.AccountID, error) GetFfVaultPositionDatas(ctx context.Context, positions []tongo.AccountID) (map[tongo.AccountID]VaultPositionData, error) GetFfVaultJettonMasters(ctx context.Context, vaults []tongo.AccountID) (map[tongo.AccountID]tongo.AccountID, error) GetPythPriceFeedMeta(id string) (pyth.PriceFeedAttributes, bool) diff --git a/pkg/defi/affluent.go b/pkg/defi/affluent.go new file mode 100644 index 000000000..a25410ad6 --- /dev/null +++ b/pkg/defi/affluent.go @@ -0,0 +1,60 @@ +package defi + +import ( + "context" + "math/big" + + "github.com/shopspring/decimal" + "github.com/tonkeeper/opentonapi/pkg/oas" + "github.com/tonkeeper/opentonapi/pkg/references" + "github.com/tonkeeper/tongo" + "github.com/tonkeeper/tongo/tlb" + "go.uber.org/zap" +) + +func collectAffluentPositions(ctx context.Context, d Deps, account tongo.AccountID, rates map[string]float64) []oas.DefiAsset { + addrVal, err := tlb.TlbStructToVmCellSlice(account.ToMsgAddress()) + if err != nil { + d.warn("affluent: failed to build address arg", zap.Error(err)) + return nil + } + + var assets []oas.DefiAsset + for _, vaultRaw := range references.AffVaults { + vaultID, err := tongo.ParseAddress(vaultRaw) + if err != nil { + continue + } + stack := tlb.VmStack{} + stack.Put(addrVal) + _, resultStack, err := d.Executor.RunSmcMethod(ctx, vaultID.ID, "get_user_data", stack) + if err != nil || resultStack.Len() < 1 { + continue + } + top := resultStack.Peek(0) + if top.SumType != "VmStkInt" { + continue + } + principal := (*big.Int)(&top.VmStkInt).Int64() + if principal == 0 { + continue + } + + meta := d.JettonMeta(ctx, vaultID.ID) + score, _ := d.Score.GetJettonScore(vaultID.ID) + absPrincipal := decimal.NewFromInt(principal).Abs() + pos := JettonPositionNanoTON(rates, vaultID.ID.ToRaw(), absPrincipal, meta.Decimals) + if principal < 0 { + pos = -pos + } + asset := oas.DefiAsset{ + Type: oas.DefiAssetTypeLending, + Provider: defiProvider(references.AffluentProvider), + Amount: principal, + Position: pos, + } + asset.Jetton.SetTo(JettonPreview(vaultID.ID, meta, score)) + assets = append(assets, asset) + } + return assets +} diff --git a/pkg/defi/assets.go b/pkg/defi/assets.go new file mode 100644 index 000000000..30ec60eb0 --- /dev/null +++ b/pkg/defi/assets.go @@ -0,0 +1,54 @@ +package defi + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/tonkeeper/opentonapi/pkg/oas" + "github.com/tonkeeper/tongo" +) + +func GetDefiAssets(ctx context.Context, d Deps, account tongo.AccountID, rates map[string]float64) *oas.DefiAssets { + var ( + result oas.DefiAssets + mu sync.Mutex + wg sync.WaitGroup + ) + + collect := func(name string, fn func() []oas.DefiAsset) { + wg.Add(1) + go func() { + defer wg.Done() + start := time.Now() + assets := fn() + fmt.Println("defi collect", name, "took", time.Since(start)) + if len(assets) == 0 { + return + } + mu.Lock() + result.Assets = append(result.Assets, assets...) + mu.Unlock() + }() + } + + collect("liquidStakingJettons", func() []oas.DefiAsset { return collectLiquidStakingJettons(ctx, d, account, rates) }) + collect("whalesStaking", func() []oas.DefiAsset { return collectWhalesStaking(ctx, d, account) }) + collect("tfStaking", func() []oas.DefiAsset { return collectTFStaking(ctx, d, account) }) + collect("lpPositions", func() []oas.DefiAsset { return collectLPPositions(ctx, d, account, rates) }) + collect("toncoPositions", func() []oas.DefiAsset { return collectToncoPositions(ctx, d, account) }) + collect("stonfiExternalPositions", func() []oas.DefiAsset { return collectStonfiExternalPositions(ctx, d, account, rates) }) + collect("stonfiStakingPositions", func() []oas.DefiAsset { return collectStonfiStakingPositions(ctx, d, account, rates) }) + collect("swapCoffeePositions", func() []oas.DefiAsset { return collectSwapCoffeePositions(ctx, d, account, rates) }) + collect("evaaPositions", func() []oas.DefiAsset { return collectEvaaPositions(ctx, d, account, rates) }) + collect("affluentPositions", func() []oas.DefiAsset { return collectAffluentPositions(ctx, d, account, rates) }) + // collect("bidaskPositions", func() []oas.DefiAsset { return collectBidaskPositions(ctx, d, account, rates) }) + + wg.Wait() + + if result.Assets == nil { + result.Assets = []oas.DefiAsset{} + } + return &result +} diff --git a/pkg/defi/bidask.go b/pkg/defi/bidask.go new file mode 100644 index 000000000..f5adcbbe9 --- /dev/null +++ b/pkg/defi/bidask.go @@ -0,0 +1,188 @@ +package defi + +import ( + "context" + "math/big" + "sync" + + "github.com/tonkeeper/opentonapi/pkg/core" + "github.com/tonkeeper/opentonapi/pkg/oas" + "github.com/tonkeeper/opentonapi/pkg/references" + "github.com/tonkeeper/tongo" + "github.com/tonkeeper/tongo/abi" + "github.com/tonkeeper/tongo/tlb" +) + +func collectBidaskPositions(ctx context.Context, d Deps, account tongo.AccountID, rates map[string]float64) []oas.DefiAsset { + pools, err := d.Storage.BidaskPools(ctx) + if err != nil || len(pools) == 0 { + return nil + } + + var ( + mu sync.Mutex + wg sync.WaitGroup + assets []oas.DefiAsset + ) + for _, poolAddr := range pools { + poolAddr := poolAddr + wg.Add(1) + go func() { + defer wg.Done() + asset, ok := buildBidaskAsset(ctx, d, poolAddr, account, rates) + if !ok { + return + } + mu.Lock() + assets = append(assets, asset) + mu.Unlock() + }() + } + wg.Wait() + return assets +} + +func buildBidaskAsset(ctx context.Context, d Deps, poolAddr tongo.AccountID, account tongo.AccountID, rates map[string]float64) (oas.DefiAsset, bool) { + _, rangeRaw, err := abi.GetActiveRange(ctx, d.Executor, poolAddr) + if err != nil { + return oas.DefiAsset{}, false + } + rangeResult, ok := rangeRaw.(abi.GetActiveRange_BidaskResult) + if !ok { + return oas.DefiAsset{}, false + } + rangeID, err := tongo.AccountIDFromTlb(rangeResult.RangeAddress) + if err != nil || rangeID == nil { + return oas.DefiAsset{}, false + } + + _, multitokenRaw, err := abi.GetLpMultitokenWallet(ctx, d.Executor, *rangeID, account.ToMsgAddress()) + if err != nil { + return oas.DefiAsset{}, false + } + multitokenResult, ok := multitokenRaw.(abi.GetLpMultitokenWallet_BidaskResult) + if !ok { + return oas.DefiAsset{}, false + } + multitokenID, err := tongo.AccountIDFromTlb(multitokenResult.MultitokenAddress) + if err != nil || multitokenID == nil { + return oas.DefiAsset{}, false + } + + errCode, stack, err := d.Executor.RunSmcMethodByID(ctx, *multitokenID, 75236, tlb.VmStack{}) + if stack.Len() != 1 || (stack.Peek(0).SumType != "VmStkCell") { + return oas.DefiAsset{}, false + } + if errCode != 0 && errCode != 1 { + return oas.DefiAsset{}, false + } + type BidaskMultitokenStorage struct { + UserAddress tlb.MsgAddress `tlb:"addr"` + RangeAddress tlb.MsgAddress `tlb:"addr"` + BinsNumber uint32 `tlb:"## 32"` + Tokens tlb.HashmapE[tlb.Uint32, tlb.Uint128] `tlb:"dict 32"` + } + + type GetStorage_BidaskMultitokenResult struct { + Storage BidaskMultitokenStorage `tlb:"^"` + } + + var result GetStorage_BidaskMultitokenResult + err = stack.Unmarshal(&result) + if err != nil { + return oas.DefiAsset{}, false + } + + totalLP := new(big.Int) + for _, item := range result.Storage.Tokens.Items() { + lpAmount := new(big.Int).Set((*big.Int)(&item.Value)) + totalLP.Add(totalLP, lpAmount) + } + if totalLP.Sign() == 0 { + return oas.DefiAsset{}, false + } + + amountX, amountY := big.NewInt(0), big.NewInt(0) + + masterX, masterY := bidaskPoolTokenMasters(ctx, d, poolAddr) + + posX := currencyReserveToNanoTON(ctx, d, rates, amountX, bidaskCurrency(masterX)) + posY := currencyReserveToNanoTON(ctx, d, rates, amountY, bidaskCurrency(masterY)) + positionNanoTON := int64(posX + posY) + + asset := oas.DefiAsset{ + Type: oas.DefiAssetTypeLiquidPool, + Provider: defiProvider(references.BidaskProvider), + Amount: amountX.Int64(), + Position: positionNanoTON, + } + preview := oas.DefiNftPreview{ + Address: multitokenID.ToRaw(), + Collection: rangeID.ToRaw(), + } + if masterX != nil { + meta := d.JettonMeta(ctx, *masterX) + score, _ := d.Score.GetJettonScore(*masterX) + preview.Token0.SetTo(JettonPreview(*masterX, meta, score)) + } + if masterY != nil { + meta := d.JettonMeta(ctx, *masterY) + score, _ := d.Score.GetJettonScore(*masterY) + preview.Token1.SetTo(JettonPreview(*masterY, meta, score)) + } + asset.Nft.SetTo(preview) + return asset, true +} + +func bidaskPoolTokenMasters(ctx context.Context, d Deps, poolAddr tongo.AccountID) (*tongo.AccountID, *tongo.AccountID) { + _, infoRaw, err := abi.GetPoolInfo(ctx, d.Executor, poolAddr) + if err != nil { + return nil, nil + } + + var walletX, walletY tlb.MsgAddress + switch v := infoRaw.(type) { + case abi.GetPoolInfo_BidaskResult: + walletX, walletY = v.JettonWalletX, v.JettonWalletY + case abi.GetPoolInfo_BidaskDammResult: + walletX, walletY = v.JettonWalletX, v.JettonWalletY + default: + return nil, nil + } + + walletXID, err := tongo.AccountIDFromTlb(walletX) + if err != nil || walletXID == nil { + return nil, nil + } + walletYID, err := tongo.AccountIDFromTlb(walletY) + if err != nil || walletYID == nil { + return nil, nil + } + + masterX := resolveJettonMaster(ctx, d, *walletXID) + masterY := resolveJettonMaster(ctx, d, *walletYID) + return masterX, masterY +} + +func resolveJettonMaster(ctx context.Context, d Deps, walletAddr tongo.AccountID) *tongo.AccountID { + _, res, err := abi.GetWalletData(ctx, d.Executor, walletAddr) + if err != nil { + return nil + } + data, ok := res.(abi.GetWalletDataResult) + if !ok { + return nil + } + master, err := tongo.AccountIDFromTlb(data.Jetton) + if err != nil { + return nil + } + return master +} + +func bidaskCurrency(master *tongo.AccountID) core.Currency { + if master == nil { + return core.Currency{} + } + return core.Currency{Type: core.CurrencyJetton, Jetton: master} +} diff --git a/pkg/defi/dedust.go b/pkg/defi/dedust.go new file mode 100644 index 000000000..2affde671 --- /dev/null +++ b/pkg/defi/dedust.go @@ -0,0 +1,91 @@ +package defi + +import ( + "context" + "math/big" + + "github.com/shopspring/decimal" + "github.com/tonkeeper/opentonapi/pkg/core" + "github.com/tonkeeper/opentonapi/pkg/oas" + "github.com/tonkeeper/opentonapi/pkg/references" + "github.com/tonkeeper/tongo" + "github.com/tonkeeper/tongo/abi" +) + +func collectLPPositions(ctx context.Context, d Deps, account tongo.AccountID, rates map[string]float64) []oas.DefiAsset { + wallets, err := d.Storage.GetJettonWalletsByOwnerAddress(ctx, account, nil, false, false, 1000, 0) + if err != nil || len(wallets) == 0 { + return nil + } + + masters := make([]tongo.AccountID, 0, len(wallets)) + byMaster := make(map[tongo.AccountID]core.JettonWallet, len(wallets)) + for _, w := range wallets { + if !w.Balance.IsZero() { + masters = append(masters, w.JettonAddress) + byMaster[w.JettonAddress] = w + } + } + + dedustPools, err := d.Storage.DedustPools(ctx, masters) + if err != nil { + return nil + } + + var assets []oas.DefiAsset + for poolAddr, pool := range dedustPools { + w := byMaster[poolAddr] + assets = append(assets, buildDedustLPAsset(ctx, d, poolAddr, w, pool, rates)) + } + + return assets +} + +func buildDedustLPAsset(ctx context.Context, d Deps, poolAddr tongo.AccountID, w core.JettonWallet, pool core.DedustPool, rates map[string]float64) oas.DefiAsset { + lpMeta := d.JettonMeta(ctx, poolAddr) + score, _ := d.Score.GetJettonScore(poolAddr) + pos := dedustLPPositionNanoTON(ctx, d, rates, poolAddr, pool, w.Balance) + + asset := oas.DefiAsset{ + Type: oas.DefiAssetTypeLiquidPool, + Provider: defiProvider(references.DedustProvider), + Amount: w.Balance.BigInt().Int64(), + Position: pos, + } + asset.Jetton.SetTo(JettonPreview(poolAddr, lpMeta, score)) + return asset +} + +func dedustLPPositionNanoTON(ctx context.Context, d Deps, rates map[string]float64, poolAddr tongo.AccountID, pool core.DedustPool, lpBalance decimal.Decimal) int64 { + _, reservesRaw, err := abi.GetReserves(ctx, d.Executor, poolAddr) + if err != nil { + return 0 + } + reserves, ok := reservesRaw.(abi.GetReserves_DedustResult) + if !ok { + return 0 + } + + masterData, err := d.Storage.GetJettonMasterData(ctx, poolAddr) + if err != nil || masterData.TotalSupply.Sign() == 0 { + return 0 + } + + r0 := new(big.Int).Set((*big.Int)(&reserves.Reserve0)) + r1 := new(big.Int).Set((*big.Int)(&reserves.Reserve1)) + + c0 := currencyReserveToNanoTON(ctx, d, rates, r0, pool.Asset0) + c1 := currencyReserveToNanoTON(ctx, d, rates, r1, pool.Asset1) + + tvl := c0 + c1 + if tvl == 0 { + return 0 + } + + totalF, _ := new(big.Float).SetInt(&masterData.TotalSupply).Float64() + balF, _ := lpBalance.Float64() + if totalF == 0 { + return 0 + } + return int64(balF / totalF * tvl) +} diff --git a/pkg/defi/deps.go b/pkg/defi/deps.go new file mode 100644 index 000000000..eef6c8f1b --- /dev/null +++ b/pkg/defi/deps.go @@ -0,0 +1,54 @@ +package defi + +import ( + "context" + + "github.com/tonkeeper/opentonapi/pkg/core" + "github.com/tonkeeper/tongo" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" + "go.uber.org/zap" +) + +type Deps struct { + Storage Storage + Executor Executor + Score Score + Logger *zap.Logger + ProxyURL string + JettonMeta func(context.Context, tongo.AccountID) NormalizedJettonMeta +} + +type Storage interface { + GetJettonWalletsByOwnerAddress(ctx context.Context, address tongo.AccountID, jetton *tongo.AccountID, isJettonMaster bool, mintless bool, limit, offset int) ([]core.JettonWallet, error) + DedustPools(ctx context.Context, contracts []tongo.AccountID) (map[tongo.AccountID]core.DedustPool, error) + GetJettonMasterData(ctx context.Context, master tongo.AccountID) (core.JettonMaster, error) + BidaskPools(ctx context.Context) ([]tongo.AccountID, error) + SearchNFTs(ctx context.Context, collection *core.Filter[tongo.AccountID], owner *core.Filter[tongo.AccountID], includeOnSale bool, onlyVerified bool, limit, offset int) ([]tongo.AccountID, error) + GetNFTs(ctx context.Context, accounts []tongo.AccountID) ([]core.NftItem, error) + GetParticipatingInWhalesPools(ctx context.Context, id tongo.AccountID) ([]core.Nominator, error) + GetParticipatingInTfPools(ctx context.Context, member tongo.AccountID) ([]core.Nominator, error) +} + +type Executor interface { + RunSmcMethod(context.Context, tongo.AccountID, string, tlb.VmStack) (uint32, tlb.VmStack, error) + RunSmcMethodByID(context.Context, tongo.AccountID, int, tlb.VmStack) (uint32, tlb.VmStack, error) +} + +type Score interface { + GetJettonScore(masterID ton.AccountID) (int32, error) +} + +func (d Deps) warn(msg string, fields ...zap.Field) { + if d.Logger == nil { + return + } + d.Logger.Warn(msg, fields...) +} + +func (d Deps) info(msg string, fields ...zap.Field) { + if d.Logger == nil { + return + } + d.Logger.Info(msg, fields...) +} diff --git a/pkg/defi/evaa.go b/pkg/defi/evaa.go new file mode 100644 index 000000000..5438eed80 --- /dev/null +++ b/pkg/defi/evaa.go @@ -0,0 +1,79 @@ +package defi + +import ( + "context" + + "github.com/shopspring/decimal" + "github.com/tonkeeper/opentonapi/pkg/oas" + "github.com/tonkeeper/opentonapi/pkg/references" + "github.com/tonkeeper/tongo" + "github.com/tonkeeper/tongo/tlb" + "go.uber.org/zap" +) + +func collectEvaaPositions(ctx context.Context, d Deps, account tongo.AccountID, rates map[string]float64) []oas.DefiAsset { + addrArg, err := tlb.TlbStructToVmCellSlice(account.ToMsgAddress()) + if err != nil { + return nil + } + + stack1 := tlb.VmStack{} + stack1.Put(addrArg) + _, res1, err := d.Executor.RunSmcMethod(ctx, references.EvaaMasterAddress, "get_user_address", stack1) + if err != nil || res1.Len() < 1 { + return nil + } + top1 := res1.Peek(0) + if top1.SumType != "VmStkSlice" { + return nil + } + var userContractMsgAddr tlb.MsgAddress + if err := top1.VmStkSlice.UnmarshalToTlbStruct(&userContractMsgAddr); err != nil { + return nil + } + userContractID, err := tongo.AccountIDFromTlb(userContractMsgAddr) + if err != nil || userContractID == nil { + return nil + } + + _, res2, err := d.Executor.RunSmcMethod(ctx, *userContractID, "get_user_data", tlb.VmStack{}) + if err != nil || res2.Len() < 1 { + return nil + } + top2 := res2.Peek(res2.Len() - 1) + if top2.SumType != "VmStkCell" { + return nil + } + + var principals tlb.HashmapE[tlb.Bits256, tlb.SignedCoins] + cell := top2.VmStkCell.Value + if err := tlb.Unmarshal(&cell, &principals); err != nil { + d.warn("evaa: failed to parse principals", zap.Error(err)) + return nil + } + + var assets []oas.DefiAsset + for _, item := range principals.Items() { + principal := int64(item.Value) + if principal == 0 { + continue + } + jettonMaster := tongo.AccountID{Workchain: 0, Address: [32]byte(item.Key)} + meta := d.JettonMeta(ctx, jettonMaster) + score, _ := d.Score.GetJettonScore(jettonMaster) + absVal := decimal.NewFromInt(principal).Abs() + pos := JettonPositionNanoTON(rates, jettonMaster.ToRaw(), absVal, meta.Decimals) + if principal < 0 { + pos = -pos + } + asset := oas.DefiAsset{ + Type: oas.DefiAssetTypeLending, + Provider: defiProvider(references.EvaaProvider), + Amount: principal, + Position: pos, + } + asset.Jetton.SetTo(JettonPreview(jettonMaster, meta, score)) + assets = append(assets, asset) + } + return assets +} diff --git a/pkg/defi/jetton.go b/pkg/defi/jetton.go new file mode 100644 index 000000000..7f94cb1ab --- /dev/null +++ b/pkg/defi/jetton.go @@ -0,0 +1,39 @@ +package defi + +import ( + "github.com/tonkeeper/opentonapi/pkg/core" + "github.com/tonkeeper/opentonapi/pkg/oas" + "github.com/tonkeeper/tongo" +) + +type NormalizedJettonMeta struct { + Name string + Description string + Image string + Symbol string + Decimals int + Verification core.TrustType + Social []string + Websites []string + CustomPayloadApiUri string + PreviewImage string +} + +func JettonPreview(master tongo.AccountID, meta NormalizedJettonMeta, score int32) oas.JettonPreview { + preview := oas.JettonPreview{ + Address: master.ToRaw(), + Name: meta.Name, + Symbol: meta.Symbol, + Verification: oas.JettonVerificationType(meta.Verification), + Decimals: meta.Decimals, + Image: meta.PreviewImage, + Score: score, + } + if meta.CustomPayloadApiUri != "" { + preview.CustomPayloadAPIURI = oas.NewOptString(meta.CustomPayloadApiUri) + } + if meta.Description != "" { + preview.SetDescription(oas.NewOptString(meta.Description)) + } + return preview +} diff --git a/pkg/defi/json_format_test.go b/pkg/defi/json_format_test.go new file mode 100644 index 000000000..d9bd2e0a7 --- /dev/null +++ b/pkg/defi/json_format_test.go @@ -0,0 +1,106 @@ +package defi + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestStonfiPoolsResponseFormat(t *testing.T) { + sample := `{ + "pool_list": [ + { + "lp_balance": "1234567890", + "pool_address": "EQBZo_SgxgWMETkGinFvMxiShI3bvhkLHTXhMsEiD-tUBAfP" + }, + { + "lp_balance": "0", + "pool_address": "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs" + } + ] + }` + var resp stonfiWalletPoolsResponse + require.NoError(t, json.Unmarshal([]byte(sample), &resp)) + require.Len(t, resp.PoolList, 2) + require.Equal(t, "1234567890", resp.PoolList[0].LpBalance) + require.Equal(t, "EQBZo_SgxgWMETkGinFvMxiShI3bvhkLHTXhMsEiD-tUBAfP", resp.PoolList[0].Address) +} + +func TestStonfiPoolsResponseEmpty(t *testing.T) { + var resp stonfiWalletPoolsResponse + require.NoError(t, json.Unmarshal([]byte(`{"pool_list":[]}`), &resp)) + require.Empty(t, resp.PoolList) +} + +func TestStonfiPoolsResponseNull(t *testing.T) { + var resp stonfiWalletPoolsResponse + require.NoError(t, json.Unmarshal([]byte(`{"pool_list":null}`), &resp)) + require.Empty(t, resp.PoolList) +} + +func TestStonfiFarmsResponseFormat(t *testing.T) { + sample := `{ + "farm_list": [ + { + "pool_address": "EQBZo_SgxgWMETkGinFvMxiShI3bvhkLHTXhMsEiD-tUBAfP", + "nft_infos": [ + { + "staked_tokens": "9876543210", + "status": "active" + } + ] + } + ] + }` + var resp stonfiWalletFarmsResponse + require.NoError(t, json.Unmarshal([]byte(sample), &resp)) + require.Len(t, resp.FarmList, 1) + require.Len(t, resp.FarmList[0].NftInfos, 1) + require.Equal(t, "9876543210", resp.FarmList[0].NftInfos[0].StakedTokens) + require.Equal(t, "EQBZo_SgxgWMETkGinFvMxiShI3bvhkLHTXhMsEiD-tUBAfP", resp.FarmList[0].PoolAddress) +} + +func TestStonfiFarmsResponseEmpty(t *testing.T) { + var resp stonfiWalletFarmsResponse + require.NoError(t, json.Unmarshal([]byte(`{"farm_list":null}`), &resp)) + require.Empty(t, resp.FarmList) +} + +func TestSwapCoffeeResponseFormat(t *testing.T) { + sample := `{ + "pools": [ + { + "pool_address": "EQBZo_SgxgWMETkGinFvMxiShI3bvhkLHTXhMsEiD-tUBAfP", + "liquidity": { + "user_amount": "500000000" + } + }, + { + "pool_address": "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs", + "liquidity": { + "user_amount": "0" + } + } + ] + }` + var resp swapCoffeeResponse + require.NoError(t, json.Unmarshal([]byte(sample), &resp)) + require.Len(t, resp.Pools, 2) + require.Equal(t, "500000000", resp.Pools[0].Liquidity.UserAmount) + require.Equal(t, "EQBZo_SgxgWMETkGinFvMxiShI3bvhkLHTXhMsEiD-tUBAfP", resp.Pools[0].PoolAddress) +} + +func TestSwapCoffeeResponseEmpty(t *testing.T) { + var resp swapCoffeeResponse + require.NoError(t, json.Unmarshal([]byte(`{"pools":null}`), &resp)) + require.Empty(t, resp.Pools) +} + +func TestSwapCoffeeMissingLiquidity(t *testing.T) { + sample := `{"pools": [{"pool_address": "EQBZo_SgxgWMETkGinFvMxiShI3bvhkLHTXhMsEiD-tUBAfP"}]}` + var resp swapCoffeeResponse + require.NoError(t, json.Unmarshal([]byte(sample), &resp)) + require.Len(t, resp.Pools, 1) + require.Equal(t, "", resp.Pools[0].Liquidity.UserAmount) +} diff --git a/pkg/defi/liquid_staking.go b/pkg/defi/liquid_staking.go new file mode 100644 index 000000000..a99413987 --- /dev/null +++ b/pkg/defi/liquid_staking.go @@ -0,0 +1,45 @@ +package defi + +import ( + "context" + "sync" + + "github.com/tonkeeper/opentonapi/pkg/oas" + "github.com/tonkeeper/opentonapi/pkg/references" + "github.com/tonkeeper/tongo" +) + +func collectLiquidStakingJettons(ctx context.Context, d Deps, account tongo.AccountID, rates map[string]float64) []oas.DefiAsset { + var ( + mu sync.Mutex + wg sync.WaitGroup + assets []oas.DefiAsset + ) + for _, p := range references.LiquidStakingJettons { + p := p + wg.Add(1) + go func() { + defer wg.Done() + master := p.JettonMaster + wallets, err := d.Storage.GetJettonWalletsByOwnerAddress(ctx, account, &master, true, true, 1, 0) + if err != nil || len(wallets) == 0 || wallets[0].Balance.IsZero() { + return + } + w := wallets[0] + meta := d.JettonMeta(ctx, master) + score, _ := d.Score.GetJettonScore(master) + asset := oas.DefiAsset{ + Type: oas.DefiAssetTypeLiquidStaking, + Provider: defiProvider(p.DefiProviderMeta), + Amount: w.Balance.BigInt().Int64(), + Position: JettonPositionNanoTON(rates, master.ToRaw(), w.Balance, meta.Decimals), + } + asset.Jetton.SetTo(JettonPreview(master, meta, score)) + mu.Lock() + assets = append(assets, asset) + mu.Unlock() + }() + } + wg.Wait() + return assets +} diff --git a/pkg/defi/position_test.go b/pkg/defi/position_test.go new file mode 100644 index 000000000..5c710f833 --- /dev/null +++ b/pkg/defi/position_test.go @@ -0,0 +1,61 @@ +package defi + +import ( + "testing" + + "github.com/shopspring/decimal" + "github.com/stretchr/testify/require" +) + +func TestJettonPositionNanoTON(t *testing.T) { + rates := map[string]float64{ + "TON": 5.0, + "0:aabbccdd": 2.5, + } + + tests := []struct { + name string + balance string + decimals int + want int64 + }{ + { + name: "whole token", + balance: "1000000000", + decimals: 9, + want: 500_000_000, + }, + { + name: "two tokens", + balance: "2000000000", + decimals: 9, + want: 1_000_000_000, + }, + { + name: "zero balance", + balance: "0", + decimals: 9, + want: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bal, err := decimal.NewFromString(tt.balance) + require.NoError(t, err) + got := JettonPositionNanoTON(rates, "0:aabbccdd", bal, tt.decimals) + require.Equal(t, tt.want, got) + }) + } +} + +func TestJettonPositionNanoTONMissingRate(t *testing.T) { + rates := map[string]float64{"TON": 5.0} + bal, _ := decimal.NewFromString("1000000000") + require.Equal(t, int64(0), JettonPositionNanoTON(rates, "0:unknown", bal, 9)) +} + +func TestJettonPositionNanoTONMissingTON(t *testing.T) { + rates := map[string]float64{"0:aabbccdd": 2.5} + bal, _ := decimal.NewFromString("1000000000") + require.Equal(t, int64(0), JettonPositionNanoTON(rates, "0:aabbccdd", bal, 9)) +} diff --git a/pkg/defi/stonfi.go b/pkg/defi/stonfi.go new file mode 100644 index 000000000..d322ae289 --- /dev/null +++ b/pkg/defi/stonfi.go @@ -0,0 +1,244 @@ +package defi + +import ( + "context" + "fmt" + "math/big" + "sync" + + "github.com/shopspring/decimal" + "github.com/tonkeeper/opentonapi/pkg/core" + "github.com/tonkeeper/opentonapi/pkg/oas" + "github.com/tonkeeper/opentonapi/pkg/references" + "github.com/tonkeeper/tongo" + "go.uber.org/zap" +) + +type stonfiPoolEntry struct { + Address string `json:"address"` + Token0Address string `json:"token0_address"` + Token1Address string `json:"token1_address"` + Reserve0 string `json:"reserve0"` + Reserve1 string `json:"reserve1"` + LpTotalSupply string `json:"lp_total_supply"` + LpBalance string `json:"lp_balance"` +} + +type stonfiWalletPoolsResponse struct { + PoolList []stonfiPoolEntry `json:"pool_list"` +} + +type stonfiWalletFarmsResponse struct { + FarmList []struct { + PoolAddress string `json:"pool_address"` + NftInfos []struct { + StakedTokens string `json:"staked_tokens"` + Status string `json:"status"` + } `json:"nft_infos"` + } `json:"farm_list"` +} + +type stonfiWalletStakesResponse struct { + Nfts []struct { + StakedTokens string `json:"staked_tokens"` + Status string `json:"status"` + } `json:"nfts"` +} + +func collectStonfiExternalPositions(ctx context.Context, d Deps, account tongo.AccountID, rates map[string]float64) []oas.DefiAsset { + addr := account.ToHuman(true, false) + + var ( + poolsResp stonfiWalletPoolsResponse + farmsResp stonfiWalletFarmsResponse + poolsErr error + farmsErr error + wg sync.WaitGroup + ) + wg.Add(2) + go func() { + defer wg.Done() + poolsErr = fetchJSON(ctx, d.ProxyURL, fmt.Sprintf("https://api.ston.fi/v1/wallets/%s/pools", addr), &poolsResp) + }() + go func() { + defer wg.Done() + farmsErr = fetchJSON(ctx, d.ProxyURL, fmt.Sprintf("https://api.ston.fi/v1/wallets/%s/farms", addr), &farmsResp) + }() + wg.Wait() + + var poolsByAddr map[string]stonfiPoolEntry + if poolsErr == nil { + poolsByAddr = stonfiPoolsByAddress(poolsResp) + } else { + d.warn("stonfi pools API error", zap.Error(poolsErr)) + poolsByAddr = make(map[string]stonfiPoolEntry) + } + + var ( + lpAssets []oas.DefiAsset + farmAssets []oas.DefiAsset + procWg sync.WaitGroup + ) + + if poolsErr == nil { + procWg.Add(1) + go func() { + defer procWg.Done() + lpAssets = stonfiExternalLPAssets(ctx, d, poolsResp, rates) + }() + } + + if farmsErr != nil { + d.warn("stonfi farms API error", zap.Error(farmsErr)) + } else { + procWg.Add(1) + go func() { + defer procWg.Done() + farmAssets = stonfiExternalFarmAssets(ctx, d, farmsResp, poolsByAddr, rates) + }() + } + + procWg.Wait() + + assets := make([]oas.DefiAsset, 0, len(lpAssets)+len(farmAssets)) + assets = append(assets, lpAssets...) + assets = append(assets, farmAssets...) + return assets +} + +func stonfiPoolsByAddress(resp stonfiWalletPoolsResponse) map[string]stonfiPoolEntry { + m := make(map[string]stonfiPoolEntry, len(resp.PoolList)) + for _, p := range resp.PoolList { + m[p.Address] = p + } + return m +} + +func stonfiExternalLPAssets(ctx context.Context, d Deps, poolsResp stonfiWalletPoolsResponse, rates map[string]float64) []oas.DefiAsset { + var out []oas.DefiAsset + for _, p := range poolsResp.PoolList { + if p.LpBalance == "" || p.LpBalance == "0" { + continue + } + lpBal, ok := new(big.Int).SetString(p.LpBalance, 10) + if !ok || lpBal.Sign() == 0 { + continue + } + poolID, err := tongo.ParseAddress(p.Address) + if err != nil { + continue + } + meta := d.JettonMeta(ctx, poolID.ID) + score, _ := d.Score.GetJettonScore(poolID.ID) + pos := stonfiPoolPositionNanoTON(ctx, d, rates, p.Reserve0, p.Reserve1, p.LpTotalSupply, p.Token0Address, p.Token1Address, lpBal) + asset := oas.DefiAsset{ + Type: oas.DefiAssetTypeLiquidPool, + Provider: defiProvider(references.StonfiProvider), + Amount: lpBal.Int64(), + Position: pos, + } + asset.Jetton.SetTo(JettonPreview(poolID.ID, meta, score)) + out = append(out, asset) + } + return out +} + +func stonfiExternalFarmAssets(ctx context.Context, d Deps, farmsResp stonfiWalletFarmsResponse, poolsByAddr map[string]stonfiPoolEntry, rates map[string]float64) []oas.DefiAsset { + var out []oas.DefiAsset + for _, f := range farmsResp.FarmList { + if len(f.NftInfos) == 0 { + continue + } + pool, ok := poolsByAddr[f.PoolAddress] + if !ok { + continue + } + poolID, err := tongo.ParseAddress(f.PoolAddress) + if err != nil { + continue + } + meta := d.JettonMeta(ctx, poolID.ID) + score, _ := d.Score.GetJettonScore(poolID.ID) + for _, nft := range f.NftInfos { + if nft.Status != "active" { + continue + } + staked, ok := new(big.Int).SetString(nft.StakedTokens, 10) + if !ok || staked.Sign() == 0 { + continue + } + pos := stonfiPoolPositionNanoTON(ctx, d, rates, pool.Reserve0, pool.Reserve1, pool.LpTotalSupply, pool.Token0Address, pool.Token1Address, staked) + asset := oas.DefiAsset{ + Type: oas.DefiAssetTypeFarming, + Provider: defiProvider(references.StonfiProvider), + Amount: staked.Int64(), + Position: pos, + } + asset.Jetton.SetTo(JettonPreview(poolID.ID, meta, score)) + out = append(out, asset) + } + } + return out +} + +func collectStonfiStakingPositions(ctx context.Context, d Deps, account tongo.AccountID, rates map[string]float64) []oas.DefiAsset { + addr := account.ToHuman(true, false) + var resp stonfiWalletStakesResponse + if err := fetchJSON(ctx, d.ProxyURL, fmt.Sprintf("https://api.ston.fi/v1/wallets/%s/stakes", addr), &resp); err != nil { + d.warn("stonfi stakes API error", zap.Error(err)) + return nil + } + + stonMaster := references.StonJettonMaster + meta := d.JettonMeta(ctx, stonMaster) + score, _ := d.Score.GetJettonScore(stonMaster) + + var assets []oas.DefiAsset + for _, nft := range resp.Nfts { + if nft.Status != "active" { + continue + } + staked, ok := new(big.Int).SetString(nft.StakedTokens, 10) + if !ok || staked.Sign() == 0 { + continue + } + stakedDec := decimal.NewFromBigInt(staked, 0) + asset := oas.DefiAsset{ + Type: oas.DefiAssetTypeStaking, + Provider: defiProvider(references.StonfiProvider), + Amount: staked.Int64(), + Position: JettonPositionNanoTON(rates, stonMaster.ToRaw(), stakedDec, meta.Decimals), + } + asset.Jetton.SetTo(JettonPreview(stonMaster, meta, score)) + assets = append(assets, asset) + } + return assets +} + +func stonfiPoolPositionNanoTON(ctx context.Context, d Deps, rates map[string]float64, reserve0Str, reserve1Str, totalSupplyStr, token0AddrStr, token1AddrStr string, lpBalance *big.Int) int64 { + reserve0, ok0 := new(big.Int).SetString(reserve0Str, 10) + reserve1, ok1 := new(big.Int).SetString(reserve1Str, 10) + totalSupply, ok2 := new(big.Int).SetString(totalSupplyStr, 10) + if !ok0 || !ok1 || !ok2 || totalSupply.Sign() == 0 { + return 0 + } + + token0, err0 := tongo.ParseAddress(token0AddrStr) + token1, err1 := tongo.ParseAddress(token1AddrStr) + if err0 != nil || err1 != nil { + return 0 + } + + a0 := core.Currency{Type: core.CurrencyJetton, Jetton: &token0.ID} + a1 := core.Currency{Type: core.CurrencyJetton, Jetton: &token1.ID} + c0 := currencyReserveToNanoTON(ctx, d, rates, reserve0, a0) + c1 := currencyReserveToNanoTON(ctx, d, rates, reserve1, a1) + tvl := c0 + c1 + if tvl == 0 { + return 0 + } + + totalF, _ := new(big.Float).SetInt(totalSupply).Float64() + balF, _ := new(big.Float).SetInt(lpBalance).Float64() + return int64(balF / totalF * tvl) +} diff --git a/pkg/defi/swapcoffee.go b/pkg/defi/swapcoffee.go new file mode 100644 index 000000000..c6826c420 --- /dev/null +++ b/pkg/defi/swapcoffee.go @@ -0,0 +1,55 @@ +package defi + +import ( + "context" + "fmt" + + "github.com/shopspring/decimal" + "github.com/tonkeeper/opentonapi/pkg/oas" + "github.com/tonkeeper/opentonapi/pkg/references" + "github.com/tonkeeper/tongo" + "go.uber.org/zap" +) + +type swapCoffeeResponse struct { + Pools []struct { + PoolAddress string `json:"pool_address"` + Liquidity struct { + UserAmount string `json:"user_amount"` + } `json:"liquidity"` + } `json:"pools"` +} + +func collectSwapCoffeePositions(ctx context.Context, d Deps, account tongo.AccountID, rates map[string]float64) []oas.DefiAsset { + addr := account.ToHuman(true, false) + url := fmt.Sprintf("https://backend.swap.coffee/v1/yield/pools?blockchains=ton&with_liquidity_from=%s", addr) + + var resp swapCoffeeResponse + if err := fetchJSON(ctx, d.ProxyURL, url, &resp); err != nil { + d.warn("swap.coffee API error", zap.Error(err)) + return nil + } + + var assets []oas.DefiAsset + for _, p := range resp.Pools { + amount, err := decimal.NewFromString(p.Liquidity.UserAmount) + if err != nil || amount.IsZero() { + continue + } + poolID, err := tongo.ParseAddress(p.PoolAddress) + if err != nil { + continue + } + meta := d.JettonMeta(ctx, poolID.ID) + score, _ := d.Score.GetJettonScore(poolID.ID) + asset := oas.DefiAsset{ + Type: oas.DefiAssetTypeStaking, + Provider: defiProvider(references.SwapCoffeeProvider), + Amount: amount.BigInt().Int64(), + Position: JettonPositionNanoTON(rates, poolID.ID.ToRaw(), amount, meta.Decimals), + } + asset.Jetton.SetTo(JettonPreview(poolID.ID, meta, score)) + assets = append(assets, asset) + } + return assets +} diff --git a/pkg/defi/ton_staking.go b/pkg/defi/ton_staking.go new file mode 100644 index 000000000..8f0635f2b --- /dev/null +++ b/pkg/defi/ton_staking.go @@ -0,0 +1,51 @@ +package defi + +import ( + "context" + + "github.com/tonkeeper/opentonapi/pkg/oas" + "github.com/tonkeeper/opentonapi/pkg/references" + "github.com/tonkeeper/tongo" +) + +func collectWhalesStaking(ctx context.Context, d Deps, account tongo.AccountID) []oas.DefiAsset { + nominators, err := d.Storage.GetParticipatingInWhalesPools(ctx, account) + if err != nil { + return nil + } + var assets []oas.DefiAsset + for _, n := range nominators { + total := n.MemberBalance + n.MemberPendingDeposit + n.MemberWithdraw + if total == 0 { + continue + } + assets = append(assets, oas.DefiAsset{ + Type: oas.DefiAssetTypeStaking, + Provider: defiProvider(references.WhalesProvider), + Amount: total, + Position: total, + }) + } + return assets +} + +func collectTFStaking(ctx context.Context, d Deps, account tongo.AccountID) []oas.DefiAsset { + pools, err := d.Storage.GetParticipatingInTfPools(ctx, account) + if err != nil { + return nil + } + var assets []oas.DefiAsset + for _, n := range pools { + total := n.MemberBalance + n.MemberPendingDeposit + n.MemberWithdraw + if total == 0 { + continue + } + assets = append(assets, oas.DefiAsset{ + Type: oas.DefiAssetTypeStaking, + Provider: defiProvider(references.TFProvider), + Amount: total, + Position: total, + }) + } + return assets +} diff --git a/pkg/defi/tonco.go b/pkg/defi/tonco.go new file mode 100644 index 000000000..ba506839b --- /dev/null +++ b/pkg/defi/tonco.go @@ -0,0 +1,94 @@ +package defi + +import ( + "context" + "math/big" + + "github.com/tonkeeper/opentonapi/pkg/core" + "github.com/tonkeeper/opentonapi/pkg/oas" + "github.com/tonkeeper/opentonapi/pkg/references" + "github.com/tonkeeper/tongo" + "github.com/tonkeeper/tongo/tlb" +) + +func collectToncoPositions(ctx context.Context, d Deps, account tongo.AccountID) []oas.DefiAsset { + collectionFilter := &core.Filter[tongo.AccountID]{Value: references.ToncoNFTCollection} + ownerFilter := &core.Filter[tongo.AccountID]{Value: account} + + nftAddresses, err := d.Storage.SearchNFTs(ctx, collectionFilter, ownerFilter, false, false, 100, 0) + if err != nil || len(nftAddresses) == 0 { + return nil + } + nfts, err := d.Storage.GetNFTs(ctx, nftAddresses) + if err != nil { + return nil + } + + assets := make([]oas.DefiAsset, 0, len(nfts)) + for _, nft := range nfts { + if nft.CollectionAddress == nil { + continue + } + liquidity, token0ID, token1ID := toncoPositionInfo(ctx, d, nft.Address, *nft.CollectionAddress) + asset := oas.DefiAsset{ + Type: oas.DefiAssetTypeLiquidPool, + Provider: defiProvider(references.ToncoProvider), + Amount: liquidity, + Position: 0, // position in nanoTON requires current sqrt price — not computed here + } + preview := oas.DefiNftPreview{ + Address: nft.Address.ToRaw(), + Collection: nft.CollectionAddress.ToRaw(), + } + if token0ID != nil { + meta0 := d.JettonMeta(ctx, *token0ID) + score0, _ := d.Score.GetJettonScore(*token0ID) + preview.Token0.SetTo(JettonPreview(*token0ID, meta0, score0)) + } + if token1ID != nil { + meta1 := d.JettonMeta(ctx, *token1ID) + score1, _ := d.Score.GetJettonScore(*token1ID) + preview.Token1.SetTo(JettonPreview(*token1ID, meta1, score1)) + } + asset.Nft.SetTo(preview) + assets = append(assets, asset) + } + return assets +} + +func toncoPositionInfo(ctx context.Context, d Deps, nftAddr tongo.AccountID, collectionAddr tongo.AccountID) (int64, *tongo.AccountID, *tongo.AccountID) { + _, posStack, err := d.Executor.RunSmcMethod(ctx, nftAddr, "getPositionInfo", tlb.VmStack{}) + var liquidity int64 + if err == nil && posStack.Len() >= 5 { + v := posStack.Peek(2) + if v.SumType == "VmStkInt" { + liquidity = (*big.Int)(&v.VmStkInt).Int64() + } + } + + var token0, token1 *tongo.AccountID + _, poolStack, err := d.Executor.RunSmcMethod(ctx, collectionAddr, "getPoolData", tlb.VmStack{}) + if err == nil && poolStack.Len() >= 2 { + for i, idx := range []int{0, 1} { + v := poolStack.Peek(idx) + if v.SumType != "VmStkSlice" { + continue + } + var addr tlb.MsgAddress + if err := v.VmStkSlice.UnmarshalToTlbStruct(&addr); err != nil { + continue + } + accountID, err := tongo.AccountIDFromTlb(addr) + if err != nil || accountID == nil { + continue + } + if i == 0 { + token0 = accountID + } else { + token1 = accountID + } + } + } + + return liquidity, token0, token1 +} diff --git a/pkg/defi/utils.go b/pkg/defi/utils.go new file mode 100644 index 000000000..6f044d0d9 --- /dev/null +++ b/pkg/defi/utils.go @@ -0,0 +1,92 @@ +package defi + +import ( + "context" + "encoding/json" + "fmt" + "io" + "math/big" + "net/http" + "time" + + "github.com/shopspring/decimal" + "github.com/tonkeeper/opentonapi/pkg/core" + "github.com/tonkeeper/opentonapi/pkg/oas" + "github.com/tonkeeper/opentonapi/pkg/references" +) + +func currencyReserveToNanoTON(ctx context.Context, d Deps, rates map[string]float64, reserve *big.Int, asset core.Currency) float64 { + reserveF, _ := new(big.Float).SetInt(reserve).Float64() + switch asset.Type { + case core.CurrencyTON: + return reserveF + case core.CurrencyJetton: + if asset.Jetton == nil { + return 0 + } + tonPrice, ok := rates["TON"] + if !ok || tonPrice == 0 { + return 0 + } + tokenPrice, ok := rates[asset.Jetton.ToRaw()] + if !ok || tokenPrice == 0 { + return 0 + } + meta := d.JettonMeta(ctx, *asset.Jetton) + return (reserveF / pow10float(meta.Decimals)) * (tokenPrice / tonPrice) * 1e9 + } + return 0 +} + +func JettonPositionNanoTON(rates map[string]float64, jettonMasterRaw string, balanceRaw decimal.Decimal, decimals int) int64 { + tonPrice, ok := rates["TON"] + if !ok || tonPrice == 0 { + return 0 + } + tokenPrice, ok := rates[jettonMasterRaw] + if !ok || tokenPrice == 0 { + return 0 + } + bal, _ := balanceRaw.Float64() + divisor := pow10float(decimals) + positionTON := (bal / divisor) * (tokenPrice / tonPrice) + return int64(positionTON * 1e9) +} + +func pow10float(n int) float64 { + v := 1.0 + for i := 0; i < n; i++ { + v *= 10 + } + return v +} + +func defiProvider(meta references.DefiProviderMeta) oas.DefiProvider { + return oas.DefiProvider{ + Name: meta.Name, + URL: meta.URL, + Image: meta.Image, + } +} + +func fetchJSON(ctx context.Context, proxyURL, url string, dst any) error { + reqCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, proxyURL+url, nil) + if err != nil { + return err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status %d from %s", resp.StatusCode, url) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + return json.Unmarshal(body, dst) +} diff --git a/pkg/litestorage/bidask.go b/pkg/litestorage/bidask.go new file mode 100644 index 000000000..01beaa4c4 --- /dev/null +++ b/pkg/litestorage/bidask.go @@ -0,0 +1,11 @@ +package litestorage + +import ( + "context" + + "github.com/tonkeeper/tongo" +) + +func (s *LiteStorage) BidaskPools(_ context.Context) ([]tongo.AccountID, error) { + return nil, nil +} diff --git a/pkg/oas/oas_client_gen.go b/pkg/oas/oas_client_gen.go index 4b78e7a90..f1bea5451 100644 --- a/pkg/oas/oas_client_gen.go +++ b/pkg/oas/oas_client_gen.go @@ -125,6 +125,12 @@ type Invoker interface { // // GET /v2/accounts/{account_id} GetAccount(ctx context.Context, params GetAccountParams) (*Account, error) + // GetAccountDefiAssets invokes getAccountDefiAssets operation. + // + // Get all DeFi assets (liquid staking, liquid pools, staking, lending, farming) for an account. + // + // GET /v2/defi/{account_id}/assets + GetAccountDefiAssets(ctx context.Context, params GetAccountDefiAssetsParams) (*DefiAssets, error) // GetAccountDiff invokes getAccountDiff operation. // // Get account's balance change. @@ -2417,6 +2423,99 @@ func (c *Client) sendGetAccount(ctx context.Context, params GetAccountParams) (r return result, nil } +// GetAccountDefiAssets invokes getAccountDefiAssets operation. +// +// Get all DeFi assets (liquid staking, liquid pools, staking, lending, farming) for an account. +// +// GET /v2/defi/{account_id}/assets +func (c *Client) GetAccountDefiAssets(ctx context.Context, params GetAccountDefiAssetsParams) (*DefiAssets, error) { + res, err := c.sendGetAccountDefiAssets(ctx, params) + return res, err +} + +func (c *Client) sendGetAccountDefiAssets(ctx context.Context, params GetAccountDefiAssetsParams) (res *DefiAssets, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("getAccountDefiAssets"), + semconv.HTTPRequestMethodKey.String("GET"), + semconv.URLTemplateKey.String("/v2/defi/{account_id}/assets"), + } + otelAttrs = append(otelAttrs, c.cfg.Attributes...) + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, GetAccountDefiAssetsOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [3]string + pathParts[0] = "/v2/defi/" + { + // Encode "account_id" parameter. + e := uri.NewPathEncoder(uri.PathEncoderConfig{ + Param: "account_id", + Style: uri.PathStyleSimple, + Explode: false, + }) + if err := func() error { + return e.EncodeValue(conv.StringToString(params.AccountID)) + }(); err != nil { + return res, errors.Wrap(err, "encode path") + } + encoded, err := e.Result() + if err != nil { + return res, errors.Wrap(err, "encode path") + } + pathParts[1] = encoded + } + pathParts[2] = "/assets" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "GET", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + body := resp.Body + defer body.Close() + + stage = "DecodeResponse" + result, err := decodeGetAccountDefiAssetsResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + // GetAccountDiff invokes getAccountDiff operation. // // Get account's balance change. diff --git a/pkg/oas/oas_handlers_gen.go b/pkg/oas/oas_handlers_gen.go index 054602d3c..b6556d60c 100644 --- a/pkg/oas/oas_handlers_gen.go +++ b/pkg/oas/oas_handlers_gen.go @@ -2606,6 +2606,160 @@ func (s *Server) handleGetAccountRequest(args [1]string, argsEscaped bool, w htt } } +// handleGetAccountDefiAssetsRequest handles getAccountDefiAssets operation. +// +// Get all DeFi assets (liquid staking, liquid pools, staking, lending, farming) for an account. +// +// GET /v2/defi/{account_id}/assets +func (s *Server) handleGetAccountDefiAssetsRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("getAccountDefiAssets"), + semconv.HTTPRequestMethodKey.String("GET"), + semconv.HTTPRouteKey.String("/v2/defi/{account_id}/assets"), + } + // Add attributes from config. + otelAttrs = append(otelAttrs, s.cfg.Attributes...) + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), GetAccountDefiAssetsOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code < 100 || code >= 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: GetAccountDefiAssetsOperation, + ID: "getAccountDefiAssets", + } + ) + params, err := decodeGetAccountDefiAssetsParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var rawBody []byte + + var response *DefiAssets + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: GetAccountDefiAssetsOperation, + OperationSummary: "", + OperationID: "getAccountDefiAssets", + Body: nil, + RawBody: rawBody, + Params: middleware.Parameters{ + { + Name: "account_id", + In: "path", + }: params.AccountID, + }, + Raw: r, + } + + type ( + Request = struct{} + Params = GetAccountDefiAssetsParams + Response = *DefiAssets + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackGetAccountDefiAssetsParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.GetAccountDefiAssets(ctx, params) + return response, err + }, + ) + } else { + response, err = s.h.GetAccountDefiAssets(ctx, params) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w, span); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeGetAccountDefiAssetsResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + // handleGetAccountDiffRequest handles getAccountDiff operation. // // Get account's balance change. diff --git a/pkg/oas/oas_json_gen.go b/pkg/oas/oas_json_gen.go index 73f701338..a9c0dd77d 100644 --- a/pkg/oas/oas_json_gen.go +++ b/pkg/oas/oas_json_gen.go @@ -14029,6 +14029,612 @@ func (s *DecodedRawMessageMessage) UnmarshalJSON(data []byte) error { return s.Decode(d) } +// Encode implements json.Marshaler. +func (s *DefiAsset) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *DefiAsset) encodeFields(e *jx.Encoder) { + { + e.FieldStart("type") + s.Type.Encode(e) + } + { + if s.Jetton.Set { + e.FieldStart("jetton") + s.Jetton.Encode(e) + } + } + { + if s.Nft.Set { + e.FieldStart("nft") + s.Nft.Encode(e) + } + } + { + e.FieldStart("provider") + s.Provider.Encode(e) + } + { + e.FieldStart("amount") + e.Int64(s.Amount) + } + { + e.FieldStart("position") + e.Int64(s.Position) + } +} + +var jsonFieldsNameOfDefiAsset = [6]string{ + 0: "type", + 1: "jetton", + 2: "nft", + 3: "provider", + 4: "amount", + 5: "position", +} + +// Decode decodes DefiAsset from json. +func (s *DefiAsset) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode DefiAsset to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "type": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Type.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"type\"") + } + case "jetton": + if err := func() error { + s.Jetton.Reset() + if err := s.Jetton.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"jetton\"") + } + case "nft": + if err := func() error { + s.Nft.Reset() + if err := s.Nft.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"nft\"") + } + case "provider": + requiredBitSet[0] |= 1 << 3 + if err := func() error { + if err := s.Provider.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"provider\"") + } + case "amount": + requiredBitSet[0] |= 1 << 4 + if err := func() error { + v, err := d.Int64() + s.Amount = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"amount\"") + } + case "position": + requiredBitSet[0] |= 1 << 5 + if err := func() error { + v, err := d.Int64() + s.Position = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"position\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode DefiAsset") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00111001, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfDefiAsset) { + name = jsonFieldsNameOfDefiAsset[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *DefiAsset) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *DefiAsset) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes DefiAssetType as json. +func (s DefiAssetType) Encode(e *jx.Encoder) { + e.Str(string(s)) +} + +// Decode decodes DefiAssetType from json. +func (s *DefiAssetType) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode DefiAssetType to nil") + } + v, err := d.StrBytes() + if err != nil { + return err + } + // Try to use constant string. + switch DefiAssetType(v) { + case DefiAssetTypeLiquidStaking: + *s = DefiAssetTypeLiquidStaking + case DefiAssetTypeLiquidPool: + *s = DefiAssetTypeLiquidPool + case DefiAssetTypeStaking: + *s = DefiAssetTypeStaking + case DefiAssetTypeLending: + *s = DefiAssetTypeLending + case DefiAssetTypeFarming: + *s = DefiAssetTypeFarming + default: + *s = DefiAssetType(v) + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s DefiAssetType) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *DefiAssetType) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *DefiAssets) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *DefiAssets) encodeFields(e *jx.Encoder) { + { + e.FieldStart("assets") + e.ArrStart() + for _, elem := range s.Assets { + elem.Encode(e) + } + e.ArrEnd() + } +} + +var jsonFieldsNameOfDefiAssets = [1]string{ + 0: "assets", +} + +// Decode decodes DefiAssets from json. +func (s *DefiAssets) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode DefiAssets to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "assets": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + s.Assets = make([]DefiAsset, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem DefiAsset + if err := elem.Decode(d); err != nil { + return err + } + s.Assets = append(s.Assets, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"assets\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode DefiAssets") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000001, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfDefiAssets) { + name = jsonFieldsNameOfDefiAssets[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *DefiAssets) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *DefiAssets) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *DefiNftPreview) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *DefiNftPreview) encodeFields(e *jx.Encoder) { + { + e.FieldStart("address") + e.Str(s.Address) + } + { + e.FieldStart("collection") + e.Str(s.Collection) + } + { + if s.Token0.Set { + e.FieldStart("token0") + s.Token0.Encode(e) + } + } + { + if s.Token1.Set { + e.FieldStart("token1") + s.Token1.Encode(e) + } + } +} + +var jsonFieldsNameOfDefiNftPreview = [4]string{ + 0: "address", + 1: "collection", + 2: "token0", + 3: "token1", +} + +// Decode decodes DefiNftPreview from json. +func (s *DefiNftPreview) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode DefiNftPreview to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "address": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Str() + s.Address = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"address\"") + } + case "collection": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + v, err := d.Str() + s.Collection = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"collection\"") + } + case "token0": + if err := func() error { + s.Token0.Reset() + if err := s.Token0.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"token0\"") + } + case "token1": + if err := func() error { + s.Token1.Reset() + if err := s.Token1.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"token1\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode DefiNftPreview") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000011, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfDefiNftPreview) { + name = jsonFieldsNameOfDefiNftPreview[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *DefiNftPreview) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *DefiNftPreview) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *DefiProvider) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *DefiProvider) encodeFields(e *jx.Encoder) { + { + e.FieldStart("name") + e.Str(s.Name) + } + { + e.FieldStart("url") + e.Str(s.URL) + } + { + e.FieldStart("image") + e.Str(s.Image) + } +} + +var jsonFieldsNameOfDefiProvider = [3]string{ + 0: "name", + 1: "url", + 2: "image", +} + +// Decode decodes DefiProvider from json. +func (s *DefiProvider) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode DefiProvider to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "name": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Str() + s.Name = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"name\"") + } + case "url": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + v, err := d.Str() + s.URL = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"url\"") + } + case "image": + requiredBitSet[0] |= 1 << 2 + if err := func() error { + v, err := d.Str() + s.Image = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"image\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode DefiProvider") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000111, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfDefiProvider) { + name = jsonFieldsNameOfDefiProvider[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *DefiProvider) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *DefiProvider) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + // Encode implements json.Marshaler. func (s *DepositStakeAction) Encode(e *jx.Encoder) { e.ObjStart() @@ -33914,6 +34520,39 @@ func (s *OptDecodedMessageExtInMsgDecodedWalletV5) UnmarshalJSON(data []byte) er return s.Decode(d) } +// Encode encodes DefiNftPreview as json. +func (o OptDefiNftPreview) Encode(e *jx.Encoder) { + if !o.Set { + return + } + o.Value.Encode(e) +} + +// Decode decodes DefiNftPreview from json. +func (o *OptDefiNftPreview) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptDefiNftPreview to nil") + } + o.Set = true + if err := o.Value.Decode(d); err != nil { + return err + } + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptDefiNftPreview) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptDefiNftPreview) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + // Encode encodes DepositStakeAction as json. func (o OptDepositStakeAction) Encode(e *jx.Encoder) { if !o.Set { diff --git a/pkg/oas/oas_operations_gen.go b/pkg/oas/oas_operations_gen.go index 78017c570..1353762dd 100644 --- a/pkg/oas/oas_operations_gen.go +++ b/pkg/oas/oas_operations_gen.go @@ -22,6 +22,7 @@ const ( GaslessEstimateOperation OperationName = "GaslessEstimate" GaslessSendOperation OperationName = "GaslessSend" GetAccountOperation OperationName = "GetAccount" + GetAccountDefiAssetsOperation OperationName = "GetAccountDefiAssets" GetAccountDiffOperation OperationName = "GetAccountDiff" GetAccountDnsExpiringOperation OperationName = "GetAccountDnsExpiring" GetAccountEventOperation OperationName = "GetAccountEvent" diff --git a/pkg/oas/oas_parameters_gen.go b/pkg/oas/oas_parameters_gen.go index eb18e33aa..597e33d76 100644 --- a/pkg/oas/oas_parameters_gen.go +++ b/pkg/oas/oas_parameters_gen.go @@ -1357,6 +1357,72 @@ func decodeGetAccountParams(args [1]string, argsEscaped bool, r *http.Request) ( return params, nil } +// GetAccountDefiAssetsParams is parameters of getAccountDefiAssets operation. +type GetAccountDefiAssetsParams struct { + // Account ID. + AccountID string +} + +func unpackGetAccountDefiAssetsParams(packed middleware.Parameters) (params GetAccountDefiAssetsParams) { + { + key := middleware.ParameterKey{ + Name: "account_id", + In: "path", + } + params.AccountID = packed[key].(string) + } + return params +} + +func decodeGetAccountDefiAssetsParams(args [1]string, argsEscaped bool, r *http.Request) (params GetAccountDefiAssetsParams, _ error) { + // Decode path: account_id. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "account_id", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + params.AccountID = c + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "account_id", + In: "path", + Err: err, + } + } + return params, nil +} + // GetAccountDiffParams is parameters of getAccountDiff operation. type GetAccountDiffParams struct { // Account ID. diff --git a/pkg/oas/oas_response_decoders_gen.go b/pkg/oas/oas_response_decoders_gen.go index da6798a27..168cfc51c 100644 --- a/pkg/oas/oas_response_decoders_gen.go +++ b/pkg/oas/oas_response_decoders_gen.go @@ -1485,6 +1485,98 @@ func decodeGetAccountResponse(resp *http.Response) (res *Account, _ error) { return res, errors.Wrap(defRes, "error") } +func decodeGetAccountDefiAssetsResponse(resp *http.Response) (res *DefiAssets, _ error) { + switch resp.StatusCode { + case 200: + // Code 200. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response DefiAssets + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + } + // Convenient error response. + defRes, err := func() (res *ErrorStatusCode, err error) { + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &ErrorStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode) + } + return res, errors.Wrap(defRes, "error") +} + func decodeGetAccountDiffResponse(resp *http.Response) (res *GetAccountDiffOK, _ error) { switch resp.StatusCode { case 200: diff --git a/pkg/oas/oas_response_encoders_gen.go b/pkg/oas/oas_response_encoders_gen.go index da0126e64..5a21c0b0e 100644 --- a/pkg/oas/oas_response_encoders_gen.go +++ b/pkg/oas/oas_response_encoders_gen.go @@ -261,6 +261,20 @@ func encodeGetAccountResponse(response *Account, w http.ResponseWriter, span tra return nil } +func encodeGetAccountDefiAssetsResponse(response *DefiAssets, w http.ResponseWriter, span trace.Span) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + span.SetStatus(codes.Ok, http.StatusText(200)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil +} + func encodeGetAccountDiffResponse(response *GetAccountDiffOK, w http.ResponseWriter, span trace.Span) error { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(200) diff --git a/pkg/oas/oas_router_gen.go b/pkg/oas/oas_router_gen.go index 6a6da3456..1a32e5137 100644 --- a/pkg/oas/oas_router_gen.go +++ b/pkg/oas/oas_router_gen.go @@ -11,43 +11,43 @@ import ( ) var ( - rn63AllowedHeaders = map[string]string{ + rn67AllowedHeaders = map[string]string{ "POST": "Content-Type", } - rn39AllowedHeaders = map[string]string{ + rn43AllowedHeaders = map[string]string{ "GET": "Accept-Language", } rn21AllowedHeaders = map[string]string{ "POST": "Accept-Language,Content-Type", } - rn38AllowedHeaders = map[string]string{ + rn42AllowedHeaders = map[string]string{ "GET": "Accept-Language", } - rn43AllowedHeaders = map[string]string{ + rn47AllowedHeaders = map[string]string{ "GET": "Accept-Language", } - rn48AllowedHeaders = map[string]string{ + rn52AllowedHeaders = map[string]string{ "GET": "Accept-Language", } - rn52AllowedHeaders = map[string]string{ + rn56AllowedHeaders = map[string]string{ "GET": "Accept-Language", } - rn80AllowedHeaders = map[string]string{ + rn84AllowedHeaders = map[string]string{ "POST": "Content-Type", } rn27AllowedHeaders = map[string]string{ "POST": "Content-Type", } - rn195AllowedHeaders = map[string]string{ + rn199AllowedHeaders = map[string]string{ "POST": "Content-Type", } rn22AllowedHeaders = map[string]string{ "POST": "Accept-Language,Content-Type", } - rn92AllowedHeaders = map[string]string{ + rn96AllowedHeaders = map[string]string{ "GET": "Accept-Language", } - rn111AllowedHeaders = map[string]string{ + rn115AllowedHeaders = map[string]string{ "GET": "Accept-Language", } rn31AllowedHeaders = map[string]string{ @@ -56,40 +56,40 @@ var ( rn32AllowedHeaders = map[string]string{ "POST": "Content-Type", } - rn106AllowedHeaders = map[string]string{ + rn110AllowedHeaders = map[string]string{ "POST": "Content-Type", } - rn196AllowedHeaders = map[string]string{ + rn200AllowedHeaders = map[string]string{ "POST": "Content-Type", } rn12AllowedHeaders = map[string]string{ "POST": "Content-Type", } - rn126AllowedHeaders = map[string]string{ + rn130AllowedHeaders = map[string]string{ "POST": "Content-Type", } - rn121AllowedHeaders = map[string]string{ + rn125AllowedHeaders = map[string]string{ "POST": "Content-Type", } - rn125AllowedHeaders = map[string]string{ + rn129AllowedHeaders = map[string]string{ "GET": "Accept-Language", } - rn192AllowedHeaders = map[string]string{ + rn196AllowedHeaders = map[string]string{ "POST": "Content-Type", } - rn175AllowedHeaders = map[string]string{ + rn179AllowedHeaders = map[string]string{ "GET": "Accept-Language", } - rn178AllowedHeaders = map[string]string{ + rn182AllowedHeaders = map[string]string{ "GET": "Accept-Language", } - rn45AllowedHeaders = map[string]string{ + rn49AllowedHeaders = map[string]string{ "POST": "Content-Type", } rn23AllowedHeaders = map[string]string{ "POST": "Content-Type", } - rn199AllowedHeaders = map[string]string{ + rn203AllowedHeaders = map[string]string{ "POST": "Content-Type", } rn24AllowedHeaders = map[string]string{ @@ -188,7 +188,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn63AllowedHeaders, + allowedHeaders: rn67AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -402,7 +402,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn39AllowedHeaders, + allowedHeaders: rn43AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -472,7 +472,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn38AllowedHeaders, + allowedHeaders: rn42AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -523,7 +523,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn43AllowedHeaders, + allowedHeaders: rn47AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -650,7 +650,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn48AllowedHeaders, + allowedHeaders: rn52AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -734,7 +734,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn52AllowedHeaders, + allowedHeaders: rn56AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -949,7 +949,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn80AllowedHeaders, + allowedHeaders: rn84AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -1530,7 +1530,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn195AllowedHeaders, + allowedHeaders: rn199AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -1681,9 +1681,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } - case 'd': // Prefix: "dns/" + case 'd': // Prefix: "d" - if l := len("dns/"); len(elem) >= l && elem[0:l] == "dns/" { + if l := len("d"); len(elem) >= l && elem[0:l] == "d" { elem = elem[l:] } else { break @@ -1693,75 +1693,30 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { break } switch elem[0] { - case 'a': // Prefix: "auctions" - origElem := elem - if l := len("auctions"); len(elem) >= l && elem[0:l] == "auctions" { + case 'e': // Prefix: "efi/" + + if l := len("efi/"); len(elem) >= l && elem[0:l] == "efi/" { elem = elem[l:] } else { break } - if len(elem) == 0 { - // Leaf node. - switch r.Method { - case "GET": - s.handleGetAllAuctionsRequest([0]string{}, elemIsEscaped, w, r) - default: - s.notAllowed(w, r, notAllowedParams{ - allowedMethods: "GET", - allowedHeaders: nil, - acceptPost: "", - acceptPatch: "", - }) - } - - return - } - - elem = origElem - } - // Param: "domain_name" - // Match until "/" - idx := strings.IndexByte(elem, '/') - if idx < 0 { - idx = len(elem) - } - args[0] = elem[:idx] - elem = elem[idx:] - - if len(elem) == 0 { - switch r.Method { - case "GET": - s.handleGetDnsInfoRequest([1]string{ - args[0], - }, elemIsEscaped, w, r) - default: - s.notAllowed(w, r, notAllowedParams{ - allowedMethods: "GET", - allowedHeaders: nil, - acceptPost: "", - acceptPatch: "", - }) - } - - return - } - switch elem[0] { - case '/': // Prefix: "/" - - if l := len("/"); len(elem) >= l && elem[0:l] == "/" { - elem = elem[l:] - } else { - break + // Param: "account_id" + // Match until "/" + idx := strings.IndexByte(elem, '/') + if idx < 0 { + idx = len(elem) } + args[0] = elem[:idx] + elem = elem[idx:] if len(elem) == 0 { break } switch elem[0] { - case 'b': // Prefix: "bids" + case '/': // Prefix: "/assets" - if l := len("bids"); len(elem) >= l && elem[0:l] == "bids" { + if l := len("/assets"); len(elem) >= l && elem[0:l] == "/assets" { elem = elem[l:] } else { break @@ -1771,7 +1726,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Leaf node. switch r.Method { case "GET": - s.handleGetDomainBidsRequest([1]string{ + s.handleGetAccountDefiAssetsRequest([1]string{ args[0], }, elemIsEscaped, w, r) default: @@ -1786,9 +1741,23 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - case 'r': // Prefix: "resolve" + } + + case 'n': // Prefix: "ns/" - if l := len("resolve"); len(elem) >= l && elem[0:l] == "resolve" { + if l := len("ns/"); len(elem) >= l && elem[0:l] == "ns/" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'a': // Prefix: "auctions" + origElem := elem + if l := len("auctions"); len(elem) >= l && elem[0:l] == "auctions" { elem = elem[l:] } else { break @@ -1798,9 +1767,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Leaf node. switch r.Method { case "GET": - s.handleDnsResolveRequest([1]string{ - args[0], - }, elemIsEscaped, w, r) + s.handleGetAllAuctionsRequest([0]string{}, elemIsEscaped, w, r) default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", @@ -1813,6 +1780,103 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + elem = origElem + } + // Param: "domain_name" + // Match until "/" + idx := strings.IndexByte(elem, '/') + if idx < 0 { + idx = len(elem) + } + args[0] = elem[:idx] + elem = elem[idx:] + + if len(elem) == 0 { + switch r.Method { + case "GET": + s.handleGetDnsInfoRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, notAllowedParams{ + allowedMethods: "GET", + allowedHeaders: nil, + acceptPost: "", + acceptPatch: "", + }) + } + + return + } + switch elem[0] { + case '/': // Prefix: "/" + + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'b': // Prefix: "bids" + + if l := len("bids"); len(elem) >= l && elem[0:l] == "bids" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "GET": + s.handleGetDomainBidsRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, notAllowedParams{ + allowedMethods: "GET", + allowedHeaders: nil, + acceptPost: "", + acceptPatch: "", + }) + } + + return + } + + case 'r': // Prefix: "resolve" + + if l := len("resolve"); len(elem) >= l && elem[0:l] == "resolve" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "GET": + s.handleDnsResolveRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, notAllowedParams{ + allowedMethods: "GET", + allowedHeaders: nil, + acceptPost: "", + acceptPatch: "", + }) + } + + return + } + + } + } } @@ -1886,7 +1950,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn92AllowedHeaders, + allowedHeaders: rn96AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -1913,7 +1977,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn111AllowedHeaders, + allowedHeaders: rn115AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -2114,7 +2178,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn106AllowedHeaders, + allowedHeaders: rn110AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -2911,7 +2975,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn196AllowedHeaders, + allowedHeaders: rn200AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -3067,7 +3131,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn126AllowedHeaders, + allowedHeaders: rn130AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -3129,7 +3193,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn121AllowedHeaders, + allowedHeaders: rn125AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -3245,7 +3309,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn125AllowedHeaders, + allowedHeaders: rn129AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -3360,7 +3424,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn192AllowedHeaders, + allowedHeaders: rn196AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -3846,7 +3910,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn175AllowedHeaders, + allowedHeaders: rn179AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -3900,7 +3964,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn178AllowedHeaders, + allowedHeaders: rn182AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -4032,7 +4096,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn45AllowedHeaders, + allowedHeaders: rn49AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -4140,7 +4204,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn199AllowedHeaders, + allowedHeaders: rn203AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -5795,9 +5859,9 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { } - case 'd': // Prefix: "dns/" + case 'd': // Prefix: "d" - if l := len("dns/"); len(elem) >= l && elem[0:l] == "dns/" { + if l := len("d"); len(elem) >= l && elem[0:l] == "d" { elem = elem[l:] } else { break @@ -5807,73 +5871,30 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { break } switch elem[0] { - case 'a': // Prefix: "auctions" - origElem := elem - if l := len("auctions"); len(elem) >= l && elem[0:l] == "auctions" { + case 'e': // Prefix: "efi/" + + if l := len("efi/"); len(elem) >= l && elem[0:l] == "efi/" { elem = elem[l:] } else { break } - if len(elem) == 0 { - // Leaf node. - switch method { - case "GET": - r.name = GetAllAuctionsOperation - r.summary = "" - r.operationID = "getAllAuctions" - r.operationGroup = "" - r.pathPattern = "/v2/dns/auctions" - r.args = args - r.count = 0 - return r, true - default: - return - } - } - - elem = origElem - } - // Param: "domain_name" - // Match until "/" - idx := strings.IndexByte(elem, '/') - if idx < 0 { - idx = len(elem) - } - args[0] = elem[:idx] - elem = elem[idx:] - - if len(elem) == 0 { - switch method { - case "GET": - r.name = GetDnsInfoOperation - r.summary = "" - r.operationID = "getDnsInfo" - r.operationGroup = "" - r.pathPattern = "/v2/dns/{domain_name}" - r.args = args - r.count = 1 - return r, true - default: - return - } - } - switch elem[0] { - case '/': // Prefix: "/" - - if l := len("/"); len(elem) >= l && elem[0:l] == "/" { - elem = elem[l:] - } else { - break + // Param: "account_id" + // Match until "/" + idx := strings.IndexByte(elem, '/') + if idx < 0 { + idx = len(elem) } + args[0] = elem[:idx] + elem = elem[idx:] if len(elem) == 0 { break } switch elem[0] { - case 'b': // Prefix: "bids" + case '/': // Prefix: "/assets" - if l := len("bids"); len(elem) >= l && elem[0:l] == "bids" { + if l := len("/assets"); len(elem) >= l && elem[0:l] == "/assets" { elem = elem[l:] } else { break @@ -5883,11 +5904,11 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { // Leaf node. switch method { case "GET": - r.name = GetDomainBidsOperation + r.name = GetAccountDefiAssetsOperation r.summary = "" - r.operationID = "getDomainBids" + r.operationID = "getAccountDefiAssets" r.operationGroup = "" - r.pathPattern = "/v2/dns/{domain_name}/bids" + r.pathPattern = "/v2/defi/{account_id}/assets" r.args = args r.count = 1 return r, true @@ -5896,9 +5917,23 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { } } - case 'r': // Prefix: "resolve" + } + + case 'n': // Prefix: "ns/" - if l := len("resolve"); len(elem) >= l && elem[0:l] == "resolve" { + if l := len("ns/"); len(elem) >= l && elem[0:l] == "ns/" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'a': // Prefix: "auctions" + origElem := elem + if l := len("auctions"); len(elem) >= l && elem[0:l] == "auctions" { elem = elem[l:] } else { break @@ -5908,19 +5943,110 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { // Leaf node. switch method { case "GET": - r.name = DnsResolveOperation + r.name = GetAllAuctionsOperation r.summary = "" - r.operationID = "dnsResolve" + r.operationID = "getAllAuctions" r.operationGroup = "" - r.pathPattern = "/v2/dns/{domain_name}/resolve" + r.pathPattern = "/v2/dns/auctions" r.args = args - r.count = 1 + r.count = 0 return r, true default: return } } + elem = origElem + } + // Param: "domain_name" + // Match until "/" + idx := strings.IndexByte(elem, '/') + if idx < 0 { + idx = len(elem) + } + args[0] = elem[:idx] + elem = elem[idx:] + + if len(elem) == 0 { + switch method { + case "GET": + r.name = GetDnsInfoOperation + r.summary = "" + r.operationID = "getDnsInfo" + r.operationGroup = "" + r.pathPattern = "/v2/dns/{domain_name}" + r.args = args + r.count = 1 + return r, true + default: + return + } + } + switch elem[0] { + case '/': // Prefix: "/" + + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'b': // Prefix: "bids" + + if l := len("bids"); len(elem) >= l && elem[0:l] == "bids" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "GET": + r.name = GetDomainBidsOperation + r.summary = "" + r.operationID = "getDomainBids" + r.operationGroup = "" + r.pathPattern = "/v2/dns/{domain_name}/bids" + r.args = args + r.count = 1 + return r, true + default: + return + } + } + + case 'r': // Prefix: "resolve" + + if l := len("resolve"); len(elem) >= l && elem[0:l] == "resolve" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "GET": + r.name = DnsResolveOperation + r.summary = "" + r.operationID = "dnsResolve" + r.operationGroup = "" + r.pathPattern = "/v2/dns/{domain_name}/resolve" + r.args = args + r.count = 1 + return r, true + default: + return + } + } + + } + } } diff --git a/pkg/oas/oas_schemas_gen.go b/pkg/oas/oas_schemas_gen.go index 7ffa3a9e7..fd8d1e441 100644 --- a/pkg/oas/oas_schemas_gen.go +++ b/pkg/oas/oas_schemas_gen.go @@ -5169,6 +5169,241 @@ func (s *DecodedRawMessageMessage) SetDecodedBody(val jx.Raw) { s.DecodedBody = val } +// Ref: #/components/schemas/DefiAsset +type DefiAsset struct { + Type DefiAssetType `json:"type"` + Jetton OptJettonPreview `json:"jetton"` + Nft OptDefiNftPreview `json:"nft"` + Provider DefiProvider `json:"provider"` + // Amount in token units; negative for lending debt. + Amount int64 `json:"amount"` + // Current position value in nanoTON. + Position int64 `json:"position"` +} + +// GetType returns the value of Type. +func (s *DefiAsset) GetType() DefiAssetType { + return s.Type +} + +// GetJetton returns the value of Jetton. +func (s *DefiAsset) GetJetton() OptJettonPreview { + return s.Jetton +} + +// GetNft returns the value of Nft. +func (s *DefiAsset) GetNft() OptDefiNftPreview { + return s.Nft +} + +// GetProvider returns the value of Provider. +func (s *DefiAsset) GetProvider() DefiProvider { + return s.Provider +} + +// GetAmount returns the value of Amount. +func (s *DefiAsset) GetAmount() int64 { + return s.Amount +} + +// GetPosition returns the value of Position. +func (s *DefiAsset) GetPosition() int64 { + return s.Position +} + +// SetType sets the value of Type. +func (s *DefiAsset) SetType(val DefiAssetType) { + s.Type = val +} + +// SetJetton sets the value of Jetton. +func (s *DefiAsset) SetJetton(val OptJettonPreview) { + s.Jetton = val +} + +// SetNft sets the value of Nft. +func (s *DefiAsset) SetNft(val OptDefiNftPreview) { + s.Nft = val +} + +// SetProvider sets the value of Provider. +func (s *DefiAsset) SetProvider(val DefiProvider) { + s.Provider = val +} + +// SetAmount sets the value of Amount. +func (s *DefiAsset) SetAmount(val int64) { + s.Amount = val +} + +// SetPosition sets the value of Position. +func (s *DefiAsset) SetPosition(val int64) { + s.Position = val +} + +// Ref: #/components/schemas/DefiAssetType +type DefiAssetType string + +const ( + DefiAssetTypeLiquidStaking DefiAssetType = "liquid_staking" + DefiAssetTypeLiquidPool DefiAssetType = "liquid_pool" + DefiAssetTypeStaking DefiAssetType = "staking" + DefiAssetTypeLending DefiAssetType = "lending" + DefiAssetTypeFarming DefiAssetType = "farming" +) + +// AllValues returns all DefiAssetType values. +func (DefiAssetType) AllValues() []DefiAssetType { + return []DefiAssetType{ + DefiAssetTypeLiquidStaking, + DefiAssetTypeLiquidPool, + DefiAssetTypeStaking, + DefiAssetTypeLending, + DefiAssetTypeFarming, + } +} + +// MarshalText implements encoding.TextMarshaler. +func (s DefiAssetType) MarshalText() ([]byte, error) { + switch s { + case DefiAssetTypeLiquidStaking: + return []byte(s), nil + case DefiAssetTypeLiquidPool: + return []byte(s), nil + case DefiAssetTypeStaking: + return []byte(s), nil + case DefiAssetTypeLending: + return []byte(s), nil + case DefiAssetTypeFarming: + return []byte(s), nil + default: + return nil, errors.Errorf("invalid value: %q", s) + } +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (s *DefiAssetType) UnmarshalText(data []byte) error { + switch DefiAssetType(data) { + case DefiAssetTypeLiquidStaking: + *s = DefiAssetTypeLiquidStaking + return nil + case DefiAssetTypeLiquidPool: + *s = DefiAssetTypeLiquidPool + return nil + case DefiAssetTypeStaking: + *s = DefiAssetTypeStaking + return nil + case DefiAssetTypeLending: + *s = DefiAssetTypeLending + return nil + case DefiAssetTypeFarming: + *s = DefiAssetTypeFarming + return nil + default: + return errors.Errorf("invalid value: %q", data) + } +} + +// Ref: #/components/schemas/DefiAssets +type DefiAssets struct { + Assets []DefiAsset `json:"assets"` +} + +// GetAssets returns the value of Assets. +func (s *DefiAssets) GetAssets() []DefiAsset { + return s.Assets +} + +// SetAssets sets the value of Assets. +func (s *DefiAssets) SetAssets(val []DefiAsset) { + s.Assets = val +} + +// Ref: #/components/schemas/DefiNftPreview +type DefiNftPreview struct { + Address string `json:"address"` + Collection string `json:"collection"` + Token0 OptJettonPreview `json:"token0"` + Token1 OptJettonPreview `json:"token1"` +} + +// GetAddress returns the value of Address. +func (s *DefiNftPreview) GetAddress() string { + return s.Address +} + +// GetCollection returns the value of Collection. +func (s *DefiNftPreview) GetCollection() string { + return s.Collection +} + +// GetToken0 returns the value of Token0. +func (s *DefiNftPreview) GetToken0() OptJettonPreview { + return s.Token0 +} + +// GetToken1 returns the value of Token1. +func (s *DefiNftPreview) GetToken1() OptJettonPreview { + return s.Token1 +} + +// SetAddress sets the value of Address. +func (s *DefiNftPreview) SetAddress(val string) { + s.Address = val +} + +// SetCollection sets the value of Collection. +func (s *DefiNftPreview) SetCollection(val string) { + s.Collection = val +} + +// SetToken0 sets the value of Token0. +func (s *DefiNftPreview) SetToken0(val OptJettonPreview) { + s.Token0 = val +} + +// SetToken1 sets the value of Token1. +func (s *DefiNftPreview) SetToken1(val OptJettonPreview) { + s.Token1 = val +} + +// Ref: #/components/schemas/DefiProvider +type DefiProvider struct { + Name string `json:"name"` + URL string `json:"url"` + Image string `json:"image"` +} + +// GetName returns the value of Name. +func (s *DefiProvider) GetName() string { + return s.Name +} + +// GetURL returns the value of URL. +func (s *DefiProvider) GetURL() string { + return s.URL +} + +// GetImage returns the value of Image. +func (s *DefiProvider) GetImage() string { + return s.Image +} + +// SetName sets the value of Name. +func (s *DefiProvider) SetName(val string) { + s.Name = val +} + +// SetURL sets the value of URL. +func (s *DefiProvider) SetURL(val string) { + s.URL = val +} + +// SetImage sets the value of Image. +func (s *DefiProvider) SetImage(val string) { + s.Image = val +} + // Validator's participation in elections. // Ref: #/components/schemas/DepositStakeAction type DepositStakeAction struct { @@ -13474,6 +13709,52 @@ func (o OptDecodedMessageExtInMsgDecodedWalletV5) Or(d DecodedMessageExtInMsgDec return d } +// NewOptDefiNftPreview returns new OptDefiNftPreview with value set to v. +func NewOptDefiNftPreview(v DefiNftPreview) OptDefiNftPreview { + return OptDefiNftPreview{ + Value: v, + Set: true, + } +} + +// OptDefiNftPreview is optional DefiNftPreview. +type OptDefiNftPreview struct { + Value DefiNftPreview + Set bool +} + +// IsSet returns true if OptDefiNftPreview was set. +func (o OptDefiNftPreview) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptDefiNftPreview) Reset() { + var v DefiNftPreview + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptDefiNftPreview) SetTo(v DefiNftPreview) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptDefiNftPreview) Get() (v DefiNftPreview, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptDefiNftPreview) Or(d DefiNftPreview) DefiNftPreview { + if v, ok := o.Get(); ok { + return v + } + return d +} + // NewOptDepositStakeAction returns new OptDepositStakeAction with value set to v. func NewOptDepositStakeAction(v DepositStakeAction) OptDepositStakeAction { return OptDepositStakeAction{ diff --git a/pkg/oas/oas_server_gen.go b/pkg/oas/oas_server_gen.go index e10d00154..9de5a6596 100644 --- a/pkg/oas/oas_server_gen.go +++ b/pkg/oas/oas_server_gen.go @@ -107,6 +107,12 @@ type Handler interface { // // GET /v2/accounts/{account_id} GetAccount(ctx context.Context, params GetAccountParams) (*Account, error) + // GetAccountDefiAssets implements getAccountDefiAssets operation. + // + // Get all DeFi assets (liquid staking, liquid pools, staking, lending, farming) for an account. + // + // GET /v2/defi/{account_id}/assets + GetAccountDefiAssets(ctx context.Context, params GetAccountDefiAssetsParams) (*DefiAssets, error) // GetAccountDiff implements getAccountDiff operation. // // Get account's balance change. diff --git a/pkg/oas/oas_unimplemented_gen.go b/pkg/oas/oas_unimplemented_gen.go index 7a23d3b43..87ead7e2c 100644 --- a/pkg/oas/oas_unimplemented_gen.go +++ b/pkg/oas/oas_unimplemented_gen.go @@ -159,6 +159,15 @@ func (UnimplementedHandler) GetAccount(ctx context.Context, params GetAccountPar return r, ht.ErrNotImplemented } +// GetAccountDefiAssets implements getAccountDefiAssets operation. +// +// Get all DeFi assets (liquid staking, liquid pools, staking, lending, farming) for an account. +// +// GET /v2/defi/{account_id}/assets +func (UnimplementedHandler) GetAccountDefiAssets(ctx context.Context, params GetAccountDefiAssetsParams) (r *DefiAssets, _ error) { + return r, ht.ErrNotImplemented +} + // GetAccountDiff implements getAccountDiff operation. // // Get account's balance change. diff --git a/pkg/oas/oas_validators_gen.go b/pkg/oas/oas_validators_gen.go index 9a3b13e4a..6386cd04a 100644 --- a/pkg/oas/oas_validators_gen.go +++ b/pkg/oas/oas_validators_gen.go @@ -2361,6 +2361,170 @@ func (s *DecodedMessageExtInMsgDecodedWalletV5) Validate() error { return nil } +func (s *DefiAsset) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Type.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "type", + Error: err, + }) + } + if err := func() error { + if value, ok := s.Jetton.Get(); ok { + if err := func() error { + if err := value.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "jetton", + Error: err, + }) + } + if err := func() error { + if value, ok := s.Nft.Get(); ok { + if err := func() error { + if err := value.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "nft", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s DefiAssetType) Validate() error { + switch s { + case "liquid_staking": + return nil + case "liquid_pool": + return nil + case "staking": + return nil + case "lending": + return nil + case "farming": + return nil + default: + return errors.Errorf("invalid value: %v", s) + } +} + +func (s *DefiAssets) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if s.Assets == nil { + return errors.New("nil is invalid value") + } + var failures []validate.FieldError + for i, elem := range s.Assets { + if err := func() error { + if err := elem.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: fmt.Sprintf("[%d]", i), + Error: err, + }) + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "assets", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *DefiNftPreview) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if value, ok := s.Token0.Get(); ok { + if err := func() error { + if err := value.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "token0", + Error: err, + }) + } + if err := func() error { + if value, ok := s.Token1.Get(); ok { + if err := func() error { + if err := value.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "token1", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + func (s *DepositStakeAction) Validate() error { if s == nil { return validate.ErrNilPointer diff --git a/pkg/references/defi.go b/pkg/references/defi.go new file mode 100644 index 000000000..dddfb3072 --- /dev/null +++ b/pkg/references/defi.go @@ -0,0 +1,144 @@ +package references + +import "github.com/tonkeeper/tongo/ton" + +type DefiProviderMeta struct { + Name string + URL string + Image string +} + +type LiquidStakingJetton struct { + DefiProviderMeta + JettonMaster ton.AccountID +} + +var LiquidStakingJettons = []LiquidStakingJetton{ + { + DefiProviderMeta: DefiProviderMeta{ + Name: "Tonstakers", + URL: "https://app.tonstakers.com/", + Image: "https://cache.tonapi.io/imgproxy/qkMSC0tZBAgHZkpLfCvhQJMXnHVpFKjhGM4eMD7HWWQ/rs:fill:200:200:1/g:no/aHR0cHM6Ly90b25zdGFrZXJzLmNvbS9sb2dvLnBuZw.webp", + }, + JettonMaster: ton.MustParseAccountID("0:bdf3fa8098d129b54b4f73b5bac5d1e1fd91eb054169c3916dfc8ccd536d1000"), + }, + { + DefiProviderMeta: DefiProviderMeta{ + Name: "Stakee", + URL: "https://app.stakee.org/", + Image: "https://app.stakee.org/favicon.ico", + }, + JettonMaster: ton.MustParseAccountID("0:aa0ba121449feda569e02b12fa755d24e834a7454aecf4649590b6df742aac8f"), + }, + { + DefiProviderMeta: DefiProviderMeta{ + Name: "Hipo", + URL: "https://app.hipo.finance/", + Image: "https://app.hipo.finance/favicon.ico", + }, + JettonMaster: ton.MustParseAccountID("0:cf76af318c0872b58a9f1925fc29c156211782b9fb01f56760d292e56123bf87"), + }, + { + DefiProviderMeta: DefiProviderMeta{ + Name: "Bemo", + URL: "https://bemo.finance/", + Image: "https://bemo.finance/favicon.ico", + }, + JettonMaster: ton.MustParseAccountID("0:92c4664f1ea6b74ed9ce0e031a9fc0843348dfe87a58faea27fcd31e1608caaa"), + }, + { + DefiProviderMeta: DefiProviderMeta{ + Name: "Storm", + URL: "https://storm.tg/", + Image: "https://storm.tg/favicon.ico", + }, + JettonMaster: ton.MustParseAccountID("0:6ca2c99c66b0fa1478a303ba9618bc39c28fda1fc50de37e618bddf98c9fd24c"), + }, + { + DefiProviderMeta: DefiProviderMeta{ + Name: "UTonics", + URL: "https://utonic.org/home", + Image: "https://utonic.org/favicon.ico", + }, + JettonMaster: ton.MustParseAccountID("0:1f1798f724c2296652e6002bfb51bed11fb5a689532e5788af7203581ef367a8"), + }, + { + DefiProviderMeta: JVaultProvider, + JettonMaster: JVaultJettonMaster, + }, +} + +var LiquidStakingJettonsByMaster = func() map[ton.AccountID]LiquidStakingJetton { + m := make(map[ton.AccountID]LiquidStakingJetton, len(LiquidStakingJettons)) + for _, p := range LiquidStakingJettons { + m[p.JettonMaster] = p + } + return m +}() + +var ( + DedustProvider = DefiProviderMeta{ + Name: "DeDust", + URL: "https://dedust.io/", + Image: "https://dedust.io/favicon.ico", + } + + StonfiProvider = DefiProviderMeta{ + Name: "StonFi", + URL: "https://app.ston.fi/", + Image: StonfiImage, + } + + ToncoProvider = DefiProviderMeta{ + Name: "Tonco", + URL: "https://app.tonco.io/", + Image: ToncoImage, + } + + WhalesProvider = DefiProviderMeta{ + Name: "TON Whales", + URL: WhalesPoolImplementationsURL, + Image: "https://tonwhales.com/favicon.ico", + } + + TFProvider = DefiProviderMeta{ + Name: "TON Nominators", + URL: TFPoolImplementationsURL, + Image: "https://tonvalidators.org/favicon.ico", + } + + JVaultProvider = DefiProviderMeta{ + Name: "JVault", + URL: "https://jvault.xyz/", + Image: "https://jvault.xyz/favicon.ico", + } + + SwapCoffeeProvider = DefiProviderMeta{ + Name: "Swap.Coffee", + URL: "https://swap.coffee/", + Image: "https://swap.coffee/favicon.ico", + } + + EvaaProvider = DefiProviderMeta{ + Name: "Evaa", + URL: "https://evaa.finance/", + Image: "https://evaa.finance/favicon.ico", + } + + AffluentProvider = DefiProviderMeta{ + Name: "Affluent", + URL: "https://affluent.finance/", + Image: AffluentImage, + } + + BidaskProvider = DefiProviderMeta{ + Name: "BidAsk", + URL: "https://bidask.io/", + Image: "https://bidask.io/favicon.ico", + } +) + +var EvaaMasterAddress = ton.MustParseAccountID("EQC8rUZqR_pWV1BylWUlPNBzyiTYVoBEmQkMIQDZXICfnuRr") +var JVaultJettonMaster = ton.MustParseAccountID("EQC8FoZMlBcZhZ6Pr9sHGyHzkFv9y2B5X9tN61RvucLRzFZz") +var ToncoNFTCollection = ton.MustParseAccountID("0:bffadd270a738531da7b13ba8fc403826c2586173f9ede9c316fab53bc59ac86") +var StonJettonMaster = ton.MustParseAccountID("EQA2kCVNwVsil2EM2mB0SkXytxCqQjS4mttjDpnXmwG9T6bO") diff --git a/pkg/references/whitelist.go b/pkg/references/whitelist.go index 0b9d20e45..e327c7be3 100644 --- a/pkg/references/whitelist.go +++ b/pkg/references/whitelist.go @@ -7,51 +7,53 @@ import ( ) var StonfiWhitelistVaults = map[tongo.AccountID]struct{}{ - tongo.MustParseAddress("EQBQ_UBQvR9ryUjKDwijtoiyyga2Wl-yJm6Y8gl0k-HDh_5x").ID: {}, - tongo.MustParseAddress("EQBCtlN7Zy96qx-3yH0Yi4V0SNtQ-8RbhYaNs65MC4Hwfq31").ID: {}, - tongo.MustParseAddress("EQDBYUj5KEPUQrbj7da742UYJIeT9QU5C2dKsi12SdQ3yh9a").ID: {}, - tongo.MustParseAddress("EQDwyjgjnTXJVPjXji3OPtUilcCjceGVQOLGwr9_sRLjImfG").ID: {}, - tongo.MustParseAddress("EQAJG5pyZPWEiQiMVJdf7bDRgRLzg6QR57qKeRsOrMO-ncZN").ID: {}, - tongo.MustParseAddress("EQBzkqAN4ViYdS24lD2fFPe8odHn2rUkfMYbEJ88EBKBAS1b").ID: {}, - tongo.MustParseAddress("EQAiv3IuxYA6ZGEunOgZSTuMBzbpjwRbWw09-WsE-iqKKMrK").ID: {}, - tongo.MustParseAddress("EQDi1eWU3HWWst8owY8OMq2Dz9nJJEHUROza8R-_wEGb8yu6").ID: {}, - tongo.MustParseAddress("EQBCl1JANkTpMpJ9N3lZktPMpp2btRe2vVwHon0la8ibRied").ID: {}, - tongo.MustParseAddress("EQDTb1w1TCohFqnNcyPrrbbBJQdAwwPn8DbCoaSUd0S5T4fB").ID: {}, - tongo.MustParseAddress("EQBSNX_5mSikBVttWhIaIb0f8jJU7fL6kvyyFVppd7dWRO6M").ID: {}, - tongo.MustParseAddress("EQABT9GCyDI60CbC4c6uS33HFDwaqd6MddiwIIw7CXTgNR3A").ID: {}, - tongo.MustParseAddress("EQADEFMTMnC-gu5v2U0ZY8AYaGhAOk9TcECg1TOquAW3r-IE").ID: {}, - tongo.MustParseAddress("EQB3ncyBUTjZUA5EnFKR5_EnOMI9V1tTEAAPaiU71gc4TiUt").ID: {}, + tongo.MustParseAddress("EQCxkYVQcfXKw9uJ-MMtutvR2Cu0DVCZFfLNBp6NwXgO8vQY").ID: {}, + tongo.MustParseAddress("EQACn16m9OrZ-mw186M4NlIpVP8Tb3q6SV9aX8NjSgVfJTo9").ID: {}, tongo.MustParseAddress("EQCRgwuFbPRR7TGodkJwbjiBtNtb0hfzJIliV-5kY6lKr_18").ID: {}, - tongo.MustParseAddress("EQDAPye7HAPAAl4WXpz5jOCdhf2H9h9QkkzRQ-6K5usiuQeC").ID: {}, + tongo.MustParseAddress("EQBigMnbY4NU1uwdvzertV5mv_yI7282R-ffW7XZFWPEVRDG").ID: {}, + tongo.MustParseAddress("EQBQErJi0DHgKYseIHtrQk4N5CQLCr3XYwkQIEw0HNs470OG").ID: {}, + tongo.MustParseAddress("EQADEFMTMnC-gu5v2U0ZY8AYaGhAOk9TcECg1TOquAW3r-IE").ID: {}, + tongo.MustParseAddress("EQD11suHkrO_1Mb5IIdYFx5ZPy38MuHoeHx6dA-QRaD8w0UJ").ID: {}, + tongo.MustParseAddress("EQAgERF5tvrNn0AM2Rrrvk-MutGP60ZL70bJPuqvCTGY-17_").ID: {}, + tongo.MustParseAddress("EQCCdNmj4QbNjrg_PM-JJE-B9f_czXLkYmrO7P9UkA6tt95m").ID: {}, tongo.MustParseAddress("EQDgebEMA6yriI7SMffE65DIVA9rzSRmfGV_gy3ylIhLicY8").ID: {}, - tongo.MustParseAddress("EQCx0HDJ_DxLxDSQyfsEqHI8Rs65nygvdmeD9Ra7rY15OWN8").ID: {}, + tongo.MustParseAddress("EQBwpBGEAb-NgjUxpmARAgVl8C4F_5GsXxZ3dpsA1qzQerNl").ID: {}, tongo.MustParseAddress("EQAiLV677BgHNXEUuDJ3Cw8K5WOiJSO86xh8YQq2LthJEoED").ID: {}, + tongo.MustParseAddress("EQDkncuJ267Py3EmL2XAN7YsSNQMUu8u-GHsW9jVljcH8fr5").ID: {}, + tongo.MustParseAddress("EQBZj7nhXNhB4O9rRCn4qGS82DZaPUPlyM2k6ZrbvQ1j3Ge7").ID: {}, + tongo.MustParseAddress("EQDTb1w1TCohFqnNcyPrrbbBJQdAwwPn8DbCoaSUd0S5T4fB").ID: {}, + tongo.MustParseAddress("EQATvO_BXfkFocOXhlve01EZfsiyFjoV-0k9CLmpgwtzVtcN").ID: {}, + tongo.MustParseAddress("EQCpuYtq55nhkwYDmL4OWjsrdYy83gj5_49nNRQ5CrPOze49").ID: {}, + tongo.MustParseAddress("EQAyD7O8CvVdR8AEJcr96fHI1ifFq21S8QMt1czi5IfJPyfA").ID: {}, + tongo.MustParseAddress("EQB3ncyBUTjZUA5EnFKR5_EnOMI9V1tTEAAPaiU71gc4TiUt").ID: {}, + tongo.MustParseAddress("EQAGV9vw11tKW2QOCYCXEmIdyufM3p5CfcgHcY9NiiBLfZGH").ID: {}, tongo.MustParseAddress("EQChoROpuUM4cpN6IRzqNTrkP9iVZHYoHgxMABDVU28vlUiG").ID: {}, - tongo.MustParseAddress("EQAgERF5tvrNn0AM2Rrrvk-MutGP60ZL70bJPuqvCTGY-17_").ID: {}, + tongo.MustParseAddress("EQDBYUj5KEPUQrbj7da742UYJIeT9QU5C2dKsi12SdQ3yh9a").ID: {}, + tongo.MustParseAddress("EQCiypoBWNIEPlarBp04UePyEj5zH0ZDHxuRNqJ1WQx3FCY-").ID: {}, + tongo.MustParseAddress("EQDAPye7HAPAAl4WXpz5jOCdhf2H9h9QkkzRQ-6K5usiuQeC").ID: {}, + tongo.MustParseAddress("EQBzkqAN4ViYdS24lD2fFPe8odHn2rUkfMYbEJ88EBKBAS1b").ID: {}, + tongo.MustParseAddress("EQCx0HDJ_DxLxDSQyfsEqHI8Rs65nygvdmeD9Ra7rY15OWN8").ID: {}, + tongo.MustParseAddress("EQBQ_UBQvR9ryUjKDwijtoiyyga2Wl-yJm6Y8gl0k-HDh_5x").ID: {}, + tongo.MustParseAddress("EQAiv3IuxYA6ZGEunOgZSTuMBzbpjwRbWw09-WsE-iqKKMrK").ID: {}, tongo.MustParseAddress("EQDh5oHPvfRwPu2bORBGCoLEO4WQZKL4fk5DD1gydeNG9oEH").ID: {}, - tongo.MustParseAddress("EQBigMnbY4NU1uwdvzertV5mv_yI7282R-ffW7XZFWPEVRDG").ID: {}, + tongo.MustParseAddress("EQBSNX_5mSikBVttWhIaIb0f8jJU7fL6kvyyFVppd7dWRO6M").ID: {}, + tongo.MustParseAddress("EQDi1eWU3HWWst8owY8OMq2Dz9nJJEHUROza8R-_wEGb8yu6").ID: {}, + tongo.MustParseAddress("EQBjM7B2PKa82IPKrUFbMFaKeQDFGTMRnrvY1TmptC7Kxz7B").ID: {}, tongo.MustParseAddress("EQBjK_kjY5R_DoyTRff109VzFrSlKFCC_gOOWIMtyEvCcv2J").ID: {}, - tongo.MustParseAddress("EQDx--jUU9PUtHltPYZX7wdzIi0SPY3KZ8nvOs0iZvQJd6Ql").ID: {}, + tongo.MustParseAddress("EQAz1D0ZUiG_9XCyjrJ1-xTx-CnmnQ3J3LMKQ7sZTr-XlNZP").ID: {}, + tongo.MustParseAddress("EQBqgCTdrtSod76UrcOeALSiLCp3WuNIFQBQvyjjlQMvwLkc").ID: {}, tongo.MustParseAddress("EQAyY2lBQ6RsVe88CKTmeH3BWWsUCWu7ugQNaf5kwLDYAoKt").ID: {}, - tongo.MustParseAddress("EQBZj7nhXNhB4O9rRCn4qGS82DZaPUPlyM2k6ZrbvQ1j3Ge7").ID: {}, - tongo.MustParseAddress("EQAyD7O8CvVdR8AEJcr96fHI1ifFq21S8QMt1czi5IfJPyfA").ID: {}, - tongo.MustParseAddress("EQDQ6j53q21HuZtw6oclm7z4LU2cG6S2OKvpSSMH548d7kJT").ID: {}, + tongo.MustParseAddress("EQDx--jUU9PUtHltPYZX7wdzIi0SPY3KZ8nvOs0iZvQJd6Ql").ID: {}, + tongo.MustParseAddress("EQABT9GCyDI60CbC4c6uS33HFDwaqd6MddiwIIw7CXTgNR3A").ID: {}, + tongo.MustParseAddress("EQCDT9dCT52pdfsLNW0e6qP5T3cgq7M4Ug72zkGYgP17tsWD").ID: {}, + tongo.MustParseAddress("EQDwyjgjnTXJVPjXji3OPtUilcCjceGVQOLGwr9_sRLjImfG").ID: {}, + tongo.MustParseAddress("EQAJG5pyZPWEiQiMVJdf7bDRgRLzg6QR57qKeRsOrMO-ncZN").ID: {}, + tongo.MustParseAddress("EQBCl1JANkTpMpJ9N3lZktPMpp2btRe2vVwHon0la8ibRied").ID: {}, tongo.MustParseAddress("EQCS4UEa5UaJLzOyyKieqQOQ2P9M-7kXpkO5HnP3Bv250cN3").ID: {}, - tongo.MustParseAddress("EQBwpBGEAb-NgjUxpmARAgVl8C4F_5GsXxZ3dpsA1qzQerNl").ID: {}, - tongo.MustParseAddress("EQBqgCTdrtSod76UrcOeALSiLCp3WuNIFQBQvyjjlQMvwLkc").ID: {}, + tongo.MustParseAddress("EQAQYbnb1EGK0Wb8mk3vEW4vbHTyv7cOcfJlPWQ87_6_qfzR").ID: {}, + tongo.MustParseAddress("EQDQ6j53q21HuZtw6oclm7z4LU2cG6S2OKvpSSMH548d7kJT").ID: {}, + tongo.MustParseAddress("EQC67o2-2UzR1cJFrUGL5M7OAnLgG8oY_tHaTgGmR63LQNV-").ID: {}, tongo.MustParseAddress("EQCiz74FCV2lYlvFPEYhL3Jql8WwIO7QvbvYT-LQH0SmtCgI").ID: {}, - tongo.MustParseAddress("EQBjM7B2PKa82IPKrUFbMFaKeQDFGTMRnrvY1TmptC7Kxz7B").ID: {}, - tongo.MustParseAddress("EQATvO_BXfkFocOXhlve01EZfsiyFjoV-0k9CLmpgwtzVtcN").ID: {}, - tongo.MustParseAddress("EQCiypoBWNIEPlarBp04UePyEj5zH0ZDHxuRNqJ1WQx3FCY-").ID: {}, - tongo.MustParseAddress("EQACn16m9OrZ-mw186M4NlIpVP8Tb3q6SV9aX8NjSgVfJTo9").ID: {}, - tongo.MustParseAddress("EQAz1D0ZUiG_9XCyjrJ1-xTx-CnmnQ3J3LMKQ7sZTr-XlNZP").ID: {}, - tongo.MustParseAddress("EQAGV9vw11tKW2QOCYCXEmIdyufM3p5CfcgHcY9NiiBLfZGH").ID: {}, - tongo.MustParseAddress("EQCDT9dCT52pdfsLNW0e6qP5T3cgq7M4Ug72zkGYgP17tsWD").ID: {}, - tongo.MustParseAddress("EQBQErJi0DHgKYseIHtrQk4N5CQLCr3XYwkQIEw0HNs470OG").ID: {}, tongo.MustParseAddress("EQByADL5Ra2dldrMSBctgfSm2X2W1P61NVW2RYDb8eJNJGx6").ID: {}, - tongo.MustParseAddress("EQCxkYVQcfXKw9uJ-MMtutvR2Cu0DVCZFfLNBp6NwXgO8vQY").ID: {}, - tongo.MustParseAddress("EQAQYbnb1EGK0Wb8mk3vEW4vbHTyv7cOcfJlPWQ87_6_qfzR").ID: {}, - tongo.MustParseAddress("EQCpuYtq55nhkwYDmL4OWjsrdYy83gj5_49nNRQ5CrPOze49").ID: {}, - tongo.MustParseAddress("EQCCdNmj4QbNjrg_PM-JJE-B9f_czXLkYmrO7P9UkA6tt95m").ID: {}, - tongo.MustParseAddress("EQDkncuJ267Py3EmL2XAN7YsSNQMUu8u-GHsW9jVljcH8fr5").ID: {}, + tongo.MustParseAddress("EQBCtlN7Zy96qx-3yH0Yi4V0SNtQ-8RbhYaNs65MC4Hwfq31").ID: {}, }