diff --git a/api/openapi.json b/api/openapi.json index 1f668fab..c2318732 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -4991,6 +4991,134 @@ ], "type": "object" }, + "MigrationPrepareRequest": { + "properties": { + "currency": { + "default": "USD", + "description": "fiat currency for the preview values", + "example": "USD", + "type": "string" + }, + "from": { + "description": "legacy source wallet to drain", + "format": "address", + "type": "string" + }, + "to": { + "description": "destination wallet TON address", + "format": "address", + "type": "string" + } + }, + "required": [ + "from", + "to" + ], + "type": "object" + }, + "MigrationPrepareResponse": { + "properties": { + "from": { + "format": "address", + "type": "string" + }, + "to": { + "format": "address", + "type": "string" + }, + "transactions": { + "description": "ordered; sign and broadcast in array order, one entry per external message", + "items": { + "$ref": "#/components/schemas/MigrationTransaction" + }, + "type": "array" + }, + "wallet_version": { + "description": "source wallet version (informational)", + "example": "v4R2", + "type": "string" + } + }, + "required": [ + "from", + "to", + "wallet_version", + "transactions" + ], + "type": "object" + }, + "MigrationTransaction": { + "properties": { + "boc": { + "description": "base64 BOC of the unsigned wallet body. Sign its hash, prepend the signature, wrap it in an external message and broadcast.\n", + "type": "string" + }, + "emulation": { + "$ref": "#/components/schemas/MessageConsequences" + }, + "seqno": { + "description": "wallet seqno baked into the unsigned body", + "format": "int32", + "type": "integer" + } + }, + "required": [ + "seqno", + "boc", + "emulation" + ], + "type": "object" + }, + "MigrationWalletValue": { + "properties": { + "account": { + "example": "0:97264395BD65A255A429B11326C84128B7D70FFED7949ABAE3036D506BA38621", + "format": "address", + "type": "string" + }, + "balance": { + "description": "TON balance in nanotons", + "format": "int64", + "type": "integer" + }, + "jettons": { + "items": { + "$ref": "#/components/schemas/JettonBalance" + }, + "type": "array" + }, + "nft_count": { + "description": "number of NFTs owned by the account", + "format": "int32", + "type": "integer" + }, + "status": { + "$ref": "#/components/schemas/AccountStatus" + } + }, + "required": [ + "account", + "balance", + "status", + "jettons", + "nft_count" + ], + "type": "object" + }, + "MigrationWallets": { + "properties": { + "wallets": { + "items": { + "$ref": "#/components/schemas/MigrationWalletValue" + }, + "type": "array" + } + }, + "required": [ + "wallets" + ], + "type": "object" + }, "MisbehaviourPunishmentConfig": { "properties": { "default_flat_fine": { @@ -11561,6 +11689,72 @@ ] } }, + "/v2/migration/prepare": { + "post": { + "description": "Prepare ordered signable transactions that migrate every asset from `from` to `to`.", + "operationId": "prepareMigration", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MigrationPrepareRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MigrationPrepareResponse" + } + } + }, + "description": "ordered transactions to sign and broadcast" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "tags": [ + "Migration" + ] + } + }, + "/v2/migration/wallets": { + "post": { + "description": "Get migratable assets value (TON balance, jettons with prices, NFT count) for several wallets at once.", + "operationId": "getMigrationWallets", + "parameters": [ + { + "$ref": "#/components/parameters/currenciesQuery" + } + ], + "requestBody": { + "$ref": "#/components/requestBodies/AccountIDs" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MigrationWallets" + } + } + }, + "description": "migratable value per requested wallet" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "tags": [ + "Migration" + ] + } + }, "/v2/multisig/order/{account_id}": { "get": { "description": "Get multisig order", @@ -13020,6 +13214,9 @@ }, "name": "Accounts" }, + { + "name": "Migration" + }, { "externalDocs": { "description": "Additional documentation", diff --git a/api/openapi.yml b/api/openapi.yml index f90a4d40..e740998f 100644 --- a/api/openapi.yml +++ b/api/openapi.yml @@ -16,6 +16,7 @@ tags: externalDocs: description: Additional documentation url: https://docs.tonconsole.com/tonapi/rest-api/accounts + - name: Migration - name: NFT externalDocs: description: Additional documentation @@ -632,6 +633,46 @@ paths: type: boolean default: $ref: '#/components/responses/Error' + /v2/migration/wallets: + post: + description: Get migratable assets value (TON balance, jettons with prices, NFT count) for several wallets at once. + operationId: getMigrationWallets + tags: + - Migration + parameters: + - $ref: '#/components/parameters/currenciesQuery' + requestBody: + $ref: "#/components/requestBodies/AccountIDs" + responses: + '200': + description: migratable value per requested wallet + content: + application/json: + schema: + $ref: '#/components/schemas/MigrationWallets' + 'default': + $ref: '#/components/responses/Error' + /v2/migration/prepare: + post: + description: Prepare ordered signable transactions that migrate every asset from `from` to `to`. + operationId: prepareMigration + tags: + - Migration + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/MigrationPrepareRequest' + responses: + '200': + description: ordered transactions to sign and broadcast + content: + application/json: + schema: + $ref: '#/components/schemas/MigrationPrepareResponse' + 'default': + $ref: '#/components/responses/Error' /v2/accounts/_bulk: post: description: Get human-friendly information about several accounts without low-level details. @@ -6145,7 +6186,103 @@ components: balances: type: array items: - $ref: '#/components/schemas/JettonBalance' + $ref: "#/components/schemas/JettonBalance" + MigrationWallets: + type: object + required: + - wallets + properties: + wallets: + type: array + items: + $ref: "#/components/schemas/MigrationWalletValue" + MigrationWalletValue: + type: object + required: + - account + - balance + - status + - jettons + - nft_count + properties: + account: + type: string + format: address + example: 0:97264395BD65A255A429B11326C84128B7D70FFED7949ABAE3036D506BA38621 + balance: + type: integer + format: int64 + description: TON balance in nanotons + status: + $ref: "#/components/schemas/AccountStatus" + jettons: + type: array + items: + $ref: "#/components/schemas/JettonBalance" + nft_count: + type: integer + format: int32 + description: number of NFTs owned by the account + MigrationPrepareRequest: + type: object + required: + - from + - to + properties: + from: + type: string + format: address + description: legacy source wallet to drain + to: + type: string + format: address + description: destination wallet TON address + currency: + type: string + default: USD + description: fiat currency for the preview values + example: USD + MigrationPrepareResponse: + type: object + required: + - from + - to + - wallet_version + - transactions + properties: + from: + type: string + format: address + to: + type: string + format: address + wallet_version: + type: string + description: source wallet version (informational) + example: v4R2 + transactions: + type: array + description: ordered; sign and broadcast in array order, one entry per external message + items: + $ref: "#/components/schemas/MigrationTransaction" + MigrationTransaction: + type: object + required: + - seqno + - boc + - emulation + properties: + seqno: + type: integer + format: int32 + description: wallet seqno baked into the unsigned body + boc: + type: string + description: > + base64 BOC of the unsigned wallet body. Sign its hash, prepend the signature, + wrap it in an external message and broadcast. + emulation: + $ref: "#/components/schemas/MessageConsequences" CurrencyType: type: string example: jetton diff --git a/pkg/api/migration_handlers.go b/pkg/api/migration_handlers.go new file mode 100644 index 00000000..1babad8c --- /dev/null +++ b/pkg/api/migration_handlers.go @@ -0,0 +1,523 @@ +package api + +import ( + "context" + "crypto/ed25519" + "errors" + "fmt" + "math/big" + "net/http" + "sync" + "time" + + "github.com/tonkeeper/opentonapi/pkg/bath" + "github.com/tonkeeper/opentonapi/pkg/core" + "github.com/tonkeeper/opentonapi/pkg/oas" + "github.com/tonkeeper/opentonapi/pkg/wallet" + "github.com/tonkeeper/tongo" + "github.com/tonkeeper/tongo/abi" + "github.com/tonkeeper/tongo/boc" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" + "github.com/tonkeeper/tongo/txemulator" + tonwallet "github.com/tonkeeper/tongo/wallet" + "go.uber.org/zap" +) + +// migrationGasPerTransfer is attached to every jetton/NFT transfer to cover gas and forwarding. +// Any unused part is reclaimed by the final mode-128 TON sweep, so over-estimating is safe. +const migrationGasPerTransfer = ton.OneGRAM / 20 // 0.05 TON + +// migrationForwardAmount is forwarded to the destination so it receives a transfer notification. +const migrationForwardAmount = tlb.Grams(1) + +// migrationSweepMode sends the entire remaining balance and ignores errors of the sweep itself. +const migrationSweepMode = 128 + +const migrationMsgLifetime = 5 * time.Minute + +type jettonBulkStorage interface { + GetJettonWalletsByOwnerAddresses(ctx context.Context, owners []ton.AccountID, mintless bool) ([]core.JettonWallet, error) +} + +func (h *Handler) GetMigrationWallets(ctx context.Context, req oas.OptGetMigrationWalletsReq, params oas.GetMigrationWalletsParams) (*oas.MigrationWallets, error) { + if len(req.Value.AccountIds) == 0 { + return nil, toError(http.StatusBadRequest, fmt.Errorf("empty list of ids")) + } + if !h.limits.isBulkQuantityAllowed(len(req.Value.AccountIds)) { + return nil, toError(http.StatusBadRequest, fmt.Errorf("the maximum number of accounts to request at once: %v", h.limits.BulkLimits)) + } + ids := make([]tongo.AccountID, 0, len(req.Value.AccountIds)) + for _, v := range req.Value.AccountIds { + account, err := tongo.ParseAddress(v) + if err != nil { + return nil, toError(http.StatusBadRequest, err) + } + ids = append(ids, account.ID) + } + + var accounts []*core.Account + nftCountByOwner := make(map[ton.AccountID]int32) + var jettonWallets map[ton.AccountID][]core.JettonWallet + var accountsErr, nftsErr, jettonsErr error + var wg sync.WaitGroup + wg.Go(func() { + accounts, accountsErr = h.storage.GetRawAccounts(ctx, ids) + }) + wg.Go(func() { + nfts, err := h.storage.GetNFTs(ctx, ids) + if err != nil && !errors.Is(err, core.ErrEntityNotFound) { + nftsErr = err + return + } + nftScam, scamErr := h.spamFilter.GetNftsScamData(ctx, ids) + if scamErr != nil { + h.logger.Warn("error getting nft scam data", zap.Error(scamErr)) + } + for _, item := range nfts { + if item.OwnerAddress == nil { + continue + } + if nftScam[item.Address] == core.TrustBlacklist { + continue + } + nftCountByOwner[*item.OwnerAddress]++ + } + }) + wg.Go(func() { + jettonWallets, jettonsErr = h.collectJettonWallets(ctx, ids) + }) + wg.Wait() + if accountsErr != nil { + return nil, toError(http.StatusInternalServerError, accountsErr) + } + if nftsErr != nil { + return nil, toError(http.StatusInternalServerError, nftsErr) + } + if jettonsErr != nil { + return nil, toError(http.StatusInternalServerError, jettonsErr) + } + + accountByID := make(map[ton.AccountID]*core.Account, len(accounts)) + for _, account := range accounts { + accountByID[account.AccountAddress] = account + } + jettonsByOwner := make(map[ton.AccountID][]oas.JettonBalance, len(jettonWallets)) + for owner, wallets := range jettonWallets { + balances := make([]oas.JettonBalance, 0, len(wallets)) + for _, w := range wallets { + if w.Lock != nil { + // locked jettons cannot be migrated + continue + } + balance, err := h.convertJettonBalance(ctx, w, params.Currencies, nil, nil) + if err != nil { + h.logger.Warn(fmt.Sprintf("failed to convert jetton balance for wallet %v", w.JettonAddress.ToRaw()), zap.Error(err)) + continue + } + if balance.Jetton.Verification == oas.JettonVerificationTypeBlacklist { + // skip scam jettons + continue + } + balances = append(balances, balance) + } + jettonsByOwner[owner] = balances + } + resp := &oas.MigrationWallets{Wallets: make([]oas.MigrationWalletValue, 0, len(ids))} + for _, id := range ids { + wallet := oas.MigrationWalletValue{ + Account: id.ToRaw(), + Status: oas.AccountStatusNonexist, + Jettons: jettonsByOwner[id], + NftCount: nftCountByOwner[id], + } + if wallet.Jettons == nil { + wallet.Jettons = []oas.JettonBalance{} + } + if account, ok := accountByID[id]; ok { + wallet.Balance = account.GramBalance + wallet.Status = oas.AccountStatus(account.Status) + } + resp.Wallets = append(resp.Wallets, wallet) + } + return resp, nil +} + +func (h *Handler) PrepareMigration(ctx context.Context, req *oas.MigrationPrepareRequest) (*oas.MigrationPrepareResponse, error) { + from, err := tongo.ParseAddress(req.From) + if err != nil { + return nil, toError(http.StatusBadRequest, fmt.Errorf("invalid `from` address: %w", err)) + } + to, err := tongo.ParseAddress(req.To) + if err != nil { + return nil, toError(http.StatusBadRequest, fmt.Errorf("invalid `to` address: %w", err)) + } + currency := "USD" + if req.Currency.IsSet() && req.Currency.Value != "" { + currency = req.Currency.Value + } + account, err := h.storage.GetRawAccount(ctx, from.ID) + if errors.Is(err, core.ErrEntityNotFound) { + return nil, toError(http.StatusBadRequest, fmt.Errorf("source wallet not found")) + } + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + if len(account.Code) == 0 { + return nil, toError(http.StatusBadRequest, fmt.Errorf("source wallet is not initialized")) + } + version, err := wallet.GetVersionByCode(account.Code) + if err != nil { + return nil, toError(http.StatusBadRequest, fmt.Errorf("unsupported source wallet: %w", err)) + } + startSeqno, subWalletID, publicKey, err := parseWalletData(version, account.Data) + if err != nil { + return nil, toError(http.StatusInternalServerError, fmt.Errorf("can't read wallet data: %w", err)) + } + // Discover the safe, migratable assets of the source wallet. + jettons, err := h.collectJettonWallets(ctx, []ton.AccountID{from.ID}) + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + nfts, err := h.storage.GetNFTs(ctx, []ton.AccountID{from.ID}) + if err != nil && !errors.Is(err, core.ErrEntityNotFound) { + return nil, toError(http.StatusInternalServerError, err) + } + nftScam, scamErr := h.spamFilter.GetNftsScamData(ctx, []ton.AccountID{from.ID}) + if scamErr != nil { + h.logger.Warn("error getting nft scam data", zap.Error(scamErr)) + } + // Build the ordered internal messages: jettons, then NFTs, then the final TON sweep. + var messages []tonwallet.RawMessage + var gas int64 + for _, jetton := range jettons[from.ID] { + if jetton.Lock != nil || jetton.Balance.IsZero() { + continue + } + balance, err := h.convertJettonBalance(ctx, jetton, nil, nil, nil) + if err != nil { + h.logger.Warn(fmt.Sprintf("skip jetton %v: %v", jetton.JettonAddress.ToRaw(), err)) + continue + } + if balance.Jetton.Verification == oas.JettonVerificationTypeBlacklist { + continue + } + msg, err := walletJettonTransferMessage(jetton.Address, to.ID, jetton.Balance.BigInt()) + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + msgRaw, err := toWalletRawMessage(msg) + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + messages = append(messages, msgRaw) + gas += int64(migrationGasPerTransfer) + } + for _, nft := range nfts { + if nft.OwnerAddress == nil || *nft.OwnerAddress != from.ID { + continue + } + if nftScam[nft.Address] == core.TrustBlacklist { + continue + } + msg := walletNFTTransferMessage(nft.Address, to.ID) + msgRaw, err := toWalletRawMessage(msg) + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + messages = append(messages, msgRaw) + gas += int64(migrationGasPerTransfer) + } + if account.GramBalance < gas { + return nil, toError(http.StatusBadRequest, fmt.Errorf("INSUFFICIENT_TON_FOR_GAS: required %d nanotons to cover transfer gas, available %d", gas, account.GramBalance)) + } + // The final message sweeps the remaining TON balance to the destination. + msgRaw, err := toWalletRawMessage(tonwallet.Message{ + Amount: 0, + Address: to.ID, + Bounce: false, + Mode: migrationSweepMode, + }) + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + messages = append(messages, msgRaw) + batches := chunkMessages(messages, walletMaxMessageCount(version)) + validUntil := time.Now().Add(migrationMsgLifetime) + currencyPtr := ¤cy + resp := &oas.MigrationPrepareResponse{ + From: from.ID.ToRaw(), + To: to.ID.ToRaw(), + WalletVersion: version.ToString(), + Transactions: make([]oas.MigrationTransaction, 0, len(batches)), + } + // Emulate transactions sequentially, feeding each transaction's resulting state into the next so + // that seqno, balance and emptied jetton wallets are reflected in later fees and previews. + var overrides map[ton.AccountID]tlb.ShardAccount + for i, batch := range batches { + seqno := startSeqno + uint32(i) + body, err := buildUnsignedBody(version, subWalletID, publicKey, int(from.ID.Workchain), seqno, validUntil, batch) + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + bocBase64, err := body.ToBocBase64() + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + signedBody, err := signedBodyForEmulation(version, body) + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + extMsg, err := tongo.CreateExternalMessage(from.ID, signedBody, nil, tlb.VarUInteger16{}) + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + extMsgCell := boc.NewCell() + if err := tlb.Marshal(extMsgCell, extMsg); err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + risk, err := wallet.ExtractRisk(version, extMsgCell) + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + trace, finalStates, err := h.emulateWalletMessage(ctx, extMsg, overrides) + if err != nil { + return nil, toProperEmulationError(err) + } + convertedTrace := h.convertTrace(trace, h.addressBook) + actions, err := bath.FindActions(ctx, trace, bath.ForAccount(from.ID), bath.WithInformationSource(h.storage), bath.WithAddressBook(h.addressBook)) + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + event, err := h.toAccountEvent(ctx, from.ID, trace, bath.EnrichWithIntentions(trace, actions), oas.OptString{}, true) + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + oasRisk, err := h.convertRisk(ctx, *risk, from.ID, currencyPtr) + if err != nil { + return nil, toError(http.StatusInternalServerError, err) + } + resp.Transactions = append(resp.Transactions, oas.MigrationTransaction{ + Seqno: int32(seqno), + Boc: bocBase64, + Emulation: oas.MessageConsequences{ + Trace: convertedTrace, + Event: event, + Risk: oasRisk, + }, + }) + overrides = finalStates + } + return resp, nil +} + +func (h *Handler) collectJettonWallets(ctx context.Context, owners []ton.AccountID) (map[ton.AccountID][]core.JettonWallet, error) { + bulk, ok := h.storage.(jettonBulkStorage) + if !ok { + return nil, core.ErrEntityNotFound + } + wallets, err := bulk.GetJettonWalletsByOwnerAddresses(ctx, owners, false) + if err != nil && !errors.Is(err, core.ErrEntityNotFound) { + return nil, err + } + byOwner := make(map[ton.AccountID][]core.JettonWallet, len(owners)) + for _, w := range wallets { + if w.OwnerAddress == nil { + continue + } + byOwner[*w.OwnerAddress] = append(byOwner[*w.OwnerAddress], w) + } + return byOwner, nil +} + +func (h *Handler) emulateWalletMessage(ctx context.Context, msg tlb.Message, overrides map[ton.AccountID]tlb.ShardAccount) (*core.Trace, map[ton.AccountID]tlb.ShardAccount, error) { + configBase64, err := h.storage.TrimmedConfigBase64() + if err != nil { + return nil, nil, err + } + options := []txemulator.TraceOption{ + txemulator.WithConfigBase64(configBase64), + txemulator.WithAccountsSource(h.storage), + txemulator.WithLimit(1100), + txemulator.WithIgnoreSignatureDepth(1), + } + if len(overrides) > 0 { + options = append(options, txemulator.WithAccountsMap(overrides)) + } + emulator, err := txemulator.NewTraceBuilder(options...) + if err != nil { + return nil, nil, err + } + tree, err := emulator.Run(ctx, msg) + if err != nil { + return nil, nil, err + } + trace, err := EmulatedTreeToTrace(ctx, h.executor, h.storage, tree, emulator.FinalStates(), nil, h.configPool, true) + if err != nil { + return nil, nil, err + } + return trace, emulator.FinalStates(), nil +} + +func parseWalletData(version tonwallet.Version, data []byte) (seqno uint32, subWalletID uint32, publicKey ed25519.PublicKey, err error) { + cells, err := boc.DeserializeBoc(data) + if err != nil { + return 0, 0, nil, err + } + if len(cells) == 0 { + return 0, 0, nil, fmt.Errorf("empty wallet data") + } + switch version { + case tonwallet.V3R1, tonwallet.V3R2: + var d tonwallet.DataV3 + if err := tlb.Unmarshal(cells[0], &d); err != nil { + return 0, 0, nil, err + } + return d.Seqno, d.SubWalletId, append(ed25519.PublicKey{}, d.PublicKey[:]...), nil + case tonwallet.V4R1, tonwallet.V4R2: + var d tonwallet.DataV4 + if err := tlb.Unmarshal(cells[0], &d); err != nil { + return 0, 0, nil, err + } + return d.Seqno, d.SubWalletId, append(ed25519.PublicKey{}, d.PublicKey[:]...), nil + case tonwallet.V5R1, tonwallet.V5Beta: + var d tonwallet.DataV5R1 + if err := tlb.Unmarshal(cells[0], &d); err != nil { + return 0, 0, nil, err + } + return d.Seqno, 0, append(ed25519.PublicKey{}, d.PublicKey[:]...), nil + default: + return 0, 0, nil, fmt.Errorf("unsupported wallet version for migration: %v", version.ToString()) + } +} + +func buildUnsignedBody(v tonwallet.Version, subWalletID uint32, pubkey ed25519.PublicKey, workchain int, seqno uint32, validUntil time.Time, s []tonwallet.RawMessage) (*boc.Cell, error) { + switch v { + case tonwallet.V3R1, tonwallet.V3R2: + body := tonwallet.MessageV3{ + SubWalletId: subWalletID, + ValidUntil: uint32(validUntil.Unix()), + Seqno: seqno, + RawMessages: tonwallet.PayloadV1toV4(s), + } + cell := boc.NewCell() + if err := tlb.Marshal(cell, body); err != nil { + return nil, err + } + return cell, nil + case tonwallet.V4R1, tonwallet.V4R2: + body := tonwallet.MessageV4{ + SubWalletId: subWalletID, + ValidUntil: uint32(validUntil.Unix()), + Seqno: seqno, + Op: 0, + RawMessages: tonwallet.PayloadV1toV4(s), + } + cell := boc.NewCell() + if err := tlb.Marshal(cell, body); err != nil { + return nil, err + } + return cell, nil + case tonwallet.V5R1, tonwallet.V5Beta: + w5 := tonwallet.NewWalletV5R1(pubkey, tonwallet.Options{Workchain: &workchain}) + return w5.CreateMsgBodyWithoutSignature(s, tonwallet.MessageConfig{ + Seqno: seqno, + ValidUntil: validUntil, + V5MsgType: tonwallet.V5MsgTypeSignedExternal, + }) + default: + return nil, fmt.Errorf("unsupported wallet version for migration: %v", v.ToString()) + } +} + +// signedBodyForEmulation wraps the unsigned body with a zero signature so it can be emulated with +// signature checks disabled. v3/v4 prepend the signature, v5 carries a trailing placeholder. +func signedBodyForEmulation(v tonwallet.Version, body *boc.Cell) (*boc.Cell, error) { + switch v { + case tonwallet.V5R1, tonwallet.V5Beta: + return body, nil + default: + signed := tonwallet.SignedMsgBody{Sign: tlb.Bits512{}, Message: tlb.Any(*body)} + cell := boc.NewCell() + if err := tlb.Marshal(cell, signed); err != nil { + return nil, err + } + return cell, nil + } +} + +func chunkMessages(s []tonwallet.RawMessage, n int) [][]tonwallet.RawMessage { + if n <= 0 { + n = 1 + } + var batches [][]tonwallet.RawMessage + for i := 0; i < len(s); i += n { + end := min(i+n, len(s)) + batches = append(batches, s[i:end]) + } + return batches +} + +func walletJettonTransferMessage(src, dst ton.AccountID, amount *big.Int) (tonwallet.Message, error) { + body := boc.NewCell() + msgBody := abi.JettonTransferMsgBody{ + QueryId: 0, + Amount: tlb.VarUInteger16(*amount), + Destination: dst.ToMsgAddress(), + ResponseDestination: dst.ToMsgAddress(), + ForwardTonAmount: tlb.VarUInteger16(*big.NewInt(int64(migrationForwardAmount))), + } + if err := body.WriteUint(0xf8a7ea5, 32); err != nil { + return tonwallet.Message{}, err + } + if err := tlb.Marshal(body, msgBody); err != nil { + return tonwallet.Message{}, err + } + return tonwallet.Message{ + Amount: migrationGasPerTransfer, + Address: src, + Bounce: true, + Mode: tonwallet.DefaultMessageMode, + Body: body, + }, nil +} + +func walletNFTTransferMessage(src, dst ton.AccountID) tonwallet.Message { + body := boc.NewCell() + msgBody := abi.NftTransferMsgBody{ + QueryId: 0, + NewOwner: dst.ToMsgAddress(), + ResponseDestination: dst.ToMsgAddress(), + ForwardAmount: tlb.VarUInteger16(*big.NewInt(int64(migrationForwardAmount))), + } + _ = body.WriteUint(0x5fcc3d14, 32) + _ = tlb.Marshal(body, msgBody) + return tonwallet.Message{ + Amount: migrationGasPerTransfer, + Address: src, + Bounce: true, + Mode: tonwallet.DefaultMessageMode, + Body: body, + } +} + +func toWalletRawMessage(msg tonwallet.Message) (tonwallet.RawMessage, error) { + intMsg, mode, err := msg.ToInternal() + if err != nil { + return tonwallet.RawMessage{}, err + } + cell := boc.NewCell() + if err := tlb.Marshal(cell, intMsg); err != nil { + return tonwallet.RawMessage{}, err + } + return tonwallet.RawMessage{Message: cell, Mode: mode}, nil +} + +func walletMaxMessageCount(v tonwallet.Version) int { + switch v { + case tonwallet.V5R1, tonwallet.V5Beta: + return 255 + default: + return 4 + } +} diff --git a/pkg/api/migration_handlers_test.go b/pkg/api/migration_handlers_test.go new file mode 100644 index 00000000..8e4f0ce9 --- /dev/null +++ b/pkg/api/migration_handlers_test.go @@ -0,0 +1,213 @@ +package api + +import ( + "context" + "math/big" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/tonkeeper/opentonapi/pkg/oas" + "github.com/tonkeeper/tongo/abi" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" + tongoWallet "github.com/tonkeeper/tongo/wallet" +) + +func TestHandler_GetMigrationWallets_Validation(t *testing.T) { + h := &Handler{limits: Limits{BulkLimits: 4}} + tests := []struct { + name string + ids []string + wantErrPrefix string + }{ + { + name: "empty list", + ids: []string{}, + wantErrPrefix: "empty list of ids", + }, + { + name: "over the bulk limit", + ids: []string{"0:00", "0:01", "0:02", "0:03", "0:04"}, + wantErrPrefix: "the maximum number of accounts to request at once: 4", + }, + { + name: "invalid address", + ids: []string{"not-an-address"}, + wantErrPrefix: "can't decode address", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := oas.OptGetMigrationWalletsReq{ + Set: true, + Value: oas.GetMigrationWalletsReq{AccountIds: tt.ids}, + } + _, err := h.GetMigrationWallets(context.Background(), req, oas.GetMigrationWalletsParams{}) + requireBadRequestPrefix(t, err, tt.wantErrPrefix) + }) + } +} + +func TestHandler_PrepareMigration_Validation(t *testing.T) { + h := &Handler{} + tests := []struct { + name string + from string + to string + wantErrPrefix string + }{ + { + name: "invalid from", + from: "not-an-address", + to: "0:97264395bd65a255a429b11326c84128b7d70ffed7949abae3036d506ba38621", + wantErrPrefix: "invalid `from` address", + }, + { + name: "invalid to", + from: "0:97264395bd65a255a429b11326c84128b7d70ffed7949abae3036d506ba38621", + to: "not-an-address", + wantErrPrefix: "invalid `to` address", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := h.PrepareMigration(context.Background(), &oas.MigrationPrepareRequest{From: tt.from, To: tt.to}) + requireBadRequestPrefix(t, err, tt.wantErrPrefix) + }) + } +} + +func requireBadRequestPrefix(t *testing.T, err error, prefix string) { + t.Helper() + require.Error(t, err) + badRequest, ok := err.(*oas.ErrorStatusCode) + require.True(t, ok, "expected *oas.ErrorStatusCode, got %T", err) + require.Equal(t, 400, badRequest.StatusCode) + require.Contains(t, badRequest.Response.Error, prefix) +} + +func TestMaxMessagesForVersion(t *testing.T) { + require.Equal(t, 4, walletMaxMessageCount(tongoWallet.V3R2)) + require.Equal(t, 4, walletMaxMessageCount(tongoWallet.V4R2)) + require.Equal(t, 255, walletMaxMessageCount(tongoWallet.V5R1)) +} + +func TestChunkMessages(t *testing.T) { + mk := func(n int) []tongoWallet.RawMessage { + out := make([]tongoWallet.RawMessage, n) + return out + } + require.Empty(t, chunkMessages(nil, 4)) + + // 9 messages, batches of 4 => 4 + 4 + 1 + batches := chunkMessages(mk(9), 4) + require.Len(t, batches, 3) + require.Len(t, batches[0], 4) + require.Len(t, batches[1], 4) + require.Len(t, batches[2], 1) + + // a single batch when everything fits (v5) + require.Len(t, chunkMessages(mk(200), 255), 1) +} + +func TestChunkMessages_SweepIsLast(t *testing.T) { + to := ton.MustParseAccountID("0:97264395bd65a255a429b11326c84128b7d70ffed7949abae3036d506ba38621") + jetton := ton.MustParseAccountID("0:0000000000000000000000000000000000000000000000000000000000000001") + + var messages []tongoWallet.RawMessage + for range 5 { + jm, err := walletJettonTransferMessage(jetton, to, big.NewInt(1000)) + require.NoError(t, err) + raw, err := toWalletRawMessage(jm) + require.NoError(t, err) + messages = append(messages, raw) + } + sweep, err := toWalletRawMessage(tongoWallet.Message{Amount: 0, Address: to, Mode: migrationSweepMode}) + require.NoError(t, err) + messages = append(messages, sweep) + + batches := chunkMessages(messages, 4) // v4: 6 msgs => [4][2] + require.Len(t, batches, 2) + last := batches[len(batches)-1] + require.Equal(t, byte(migrationSweepMode), last[len(last)-1].Mode, "TON sweep must be the last message") +} + +func TestJettonTransferMessage(t *testing.T) { + jettonWallet := ton.MustParseAccountID("0:0000000000000000000000000000000000000000000000000000000000000001") + destination := ton.MustParseAccountID("0:97264395bd65a255a429b11326c84128b7d70ffed7949abae3036d506ba38621") + amount := big.NewInt(123456789) + + msg, err := walletJettonTransferMessage(jettonWallet, destination, amount) + require.NoError(t, err) + require.Equal(t, jettonWallet, msg.Address) + require.Equal(t, migrationGasPerTransfer, msg.Amount) + require.Equal(t, byte(tongoWallet.DefaultMessageMode), msg.Mode) + + body := msg.Body + body.ResetCounters() + op, err := body.ReadUint(32) + require.NoError(t, err) + require.Equal(t, uint64(0xf8a7ea5), op) + var decoded abi.JettonTransferMsgBody + require.NoError(t, tlb.Unmarshal(body, &decoded)) + require.Equal(t, amount.String(), (*big.Int)(&decoded.Amount).String()) + gotDest, err := ton.AccountIDFromTlb(decoded.Destination) + require.NoError(t, err) + require.NotNil(t, gotDest) + require.Equal(t, destination, *gotDest) +} + +func TestNftTransferMessage(t *testing.T) { + item := ton.MustParseAccountID("0:0000000000000000000000000000000000000000000000000000000000000002") + destination := ton.MustParseAccountID("0:97264395bd65a255a429b11326c84128b7d70ffed7949abae3036d506ba38621") + + msg := walletNFTTransferMessage(item, destination) + require.Equal(t, item, msg.Address) + + body := msg.Body + body.ResetCounters() + op, err := body.ReadUint(32) + require.NoError(t, err) + require.Equal(t, uint64(0x5fcc3d14), op) + var decoded abi.NftTransferMsgBody + require.NoError(t, tlb.Unmarshal(body, &decoded)) + gotOwner, err := ton.AccountIDFromTlb(decoded.NewOwner) + require.NoError(t, err) + require.NotNil(t, gotOwner) + require.Equal(t, destination, *gotOwner) +} + +func TestBuildUnsignedBodyV4(t *testing.T) { + to := ton.MustParseAccountID("0:97264395bd65a255a429b11326c84128b7d70ffed7949abae3036d506ba38621") + sweep, err := toWalletRawMessage(tongoWallet.Message{Amount: 0, Address: to, Mode: migrationSweepMode}) + require.NoError(t, err) + messages := []tongoWallet.RawMessage{sweep} + + const seqno = 12 + const subWalletID = 698983191 + body, err := buildUnsignedBody(tongoWallet.V4R2, subWalletID, nil, 0, seqno, time.Unix(1900000000, 0), messages) + require.NoError(t, err) + + body.ResetCounters() + var decoded tongoWallet.MessageV4 + require.NoError(t, tlb.Unmarshal(body, &decoded)) + require.Equal(t, uint32(seqno), decoded.Seqno) + require.Equal(t, uint32(subWalletID), decoded.SubWalletId) + require.Equal(t, int8(0), decoded.Op) + require.Len(t, decoded.RawMessages, len(messages)) +} + +func TestSignedBodyForEmulation(t *testing.T) { + to := ton.MustParseAccountID("0:97264395bd65a255a429b11326c84128b7d70ffed7949abae3036d506ba38621") + sweep, err := toWalletRawMessage(tongoWallet.Message{Amount: 0, Address: to, Mode: migrationSweepMode}) + require.NoError(t, err) + body, err := buildUnsignedBody(tongoWallet.V4R2, 698983191, nil, 0, 1, time.Unix(1900000000, 0), []tongoWallet.RawMessage{sweep}) + require.NoError(t, err) + + signed, err := signedBodyForEmulation(tongoWallet.V4R2, body) + require.NoError(t, err) + // v3/v4 prepend a 512-bit signature in front of the body bits. + require.Equal(t, body.BitsAvailableForRead()+512, signed.BitsAvailableForRead()) +} diff --git a/pkg/oas/oas_client_gen.go b/pkg/oas/oas_client_gen.go index a26d05b2..e3a7463a 100644 --- a/pkg/oas/oas_client_gen.go +++ b/pkg/oas/oas_client_gen.go @@ -447,6 +447,13 @@ type Invoker interface { // // GET /v2/rates/markets GetMarketsRates(ctx context.Context) (*GetMarketsRatesOK, error) + // GetMigrationWallets invokes getMigrationWallets operation. + // + // Get migratable assets value (TON balance, jettons with prices, NFT count) for several wallets at + // once. + // + // POST /v2/migration/wallets + GetMigrationWallets(ctx context.Context, request OptGetMigrationWalletsReq, params GetMigrationWalletsParams) (*MigrationWallets, error) // GetMultisigAccount invokes getMultisigAccount operation. // // Get multisig account info. @@ -711,6 +718,12 @@ type Invoker interface { // // POST /v2/pubkeys/wallets/_bulk GetWalletsByPublicKeyBulk(ctx context.Context, request OptGetWalletsByPublicKeyBulkReq) (*WalletsByPublicKeys, error) + // PrepareMigration invokes prepareMigration operation. + // + // Prepare ordered signable transactions that migrate every asset from `from` to `to`. + // + // POST /v2/migration/prepare + PrepareMigration(ctx context.Context, request *MigrationPrepareRequest) (*MigrationPrepareResponse, error) // ReindexAccount invokes reindexAccount operation. // // Update internal cache for a particular account. @@ -8304,6 +8317,114 @@ func (c *Client) sendGetMarketsRates(ctx context.Context) (res *GetMarketsRatesO return result, nil } +// GetMigrationWallets invokes getMigrationWallets operation. +// +// Get migratable assets value (TON balance, jettons with prices, NFT count) for several wallets at +// once. +// +// POST /v2/migration/wallets +func (c *Client) GetMigrationWallets(ctx context.Context, request OptGetMigrationWalletsReq, params GetMigrationWalletsParams) (*MigrationWallets, error) { + res, err := c.sendGetMigrationWallets(ctx, request, params) + return res, err +} + +func (c *Client) sendGetMigrationWallets(ctx context.Context, request OptGetMigrationWalletsReq, params GetMigrationWalletsParams) (res *MigrationWallets, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("getMigrationWallets"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.URLTemplateKey.String("/v2/migration/wallets"), + } + 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, GetMigrationWalletsOperation, + 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 [1]string + pathParts[0] = "/v2/migration/wallets" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeQueryParams" + q := uri.NewQueryEncoder() + { + // Encode "currencies" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "currencies", + Style: uri.QueryStyleForm, + Explode: false, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + if params.Currencies != nil { + return e.EncodeArray(func(e uri.Encoder) error { + for i, item := range params.Currencies { + if err := func() error { + return e.EncodeValue(conv.StringToString(item)) + }(); err != nil { + return errors.Wrapf(err, "[%d]", i) + } + } + return nil + }) + } + return nil + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } + u.RawQuery = q.Values().Encode() + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "POST", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + if err := encodeGetMigrationWalletsRequest(request, r); err != nil { + return res, errors.Wrap(err, "encode 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 := decodeGetMigrationWalletsResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + // GetMultisigAccount invokes getMultisigAccount operation. // // Get multisig account info. @@ -12702,6 +12823,83 @@ func (c *Client) sendGetWalletsByPublicKeyBulk(ctx context.Context, request OptG return result, nil } +// PrepareMigration invokes prepareMigration operation. +// +// Prepare ordered signable transactions that migrate every asset from `from` to `to`. +// +// POST /v2/migration/prepare +func (c *Client) PrepareMigration(ctx context.Context, request *MigrationPrepareRequest) (*MigrationPrepareResponse, error) { + res, err := c.sendPrepareMigration(ctx, request) + return res, err +} + +func (c *Client) sendPrepareMigration(ctx context.Context, request *MigrationPrepareRequest) (res *MigrationPrepareResponse, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("prepareMigration"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.URLTemplateKey.String("/v2/migration/prepare"), + } + 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, PrepareMigrationOperation, + 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 [1]string + pathParts[0] = "/v2/migration/prepare" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "POST", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + if err := encodePrepareMigrationRequest(request, r); err != nil { + return res, errors.Wrap(err, "encode 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 := decodePrepareMigrationResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + // ReindexAccount invokes reindexAccount operation. // // Update internal cache for a particular account. diff --git a/pkg/oas/oas_defaults_gen.go b/pkg/oas/oas_defaults_gen.go index 2a2a7905..df2f36a8 100644 --- a/pkg/oas/oas_defaults_gen.go +++ b/pkg/oas/oas_defaults_gen.go @@ -22,6 +22,14 @@ func (s *GaslessEstimateReq) setDefaults() { } } +// setDefaults set default value of fields. +func (s *MigrationPrepareRequest) setDefaults() { + { + val := string("USD") + s.Currency.SetTo(val) + } +} + // setDefaults set default value of fields. func (s *ServiceStatus) setDefaults() { { diff --git a/pkg/oas/oas_handlers_gen.go b/pkg/oas/oas_handlers_gen.go index 5508d4f9..4b2baf7f 100644 --- a/pkg/oas/oas_handlers_gen.go +++ b/pkg/oas/oas_handlers_gen.go @@ -10687,6 +10687,176 @@ func (s *Server) handleGetMarketsRatesRequest(args [0]string, argsEscaped bool, } } +// handleGetMigrationWalletsRequest handles getMigrationWallets operation. +// +// Get migratable assets value (TON balance, jettons with prices, NFT count) for several wallets at +// once. +// +// POST /v2/migration/wallets +func (s *Server) handleGetMigrationWalletsRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("getMigrationWallets"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/v2/migration/wallets"), + } + // Add attributes from config. + otelAttrs = append(otelAttrs, s.cfg.Attributes...) + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), GetMigrationWalletsOperation, + 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: GetMigrationWalletsOperation, + ID: "getMigrationWallets", + } + ) + params, err := decodeGetMigrationWalletsParams(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 + request, rawBody, close, err := s.decodeGetMigrationWalletsRequest(r) + if err != nil { + err = &ogenerrors.DecodeRequestError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeRequest", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + defer func() { + if err := close(); err != nil { + recordError("CloseRequest", err) + } + }() + + var response *MigrationWallets + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: GetMigrationWalletsOperation, + OperationSummary: "", + OperationID: "getMigrationWallets", + Body: request, + RawBody: rawBody, + Params: middleware.Parameters{ + { + Name: "currencies", + In: "query", + }: params.Currencies, + }, + Raw: r, + } + + type ( + Request = OptGetMigrationWalletsReq + Params = GetMigrationWalletsParams + Response = *MigrationWallets + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackGetMigrationWalletsParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.GetMigrationWallets(ctx, request, params) + return response, err + }, + ) + } else { + response, err = s.h.GetMigrationWallets(ctx, request, 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 := encodeGetMigrationWalletsResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + // handleGetMultisigAccountRequest handles getMultisigAccount operation. // // Get multisig account info. @@ -17273,6 +17443,160 @@ func (s *Server) handleGetWalletsByPublicKeyBulkRequest(args [0]string, argsEsca } } +// handlePrepareMigrationRequest handles prepareMigration operation. +// +// Prepare ordered signable transactions that migrate every asset from `from` to `to`. +// +// POST /v2/migration/prepare +func (s *Server) handlePrepareMigrationRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("prepareMigration"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/v2/migration/prepare"), + } + // Add attributes from config. + otelAttrs = append(otelAttrs, s.cfg.Attributes...) + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), PrepareMigrationOperation, + 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: PrepareMigrationOperation, + ID: "prepareMigration", + } + ) + + var rawBody []byte + request, rawBody, close, err := s.decodePrepareMigrationRequest(r) + if err != nil { + err = &ogenerrors.DecodeRequestError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeRequest", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + defer func() { + if err := close(); err != nil { + recordError("CloseRequest", err) + } + }() + + var response *MigrationPrepareResponse + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: PrepareMigrationOperation, + OperationSummary: "", + OperationID: "prepareMigration", + Body: request, + RawBody: rawBody, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = *MigrationPrepareRequest + Params = struct{} + Response = *MigrationPrepareResponse + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.PrepareMigration(ctx, request) + return response, err + }, + ) + } else { + response, err = s.h.PrepareMigration(ctx, request) + } + 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 := encodePrepareMigrationResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + // handleReindexAccountRequest handles reindexAccount operation. // // Update internal cache for a particular account. diff --git a/pkg/oas/oas_json_gen.go b/pkg/oas/oas_json_gen.go index 55854690..0f906952 100644 --- a/pkg/oas/oas_json_gen.go +++ b/pkg/oas/oas_json_gen.go @@ -20661,6 +20661,114 @@ func (s *GetMarketsRatesOK) UnmarshalJSON(data []byte) error { return s.Decode(d) } +// Encode implements json.Marshaler. +func (s *GetMigrationWalletsReq) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *GetMigrationWalletsReq) encodeFields(e *jx.Encoder) { + { + e.FieldStart("account_ids") + e.ArrStart() + for _, elem := range s.AccountIds { + e.Str(elem) + } + e.ArrEnd() + } +} + +var jsonFieldsNameOfGetMigrationWalletsReq = [1]string{ + 0: "account_ids", +} + +// Decode decodes GetMigrationWalletsReq from json. +func (s *GetMigrationWalletsReq) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode GetMigrationWalletsReq to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "account_ids": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + s.AccountIds = make([]string, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem string + v, err := d.Str() + elem = string(v) + if err != nil { + return err + } + s.AccountIds = append(s.AccountIds, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"account_ids\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode GetMigrationWalletsReq") + } + // 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(jsonFieldsNameOfGetMigrationWalletsReq) { + name = jsonFieldsNameOfGetMigrationWalletsReq[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 *GetMigrationWalletsReq) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *GetMigrationWalletsReq) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + // Encode implements json.Marshaler. func (s *GetNftCollectionItemsByAddressesReq) Encode(e *jx.Encoder) { e.ObjStart() @@ -29860,165 +29968,859 @@ func (s *MethodExecutionResult) UnmarshalJSON(data []byte) error { } // Encode implements json.Marshaler. -func (s *MisbehaviourPunishmentConfig) Encode(e *jx.Encoder) { +func (s *MigrationPrepareRequest) Encode(e *jx.Encoder) { e.ObjStart() s.encodeFields(e) e.ObjEnd() } // encodeFields encodes fields. -func (s *MisbehaviourPunishmentConfig) encodeFields(e *jx.Encoder) { - { - e.FieldStart("default_flat_fine") - e.Int64(s.DefaultFlatFine) - } - { - e.FieldStart("default_proportional_fine") - e.Int64(s.DefaultProportionalFine) - } - { - e.FieldStart("severity_flat_mult") - e.Int(s.SeverityFlatMult) - } - { - e.FieldStart("severity_proportional_mult") - e.Int(s.SeverityProportionalMult) - } - { - e.FieldStart("unpunishable_interval") - e.Int(s.UnpunishableInterval) - } - { - e.FieldStart("long_interval") - e.Int(s.LongInterval) - } - { - e.FieldStart("long_flat_mult") - e.Int(s.LongFlatMult) - } - { - e.FieldStart("long_proportional_mult") - e.Int(s.LongProportionalMult) - } +func (s *MigrationPrepareRequest) encodeFields(e *jx.Encoder) { { - e.FieldStart("medium_interval") - e.Int(s.MediumInterval) + e.FieldStart("from") + e.Str(s.From) } { - e.FieldStart("medium_flat_mult") - e.Int(s.MediumFlatMult) + e.FieldStart("to") + e.Str(s.To) } { - e.FieldStart("medium_proportional_mult") - e.Int(s.MediumProportionalMult) + if s.Currency.Set { + e.FieldStart("currency") + s.Currency.Encode(e) + } } } -var jsonFieldsNameOfMisbehaviourPunishmentConfig = [11]string{ - 0: "default_flat_fine", - 1: "default_proportional_fine", - 2: "severity_flat_mult", - 3: "severity_proportional_mult", - 4: "unpunishable_interval", - 5: "long_interval", - 6: "long_flat_mult", - 7: "long_proportional_mult", - 8: "medium_interval", - 9: "medium_flat_mult", - 10: "medium_proportional_mult", +var jsonFieldsNameOfMigrationPrepareRequest = [3]string{ + 0: "from", + 1: "to", + 2: "currency", } -// Decode decodes MisbehaviourPunishmentConfig from json. -func (s *MisbehaviourPunishmentConfig) Decode(d *jx.Decoder) error { +// Decode decodes MigrationPrepareRequest from json. +func (s *MigrationPrepareRequest) Decode(d *jx.Decoder) error { if s == nil { - return errors.New("invalid: unable to decode MisbehaviourPunishmentConfig to nil") + return errors.New("invalid: unable to decode MigrationPrepareRequest to nil") } - var requiredBitSet [2]uint8 + var requiredBitSet [1]uint8 + s.setDefaults() if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { switch string(k) { - case "default_flat_fine": + case "from": requiredBitSet[0] |= 1 << 0 if err := func() error { - v, err := d.Int64() - s.DefaultFlatFine = int64(v) + v, err := d.Str() + s.From = string(v) if err != nil { return err } return nil }(); err != nil { - return errors.Wrap(err, "decode field \"default_flat_fine\"") + return errors.Wrap(err, "decode field \"from\"") } - case "default_proportional_fine": + case "to": requiredBitSet[0] |= 1 << 1 if err := func() error { - v, err := d.Int64() - s.DefaultProportionalFine = int64(v) - if err != nil { - return err - } - return nil - }(); err != nil { - return errors.Wrap(err, "decode field \"default_proportional_fine\"") - } - case "severity_flat_mult": - requiredBitSet[0] |= 1 << 2 - if err := func() error { - v, err := d.Int() - s.SeverityFlatMult = int(v) - if err != nil { - return err - } - return nil - }(); err != nil { - return errors.Wrap(err, "decode field \"severity_flat_mult\"") - } - case "severity_proportional_mult": - requiredBitSet[0] |= 1 << 3 - if err := func() error { - v, err := d.Int() - s.SeverityProportionalMult = int(v) + v, err := d.Str() + s.To = string(v) if err != nil { return err } return nil }(); err != nil { - return errors.Wrap(err, "decode field \"severity_proportional_mult\"") + return errors.Wrap(err, "decode field \"to\"") } - case "unpunishable_interval": - requiredBitSet[0] |= 1 << 4 + case "currency": if err := func() error { - v, err := d.Int() - s.UnpunishableInterval = int(v) - if err != nil { + s.Currency.Reset() + if err := s.Currency.Decode(d); err != nil { return err } return nil }(); err != nil { - return errors.Wrap(err, "decode field \"unpunishable_interval\"") + return errors.Wrap(err, "decode field \"currency\"") } - case "long_interval": - requiredBitSet[0] |= 1 << 5 - if err := func() error { - v, err := d.Int() - s.LongInterval = int(v) - if err != nil { - return err + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode MigrationPrepareRequest") + } + // 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(jsonFieldsNameOfMigrationPrepareRequest) { + name = jsonFieldsNameOfMigrationPrepareRequest[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) } - return nil - }(); err != nil { - return errors.Wrap(err, "decode field \"long_interval\"") + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx } - case "long_flat_mult": - requiredBitSet[0] |= 1 << 6 - if err := func() error { - v, err := d.Int() - s.LongFlatMult = int(v) - if err != nil { - return err - } - return nil - }(); err != nil { + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *MigrationPrepareRequest) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *MigrationPrepareRequest) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *MigrationPrepareResponse) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *MigrationPrepareResponse) encodeFields(e *jx.Encoder) { + { + e.FieldStart("from") + e.Str(s.From) + } + { + e.FieldStart("to") + e.Str(s.To) + } + { + e.FieldStart("wallet_version") + e.Str(s.WalletVersion) + } + { + e.FieldStart("transactions") + e.ArrStart() + for _, elem := range s.Transactions { + elem.Encode(e) + } + e.ArrEnd() + } +} + +var jsonFieldsNameOfMigrationPrepareResponse = [4]string{ + 0: "from", + 1: "to", + 2: "wallet_version", + 3: "transactions", +} + +// Decode decodes MigrationPrepareResponse from json. +func (s *MigrationPrepareResponse) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode MigrationPrepareResponse to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "from": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Str() + s.From = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"from\"") + } + case "to": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + v, err := d.Str() + s.To = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"to\"") + } + case "wallet_version": + requiredBitSet[0] |= 1 << 2 + if err := func() error { + v, err := d.Str() + s.WalletVersion = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"wallet_version\"") + } + case "transactions": + requiredBitSet[0] |= 1 << 3 + if err := func() error { + s.Transactions = make([]MigrationTransaction, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem MigrationTransaction + if err := elem.Decode(d); err != nil { + return err + } + s.Transactions = append(s.Transactions, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"transactions\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode MigrationPrepareResponse") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00001111, + } { + 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(jsonFieldsNameOfMigrationPrepareResponse) { + name = jsonFieldsNameOfMigrationPrepareResponse[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 *MigrationPrepareResponse) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *MigrationPrepareResponse) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *MigrationTransaction) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *MigrationTransaction) encodeFields(e *jx.Encoder) { + { + e.FieldStart("seqno") + e.Int32(s.Seqno) + } + { + e.FieldStart("boc") + e.Str(s.Boc) + } + { + e.FieldStart("emulation") + s.Emulation.Encode(e) + } +} + +var jsonFieldsNameOfMigrationTransaction = [3]string{ + 0: "seqno", + 1: "boc", + 2: "emulation", +} + +// Decode decodes MigrationTransaction from json. +func (s *MigrationTransaction) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode MigrationTransaction to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "seqno": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Int32() + s.Seqno = int32(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"seqno\"") + } + case "boc": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + v, err := d.Str() + s.Boc = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"boc\"") + } + case "emulation": + requiredBitSet[0] |= 1 << 2 + if err := func() error { + if err := s.Emulation.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"emulation\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode MigrationTransaction") + } + // 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(jsonFieldsNameOfMigrationTransaction) { + name = jsonFieldsNameOfMigrationTransaction[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 *MigrationTransaction) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *MigrationTransaction) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *MigrationWalletValue) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *MigrationWalletValue) encodeFields(e *jx.Encoder) { + { + e.FieldStart("account") + e.Str(s.Account) + } + { + e.FieldStart("balance") + e.Int64(s.Balance) + } + { + e.FieldStart("status") + s.Status.Encode(e) + } + { + e.FieldStart("jettons") + e.ArrStart() + for _, elem := range s.Jettons { + elem.Encode(e) + } + e.ArrEnd() + } + { + e.FieldStart("nft_count") + e.Int32(s.NftCount) + } +} + +var jsonFieldsNameOfMigrationWalletValue = [5]string{ + 0: "account", + 1: "balance", + 2: "status", + 3: "jettons", + 4: "nft_count", +} + +// Decode decodes MigrationWalletValue from json. +func (s *MigrationWalletValue) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode MigrationWalletValue to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "account": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Str() + s.Account = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"account\"") + } + case "balance": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + v, err := d.Int64() + s.Balance = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"balance\"") + } + case "status": + requiredBitSet[0] |= 1 << 2 + if err := func() error { + if err := s.Status.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"status\"") + } + case "jettons": + requiredBitSet[0] |= 1 << 3 + if err := func() error { + s.Jettons = make([]JettonBalance, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem JettonBalance + if err := elem.Decode(d); err != nil { + return err + } + s.Jettons = append(s.Jettons, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"jettons\"") + } + case "nft_count": + requiredBitSet[0] |= 1 << 4 + if err := func() error { + v, err := d.Int32() + s.NftCount = int32(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"nft_count\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode MigrationWalletValue") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00011111, + } { + 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(jsonFieldsNameOfMigrationWalletValue) { + name = jsonFieldsNameOfMigrationWalletValue[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 *MigrationWalletValue) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *MigrationWalletValue) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *MigrationWallets) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *MigrationWallets) encodeFields(e *jx.Encoder) { + { + e.FieldStart("wallets") + e.ArrStart() + for _, elem := range s.Wallets { + elem.Encode(e) + } + e.ArrEnd() + } +} + +var jsonFieldsNameOfMigrationWallets = [1]string{ + 0: "wallets", +} + +// Decode decodes MigrationWallets from json. +func (s *MigrationWallets) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode MigrationWallets to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "wallets": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + s.Wallets = make([]MigrationWalletValue, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem MigrationWalletValue + if err := elem.Decode(d); err != nil { + return err + } + s.Wallets = append(s.Wallets, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"wallets\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode MigrationWallets") + } + // 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(jsonFieldsNameOfMigrationWallets) { + name = jsonFieldsNameOfMigrationWallets[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 *MigrationWallets) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *MigrationWallets) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *MisbehaviourPunishmentConfig) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *MisbehaviourPunishmentConfig) encodeFields(e *jx.Encoder) { + { + e.FieldStart("default_flat_fine") + e.Int64(s.DefaultFlatFine) + } + { + e.FieldStart("default_proportional_fine") + e.Int64(s.DefaultProportionalFine) + } + { + e.FieldStart("severity_flat_mult") + e.Int(s.SeverityFlatMult) + } + { + e.FieldStart("severity_proportional_mult") + e.Int(s.SeverityProportionalMult) + } + { + e.FieldStart("unpunishable_interval") + e.Int(s.UnpunishableInterval) + } + { + e.FieldStart("long_interval") + e.Int(s.LongInterval) + } + { + e.FieldStart("long_flat_mult") + e.Int(s.LongFlatMult) + } + { + e.FieldStart("long_proportional_mult") + e.Int(s.LongProportionalMult) + } + { + e.FieldStart("medium_interval") + e.Int(s.MediumInterval) + } + { + e.FieldStart("medium_flat_mult") + e.Int(s.MediumFlatMult) + } + { + e.FieldStart("medium_proportional_mult") + e.Int(s.MediumProportionalMult) + } +} + +var jsonFieldsNameOfMisbehaviourPunishmentConfig = [11]string{ + 0: "default_flat_fine", + 1: "default_proportional_fine", + 2: "severity_flat_mult", + 3: "severity_proportional_mult", + 4: "unpunishable_interval", + 5: "long_interval", + 6: "long_flat_mult", + 7: "long_proportional_mult", + 8: "medium_interval", + 9: "medium_flat_mult", + 10: "medium_proportional_mult", +} + +// Decode decodes MisbehaviourPunishmentConfig from json. +func (s *MisbehaviourPunishmentConfig) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode MisbehaviourPunishmentConfig to nil") + } + var requiredBitSet [2]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "default_flat_fine": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Int64() + s.DefaultFlatFine = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"default_flat_fine\"") + } + case "default_proportional_fine": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + v, err := d.Int64() + s.DefaultProportionalFine = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"default_proportional_fine\"") + } + case "severity_flat_mult": + requiredBitSet[0] |= 1 << 2 + if err := func() error { + v, err := d.Int() + s.SeverityFlatMult = int(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"severity_flat_mult\"") + } + case "severity_proportional_mult": + requiredBitSet[0] |= 1 << 3 + if err := func() error { + v, err := d.Int() + s.SeverityProportionalMult = int(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"severity_proportional_mult\"") + } + case "unpunishable_interval": + requiredBitSet[0] |= 1 << 4 + if err := func() error { + v, err := d.Int() + s.UnpunishableInterval = int(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"unpunishable_interval\"") + } + case "long_interval": + requiredBitSet[0] |= 1 << 5 + if err := func() error { + v, err := d.Int() + s.LongInterval = int(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"long_interval\"") + } + case "long_flat_mult": + requiredBitSet[0] |= 1 << 6 + if err := func() error { + v, err := d.Int() + s.LongFlatMult = int(v) + if err != nil { + return err + } + return nil + }(); err != nil { return errors.Wrap(err, "decode field \"long_flat_mult\"") } case "long_proportional_mult": @@ -35499,6 +36301,39 @@ func (s *OptGetJettonInfosByAddressesReq) UnmarshalJSON(data []byte) error { return s.Decode(d) } +// Encode encodes GetMigrationWalletsReq as json. +func (o OptGetMigrationWalletsReq) Encode(e *jx.Encoder) { + if !o.Set { + return + } + o.Value.Encode(e) +} + +// Decode decodes GetMigrationWalletsReq from json. +func (o *OptGetMigrationWalletsReq) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptGetMigrationWalletsReq to nil") + } + o.Set = true + if err := o.Value.Decode(d); err != nil { + return err + } + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptGetMigrationWalletsReq) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptGetMigrationWalletsReq) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + // Encode encodes GetNftCollectionItemsByAddressesReq as json. func (o OptGetNftCollectionItemsByAddressesReq) Encode(e *jx.Encoder) { if !o.Set { diff --git a/pkg/oas/oas_operations_gen.go b/pkg/oas/oas_operations_gen.go index 1353762d..ed8b1e60 100644 --- a/pkg/oas/oas_operations_gen.go +++ b/pkg/oas/oas_operations_gen.go @@ -73,6 +73,7 @@ const ( GetJettonsEventsOperation OperationName = "GetJettonsEvents" GetLibraryByHashOperation OperationName = "GetLibraryByHash" GetMarketsRatesOperation OperationName = "GetMarketsRates" + GetMigrationWalletsOperation OperationName = "GetMigrationWallets" GetMultisigAccountOperation OperationName = "GetMultisigAccount" GetMultisigOrderOperation OperationName = "GetMultisigOrder" GetNftCollectionOperation OperationName = "GetNftCollection" @@ -116,6 +117,7 @@ const ( GetWalletInfoOperation OperationName = "GetWalletInfo" GetWalletsByPublicKeyOperation OperationName = "GetWalletsByPublicKey" GetWalletsByPublicKeyBulkOperation OperationName = "GetWalletsByPublicKeyBulk" + PrepareMigrationOperation OperationName = "PrepareMigration" ReindexAccountOperation OperationName = "ReindexAccount" SearchAccountsOperation OperationName = "SearchAccounts" SendBlockchainMessageOperation OperationName = "SendBlockchainMessage" diff --git a/pkg/oas/oas_parameters_gen.go b/pkg/oas/oas_parameters_gen.go index 8d76b6da..1a0e9812 100644 --- a/pkg/oas/oas_parameters_gen.go +++ b/pkg/oas/oas_parameters_gen.go @@ -8676,6 +8676,73 @@ func decodeGetLibraryByHashParams(args [1]string, argsEscaped bool, r *http.Requ return params, nil } +// GetMigrationWalletsParams is parameters of getMigrationWallets operation. +type GetMigrationWalletsParams struct { + // Accept gram and all possible fiat currencies, separated by commas. + Currencies []string `json:",omitempty"` +} + +func unpackGetMigrationWalletsParams(packed middleware.Parameters) (params GetMigrationWalletsParams) { + { + key := middleware.ParameterKey{ + Name: "currencies", + In: "query", + } + if v, ok := packed[key]; ok { + params.Currencies = v.([]string) + } + } + return params +} + +func decodeGetMigrationWalletsParams(args [0]string, argsEscaped bool, r *http.Request) (params GetMigrationWalletsParams, _ error) { + q := uri.NewQueryDecoder(r.URL.Query()) + // Decode query: currencies. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "currencies", + Style: uri.QueryStyleForm, + Explode: false, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + return d.DecodeArray(func(d uri.Decoder) error { + var paramsDotCurrenciesVal string + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + paramsDotCurrenciesVal = c + return nil + }(); err != nil { + return err + } + params.Currencies = append(params.Currencies, paramsDotCurrenciesVal) + return nil + }) + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "currencies", + In: "query", + Err: err, + } + } + return params, nil +} + // GetMultisigAccountParams is parameters of getMultisigAccount operation. type GetMultisigAccountParams struct { // Account ID. diff --git a/pkg/oas/oas_request_decoders_gen.go b/pkg/oas/oas_request_decoders_gen.go index 7d252753..8034b0cd 100644 --- a/pkg/oas/oas_request_decoders_gen.go +++ b/pkg/oas/oas_request_decoders_gen.go @@ -950,6 +950,96 @@ func (s *Server) decodeGetJettonInfosByAddressesRequest(r *http.Request) ( } } +func (s *Server) decodeGetMigrationWalletsRequest(r *http.Request) ( + req OptGetMigrationWalletsReq, + rawBody []byte, + close func() error, + rerr error, +) { + var closers []func() error + close = func() error { + var merr error + // Close in reverse order, to match defer behavior. + for i := len(closers) - 1; i >= 0; i-- { + c := closers[i] + merr = errors.Join(merr, c()) + } + return merr + } + defer func() { + if rerr != nil { + rerr = errors.Join(rerr, close()) + } + }() + if _, ok := r.Header["Content-Type"]; !ok && r.ContentLength == 0 { + return req, rawBody, close, nil + } + ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return req, rawBody, close, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + if r.ContentLength == 0 { + return req, rawBody, close, nil + } + buf, err := io.ReadAll(r.Body) + defer func() { + _ = r.Body.Close() + }() + if err != nil { + return req, rawBody, close, err + } + + // Reset the body to allow for downstream reading. + r.Body = io.NopCloser(bytes.NewBuffer(buf)) + + if len(buf) == 0 { + return req, rawBody, close, nil + } + + rawBody = append(rawBody, buf...) + d := jx.DecodeBytes(buf) + + var request OptGetMigrationWalletsReq + if err := func() error { + request.Reset() + if err := request.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 req, rawBody, close, err + } + if err := func() error { + if value, ok := request.Get(); ok { + if err := func() error { + if err := value.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + return req, rawBody, close, errors.Wrap(err, "validate") + } + return request, rawBody, close, nil + default: + return req, rawBody, close, validate.InvalidContentType(ct) + } +} + func (s *Server) decodeGetNftCollectionItemsByAddressesRequest(r *http.Request) ( req OptGetNftCollectionItemsByAddressesReq, rawBody []byte, @@ -1220,6 +1310,77 @@ func (s *Server) decodeGetWalletsByPublicKeyBulkRequest(r *http.Request) ( } } +func (s *Server) decodePrepareMigrationRequest(r *http.Request) ( + req *MigrationPrepareRequest, + rawBody []byte, + close func() error, + rerr error, +) { + var closers []func() error + close = func() error { + var merr error + // Close in reverse order, to match defer behavior. + for i := len(closers) - 1; i >= 0; i-- { + c := closers[i] + merr = errors.Join(merr, c()) + } + return merr + } + defer func() { + if rerr != nil { + rerr = errors.Join(rerr, close()) + } + }() + ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return req, rawBody, close, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + if r.ContentLength == 0 { + return req, rawBody, close, validate.ErrBodyRequired + } + buf, err := io.ReadAll(r.Body) + defer func() { + _ = r.Body.Close() + }() + if err != nil { + return req, rawBody, close, err + } + + // Reset the body to allow for downstream reading. + r.Body = io.NopCloser(bytes.NewBuffer(buf)) + + if len(buf) == 0 { + return req, rawBody, close, validate.ErrBodyRequired + } + + rawBody = append(rawBody, buf...) + d := jx.DecodeBytes(buf) + + var request MigrationPrepareRequest + if err := func() error { + if err := request.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 req, rawBody, close, err + } + return &request, rawBody, close, nil + default: + return req, rawBody, close, validate.InvalidContentType(ct) + } +} + func (s *Server) decodeSendBlockchainMessageRequest(r *http.Request) ( req *SendBlockchainMessageReq, rawBody []byte, diff --git a/pkg/oas/oas_request_encoders_gen.go b/pkg/oas/oas_request_encoders_gen.go index 8b286ed5..5ef5d939 100644 --- a/pkg/oas/oas_request_encoders_gen.go +++ b/pkg/oas/oas_request_encoders_gen.go @@ -202,6 +202,26 @@ func encodeGetJettonInfosByAddressesRequest( return nil } +func encodeGetMigrationWalletsRequest( + req OptGetMigrationWalletsReq, + r *http.Request, +) error { + const contentType = "application/json" + if !req.Set { + // Keep request with empty body if value is not set. + return nil + } + e := new(jx.Encoder) + { + if req.Set { + req.Encode(e) + } + } + encoded := e.Bytes() + ht.SetBody(r, bytes.NewReader(encoded), contentType) + return nil +} + func encodeGetNftCollectionItemsByAddressesRequest( req OptGetNftCollectionItemsByAddressesReq, r *http.Request, @@ -262,6 +282,20 @@ func encodeGetWalletsByPublicKeyBulkRequest( return nil } +func encodePrepareMigrationRequest( + req *MigrationPrepareRequest, + r *http.Request, +) error { + const contentType = "application/json" + e := new(jx.Encoder) + { + req.Encode(e) + } + encoded := e.Bytes() + ht.SetBody(r, bytes.NewReader(encoded), contentType) + return nil +} + func encodeSendBlockchainMessageRequest( req *SendBlockchainMessageReq, r *http.Request, diff --git a/pkg/oas/oas_response_decoders_gen.go b/pkg/oas/oas_response_decoders_gen.go index 168cfc51..d671c242 100644 --- a/pkg/oas/oas_response_decoders_gen.go +++ b/pkg/oas/oas_response_decoders_gen.go @@ -6105,6 +6105,98 @@ func decodeGetMarketsRatesResponse(resp *http.Response) (res *GetMarketsRatesOK, return res, errors.Wrap(defRes, "error") } +func decodeGetMigrationWalletsResponse(resp *http.Response) (res *MigrationWallets, _ 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 MigrationWallets + 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 decodeGetMultisigAccountResponse(resp *http.Response) (res *Multisig, _ error) { switch resp.StatusCode { case 200: @@ -9923,6 +10015,98 @@ func decodeGetWalletsByPublicKeyBulkResponse(resp *http.Response) (res *WalletsB return res, errors.Wrap(defRes, "error") } +func decodePrepareMigrationResponse(resp *http.Response) (res *MigrationPrepareResponse, _ 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 MigrationPrepareResponse + 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 decodeReindexAccountResponse(resp *http.Response) (res *ReindexAccountOK, _ 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 5a21c0b0..8c58bdd4 100644 --- a/pkg/oas/oas_response_encoders_gen.go +++ b/pkg/oas/oas_response_encoders_gen.go @@ -975,6 +975,20 @@ func encodeGetMarketsRatesResponse(response *GetMarketsRatesOK, w http.ResponseW return nil } +func encodeGetMigrationWalletsResponse(response *MigrationWallets, 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 encodeGetMultisigAccountResponse(response *Multisig, w http.ResponseWriter, span trace.Span) error { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(200) @@ -1581,6 +1595,20 @@ func encodeGetWalletsByPublicKeyBulkResponse(response *WalletsByPublicKeys, w ht return nil } +func encodePrepareMigrationResponse(response *MigrationPrepareResponse, 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 encodeReindexAccountResponse(response *ReindexAccountOK, w http.ResponseWriter, span trace.Span) error { w.WriteHeader(200) span.SetStatus(codes.Ok, http.StatusText(200)) diff --git a/pkg/oas/oas_router_gen.go b/pkg/oas/oas_router_gen.go index aa8418a4..4fd70e0f 100644 --- a/pkg/oas/oas_router_gen.go +++ b/pkg/oas/oas_router_gen.go @@ -38,7 +38,7 @@ var ( rn27AllowedHeaders = map[string]string{ "POST": "Content-Type", } - rn196AllowedHeaders = map[string]string{ + rn199AllowedHeaders = map[string]string{ "POST": "Content-Type", } rn22AllowedHeaders = map[string]string{ @@ -59,28 +59,34 @@ var ( rn107AllowedHeaders = map[string]string{ "POST": "Content-Type", } - rn197AllowedHeaders = map[string]string{ + rn200AllowedHeaders = map[string]string{ "POST": "Content-Type", } rn12AllowedHeaders = map[string]string{ "POST": "Content-Type", } - rn127AllowedHeaders = map[string]string{ + rn196AllowedHeaders = map[string]string{ + "POST": "Content-Type", + } + rn118AllowedHeaders = map[string]string{ + "POST": "Content-Type", + } + rn128AllowedHeaders = map[string]string{ "POST": "Content-Type", } - rn122AllowedHeaders = map[string]string{ + rn123AllowedHeaders = map[string]string{ "POST": "Content-Type", } - rn126AllowedHeaders = map[string]string{ + rn127AllowedHeaders = map[string]string{ "GET": "Accept-Language", } - rn193AllowedHeaders = map[string]string{ + rn194AllowedHeaders = map[string]string{ "POST": "Content-Type", } - rn176AllowedHeaders = map[string]string{ + rn177AllowedHeaders = map[string]string{ "GET": "Accept-Language", } - rn179AllowedHeaders = map[string]string{ + rn180AllowedHeaders = map[string]string{ "GET": "Accept-Language", } rn46AllowedHeaders = map[string]string{ @@ -89,7 +95,7 @@ var ( rn23AllowedHeaders = map[string]string{ "POST": "Content-Type", } - rn200AllowedHeaders = map[string]string{ + rn203AllowedHeaders = map[string]string{ "POST": "Content-Type", } rn24AllowedHeaders = map[string]string{ @@ -1557,7 +1563,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn196AllowedHeaders, + allowedHeaders: rn199AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -2938,7 +2944,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn197AllowedHeaders, + allowedHeaders: rn200AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -2986,6 +2992,70 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + case 'i': // Prefix: "igration/" + + if l := len("igration/"); len(elem) >= l && elem[0:l] == "igration/" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'p': // Prefix: "prepare" + + if l := len("prepare"); len(elem) >= l && elem[0:l] == "prepare" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "POST": + s.handlePrepareMigrationRequest([0]string{}, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, notAllowedParams{ + allowedMethods: "POST", + allowedHeaders: rn196AllowedHeaders, + acceptPost: "application/json", + acceptPatch: "", + }) + } + + return + } + + case 'w': // Prefix: "wallets" + + if l := len("wallets"); len(elem) >= l && elem[0:l] == "wallets" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "POST": + s.handleGetMigrationWalletsRequest([0]string{}, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, notAllowedParams{ + allowedMethods: "POST", + allowedHeaders: rn118AllowedHeaders, + acceptPost: "application/json", + acceptPatch: "", + }) + } + + return + } + + } + case 'u': // Prefix: "ultisig/" if l := len("ultisig/"); len(elem) >= l && elem[0:l] == "ultisig/" { @@ -3094,7 +3164,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn127AllowedHeaders, + allowedHeaders: rn128AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -3156,7 +3226,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn122AllowedHeaders, + allowedHeaders: rn123AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -3272,7 +3342,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn126AllowedHeaders, + allowedHeaders: rn127AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -3387,7 +3457,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn193AllowedHeaders, + allowedHeaders: rn194AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -3873,7 +3943,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn176AllowedHeaders, + allowedHeaders: rn177AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -3927,7 +3997,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "GET", - allowedHeaders: rn179AllowedHeaders, + allowedHeaders: rn180AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -4167,7 +4237,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn200AllowedHeaders, + allowedHeaders: rn203AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -7081,6 +7151,70 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { } } + case 'i': // Prefix: "igration/" + + if l := len("igration/"); len(elem) >= l && elem[0:l] == "igration/" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'p': // Prefix: "prepare" + + if l := len("prepare"); len(elem) >= l && elem[0:l] == "prepare" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "POST": + r.name = PrepareMigrationOperation + r.summary = "" + r.operationID = "prepareMigration" + r.operationGroup = "" + r.pathPattern = "/v2/migration/prepare" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + + case 'w': // Prefix: "wallets" + + if l := len("wallets"); len(elem) >= l && elem[0:l] == "wallets" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "POST": + r.name = GetMigrationWalletsOperation + r.summary = "" + r.operationID = "getMigrationWallets" + r.operationGroup = "" + r.pathPattern = "/v2/migration/wallets" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + + } + case 'u': // Prefix: "ultisig/" if l := len("ultisig/"); len(elem) >= l && elem[0:l] == "ultisig/" { diff --git a/pkg/oas/oas_schemas_gen.go b/pkg/oas/oas_schemas_gen.go index 21ad8e4b..70af02f1 100644 --- a/pkg/oas/oas_schemas_gen.go +++ b/pkg/oas/oas_schemas_gen.go @@ -7290,6 +7290,20 @@ func (s *GetMarketsRatesOK) SetMarkets(val []MarketTonRates) { s.Markets = val } +type GetMigrationWalletsReq struct { + AccountIds []string `json:"account_ids"` +} + +// GetAccountIds returns the value of AccountIds. +func (s *GetMigrationWalletsReq) GetAccountIds() []string { + return s.AccountIds +} + +// SetAccountIds sets the value of AccountIds. +func (s *GetMigrationWalletsReq) SetAccountIds(val []string) { + s.AccountIds = val +} + type GetNftCollectionItemsByAddressesReq struct { AccountIds []string `json:"account_ids"` } @@ -10359,6 +10373,212 @@ func (s *MethodExecutionResult) SetDecoded(val jx.Raw) { s.Decoded = val } +// Ref: #/components/schemas/MigrationPrepareRequest +type MigrationPrepareRequest struct { + // Legacy source wallet to drain. + From string `json:"from"` + // Destination wallet TON address. + To string `json:"to"` + // Fiat currency for the preview values. + Currency OptString `json:"currency"` +} + +// GetFrom returns the value of From. +func (s *MigrationPrepareRequest) GetFrom() string { + return s.From +} + +// GetTo returns the value of To. +func (s *MigrationPrepareRequest) GetTo() string { + return s.To +} + +// GetCurrency returns the value of Currency. +func (s *MigrationPrepareRequest) GetCurrency() OptString { + return s.Currency +} + +// SetFrom sets the value of From. +func (s *MigrationPrepareRequest) SetFrom(val string) { + s.From = val +} + +// SetTo sets the value of To. +func (s *MigrationPrepareRequest) SetTo(val string) { + s.To = val +} + +// SetCurrency sets the value of Currency. +func (s *MigrationPrepareRequest) SetCurrency(val OptString) { + s.Currency = val +} + +// Ref: #/components/schemas/MigrationPrepareResponse +type MigrationPrepareResponse struct { + From string `json:"from"` + To string `json:"to"` + // Source wallet version (informational). + WalletVersion string `json:"wallet_version"` + // Ordered; sign and broadcast in array order, one entry per external message. + Transactions []MigrationTransaction `json:"transactions"` +} + +// GetFrom returns the value of From. +func (s *MigrationPrepareResponse) GetFrom() string { + return s.From +} + +// GetTo returns the value of To. +func (s *MigrationPrepareResponse) GetTo() string { + return s.To +} + +// GetWalletVersion returns the value of WalletVersion. +func (s *MigrationPrepareResponse) GetWalletVersion() string { + return s.WalletVersion +} + +// GetTransactions returns the value of Transactions. +func (s *MigrationPrepareResponse) GetTransactions() []MigrationTransaction { + return s.Transactions +} + +// SetFrom sets the value of From. +func (s *MigrationPrepareResponse) SetFrom(val string) { + s.From = val +} + +// SetTo sets the value of To. +func (s *MigrationPrepareResponse) SetTo(val string) { + s.To = val +} + +// SetWalletVersion sets the value of WalletVersion. +func (s *MigrationPrepareResponse) SetWalletVersion(val string) { + s.WalletVersion = val +} + +// SetTransactions sets the value of Transactions. +func (s *MigrationPrepareResponse) SetTransactions(val []MigrationTransaction) { + s.Transactions = val +} + +// Ref: #/components/schemas/MigrationTransaction +type MigrationTransaction struct { + // Wallet seqno baked into the unsigned body. + Seqno int32 `json:"seqno"` + // Base64 BOC of the unsigned wallet body. Sign its hash, prepend the signature, wrap it in an + // external message and broadcast. + Boc string `json:"boc"` + Emulation MessageConsequences `json:"emulation"` +} + +// GetSeqno returns the value of Seqno. +func (s *MigrationTransaction) GetSeqno() int32 { + return s.Seqno +} + +// GetBoc returns the value of Boc. +func (s *MigrationTransaction) GetBoc() string { + return s.Boc +} + +// GetEmulation returns the value of Emulation. +func (s *MigrationTransaction) GetEmulation() MessageConsequences { + return s.Emulation +} + +// SetSeqno sets the value of Seqno. +func (s *MigrationTransaction) SetSeqno(val int32) { + s.Seqno = val +} + +// SetBoc sets the value of Boc. +func (s *MigrationTransaction) SetBoc(val string) { + s.Boc = val +} + +// SetEmulation sets the value of Emulation. +func (s *MigrationTransaction) SetEmulation(val MessageConsequences) { + s.Emulation = val +} + +// Ref: #/components/schemas/MigrationWalletValue +type MigrationWalletValue struct { + Account string `json:"account"` + // TON balance in nanotons. + Balance int64 `json:"balance"` + Status AccountStatus `json:"status"` + Jettons []JettonBalance `json:"jettons"` + // Number of NFTs owned by the account. + NftCount int32 `json:"nft_count"` +} + +// GetAccount returns the value of Account. +func (s *MigrationWalletValue) GetAccount() string { + return s.Account +} + +// GetBalance returns the value of Balance. +func (s *MigrationWalletValue) GetBalance() int64 { + return s.Balance +} + +// GetStatus returns the value of Status. +func (s *MigrationWalletValue) GetStatus() AccountStatus { + return s.Status +} + +// GetJettons returns the value of Jettons. +func (s *MigrationWalletValue) GetJettons() []JettonBalance { + return s.Jettons +} + +// GetNftCount returns the value of NftCount. +func (s *MigrationWalletValue) GetNftCount() int32 { + return s.NftCount +} + +// SetAccount sets the value of Account. +func (s *MigrationWalletValue) SetAccount(val string) { + s.Account = val +} + +// SetBalance sets the value of Balance. +func (s *MigrationWalletValue) SetBalance(val int64) { + s.Balance = val +} + +// SetStatus sets the value of Status. +func (s *MigrationWalletValue) SetStatus(val AccountStatus) { + s.Status = val +} + +// SetJettons sets the value of Jettons. +func (s *MigrationWalletValue) SetJettons(val []JettonBalance) { + s.Jettons = val +} + +// SetNftCount sets the value of NftCount. +func (s *MigrationWalletValue) SetNftCount(val int32) { + s.NftCount = val +} + +// Ref: #/components/schemas/MigrationWallets +type MigrationWallets struct { + Wallets []MigrationWalletValue `json:"wallets"` +} + +// GetWallets returns the value of Wallets. +func (s *MigrationWallets) GetWallets() []MigrationWalletValue { + return s.Wallets +} + +// SetWallets sets the value of Wallets. +func (s *MigrationWallets) SetWallets(val []MigrationWalletValue) { + s.Wallets = val +} + // Ref: #/components/schemas/MisbehaviourPunishmentConfig type MisbehaviourPunishmentConfig struct { DefaultFlatFine int64 `json:"default_flat_fine"` @@ -14785,6 +15005,52 @@ func (o OptGetJettonInfosByAddressesReq) Or(d GetJettonInfosByAddressesReq) GetJ return d } +// NewOptGetMigrationWalletsReq returns new OptGetMigrationWalletsReq with value set to v. +func NewOptGetMigrationWalletsReq(v GetMigrationWalletsReq) OptGetMigrationWalletsReq { + return OptGetMigrationWalletsReq{ + Value: v, + Set: true, + } +} + +// OptGetMigrationWalletsReq is optional GetMigrationWalletsReq. +type OptGetMigrationWalletsReq struct { + Value GetMigrationWalletsReq + Set bool +} + +// IsSet returns true if OptGetMigrationWalletsReq was set. +func (o OptGetMigrationWalletsReq) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptGetMigrationWalletsReq) Reset() { + var v GetMigrationWalletsReq + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptGetMigrationWalletsReq) SetTo(v GetMigrationWalletsReq) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptGetMigrationWalletsReq) Get() (v GetMigrationWalletsReq, 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 OptGetMigrationWalletsReq) Or(d GetMigrationWalletsReq) GetMigrationWalletsReq { + if v, ok := o.Get(); ok { + return v + } + return d +} + // NewOptGetNftCollectionItemsByAddressesReq returns new OptGetNftCollectionItemsByAddressesReq with value set to v. func NewOptGetNftCollectionItemsByAddressesReq(v GetNftCollectionItemsByAddressesReq) OptGetNftCollectionItemsByAddressesReq { return OptGetNftCollectionItemsByAddressesReq{ diff --git a/pkg/oas/oas_server_gen.go b/pkg/oas/oas_server_gen.go index 1aefc969..efb3cead 100644 --- a/pkg/oas/oas_server_gen.go +++ b/pkg/oas/oas_server_gen.go @@ -429,6 +429,13 @@ type Handler interface { // // GET /v2/rates/markets GetMarketsRates(ctx context.Context) (*GetMarketsRatesOK, error) + // GetMigrationWallets implements getMigrationWallets operation. + // + // Get migratable assets value (TON balance, jettons with prices, NFT count) for several wallets at + // once. + // + // POST /v2/migration/wallets + GetMigrationWallets(ctx context.Context, req OptGetMigrationWalletsReq, params GetMigrationWalletsParams) (*MigrationWallets, error) // GetMultisigAccount implements getMultisigAccount operation. // // Get multisig account info. @@ -693,6 +700,12 @@ type Handler interface { // // POST /v2/pubkeys/wallets/_bulk GetWalletsByPublicKeyBulk(ctx context.Context, req OptGetWalletsByPublicKeyBulkReq) (*WalletsByPublicKeys, error) + // PrepareMigration implements prepareMigration operation. + // + // Prepare ordered signable transactions that migrate every asset from `from` to `to`. + // + // POST /v2/migration/prepare + PrepareMigration(ctx context.Context, req *MigrationPrepareRequest) (*MigrationPrepareResponse, error) // ReindexAccount implements reindexAccount operation. // // Update internal cache for a particular account. diff --git a/pkg/oas/oas_unimplemented_gen.go b/pkg/oas/oas_unimplemented_gen.go index e6ad89ad..cca45c26 100644 --- a/pkg/oas/oas_unimplemented_gen.go +++ b/pkg/oas/oas_unimplemented_gen.go @@ -634,6 +634,16 @@ func (UnimplementedHandler) GetMarketsRates(ctx context.Context) (r *GetMarketsR return r, ht.ErrNotImplemented } +// GetMigrationWallets implements getMigrationWallets operation. +// +// Get migratable assets value (TON balance, jettons with prices, NFT count) for several wallets at +// once. +// +// POST /v2/migration/wallets +func (UnimplementedHandler) GetMigrationWallets(ctx context.Context, req OptGetMigrationWalletsReq, params GetMigrationWalletsParams) (r *MigrationWallets, _ error) { + return r, ht.ErrNotImplemented +} + // GetMultisigAccount implements getMultisigAccount operation. // // Get multisig account info. @@ -1027,6 +1037,15 @@ func (UnimplementedHandler) GetWalletsByPublicKeyBulk(ctx context.Context, req O return r, ht.ErrNotImplemented } +// PrepareMigration implements prepareMigration operation. +// +// Prepare ordered signable transactions that migrate every asset from `from` to `to`. +// +// POST /v2/migration/prepare +func (UnimplementedHandler) PrepareMigration(ctx context.Context, req *MigrationPrepareRequest) (r *MigrationPrepareResponse, _ error) { + return r, ht.ErrNotImplemented +} + // ReindexAccount implements reindexAccount operation. // // Update internal cache for a particular account. diff --git a/pkg/oas/oas_validators_gen.go b/pkg/oas/oas_validators_gen.go index 40a6e0b9..e7ef2f2c 100644 --- a/pkg/oas/oas_validators_gen.go +++ b/pkg/oas/oas_validators_gen.go @@ -3263,6 +3263,29 @@ func (s *GetMarketsRatesOK) Validate() error { return nil } +func (s *GetMigrationWalletsReq) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if s.AccountIds == nil { + return errors.New("nil is invalid value") + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "account_ids", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + func (s *GetNftCollectionItemsByAddressesReq) Validate() error { if s == nil { return validate.ErrNilPointer @@ -4464,6 +4487,160 @@ func (s *MethodExecutionResult) Validate() error { return nil } +func (s *MigrationPrepareResponse) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if s.Transactions == nil { + return errors.New("nil is invalid value") + } + var failures []validate.FieldError + for i, elem := range s.Transactions { + 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: "transactions", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *MigrationTransaction) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Emulation.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "emulation", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *MigrationWalletValue) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Status.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "status", + Error: err, + }) + } + if err := func() error { + if s.Jettons == nil { + return errors.New("nil is invalid value") + } + var failures []validate.FieldError + for i, elem := range s.Jettons { + 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: "jettons", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *MigrationWallets) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if s.Wallets == nil { + return errors.New("nil is invalid value") + } + var failures []validate.FieldError + for i, elem := range s.Wallets { + 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: "wallets", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + func (s *Multisig) Validate() error { if s == nil { return validate.ErrNilPointer