From f7cc8476833a468eafde4cc3bfd4c4354923f427 Mon Sep 17 00:00:00 2001 From: Aaron Date: Fri, 17 Jan 2025 10:06:58 +0800 Subject: [PATCH] [api] balance api (#15755) --- Cargo.lock | 1 + api/Cargo.toml | 2 +- api/doc/spec.json | 608 +++++++++++++++++++++++++++++++++ api/doc/spec.yaml | 457 +++++++++++++++++++++++++ api/src/accounts.rs | 153 ++++++++- api/src/tests/accounts_test.rs | 66 ++++ api/types/src/account.rs | 44 ++- api/types/src/derives.rs | 23 +- api/types/src/lib.rs | 2 +- sdk/Cargo.toml | 1 + sdk/src/types.rs | 36 +- 11 files changed, 1382 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 48b27edd192c7..a388f42a93e05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3770,6 +3770,7 @@ dependencies = [ "bcs 0.1.4", "ed25519-dalek-bip32", "hex", + "lazy_static", "move-core-types", "once_cell", "rand 0.7.3", diff --git a/api/Cargo.toml b/api/Cargo.toml index fcc5efdca74ec..4bda07ed5528d 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -25,6 +25,7 @@ aptos-logger = { workspace = true } aptos-mempool = { workspace = true } aptos-metrics-core = { workspace = true } aptos-runtimes = { workspace = true } +aptos-sdk = { workspace = true } aptos-storage-interface = { workspace = true } aptos-types = { workspace = true } aptos-vm = { workspace = true } @@ -55,7 +56,6 @@ aptos-gas-meter = { workspace = true } aptos-gas-schedule = { workspace = true, features = ["testing"] } aptos-move-stdlib = { workspace = true } aptos-proptest-helpers = { workspace = true } -aptos-sdk = { workspace = true } move-package = { workspace = true } passkey-types = { workspace = true } percent-encoding = { workspace = true } diff --git a/api/doc/spec.json b/api/doc/spec.json index f08a1894615ad..5bfa0accdcf43 100644 --- a/api/doc/spec.json +++ b/api/doc/spec.json @@ -1261,6 +1261,608 @@ "operationId": "get_account_resources" } }, + "/accounts/{address}/balance/{asset_type}": { + "get": { + "tags": [ + "Accounts" + ], + "summary": "Get account resources", + "description": "Retrieves all account resources for a given account and a specific ledger version. If the\nledger version is not specified in the request, the latest ledger version is used.\n\nThe Aptos nodes prune account state history, via a configurable time window.\nIf the requested ledger version has been pruned, the server responds with a 410.", + "parameters": [ + { + "name": "address", + "schema": { + "$ref": "#/components/schemas/Address" + }, + "in": "path", + "description": "Address of account with or without a `0x` prefix", + "required": true, + "deprecated": false, + "explode": true + }, + { + "name": "asset_type", + "schema": { + "$ref": "#/components/schemas/AssetType" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + }, + { + "name": "ledger_version", + "schema": { + "$ref": "#/components/schemas/U64" + }, + "in": "query", + "description": "Ledger version to get state of account\n\nIf not provided, it will be the latest version", + "required": false, + "deprecated": false, + "explode": true + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "application/x-bcs": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8" + } + } + } + }, + "headers": { + "X-APTOS-CHAIN-ID": { + "description": "Chain ID of the current chain", + "required": true, + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint8" + } + }, + "X-APTOS-LEDGER-VERSION": { + "description": "Current ledger version of the chain", + "required": true, + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-LEDGER-OLDEST-VERSION": { + "description": "Oldest non-pruned ledger version of the chain", + "required": true, + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-LEDGER-TIMESTAMPUSEC": { + "description": "Current timestamp of the chain", + "required": true, + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-EPOCH": { + "description": "Current epoch of the chain", + "required": true, + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-BLOCK-HEIGHT": { + "description": "Current block height of the chain", + "required": true, + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-OLDEST-BLOCK-HEIGHT": { + "description": "Oldest non-pruned block height of the chain", + "required": true, + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-GAS-USED": { + "description": "The cost of the call in terms of gas", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-CURSOR": { + "description": "Cursor to be used for endpoints that support cursor-based\npagination. Pass this to the `start` field of the endpoint\non the next call to get the next page of results.", + "deprecated": false, + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AptosError" + } + } + }, + "headers": { + "X-APTOS-CHAIN-ID": { + "description": "Chain ID of the current chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint8" + } + }, + "X-APTOS-LEDGER-VERSION": { + "description": "Current ledger version of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-LEDGER-OLDEST-VERSION": { + "description": "Oldest non-pruned ledger version of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-LEDGER-TIMESTAMPUSEC": { + "description": "Current timestamp of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-EPOCH": { + "description": "Current epoch of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-BLOCK-HEIGHT": { + "description": "Current block height of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-OLDEST-BLOCK-HEIGHT": { + "description": "Oldest non-pruned block height of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-GAS-USED": { + "description": "The cost of the call in terms of gas", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AptosError" + } + } + }, + "headers": { + "X-APTOS-CHAIN-ID": { + "description": "Chain ID of the current chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint8" + } + }, + "X-APTOS-LEDGER-VERSION": { + "description": "Current ledger version of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-LEDGER-OLDEST-VERSION": { + "description": "Oldest non-pruned ledger version of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-LEDGER-TIMESTAMPUSEC": { + "description": "Current timestamp of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-EPOCH": { + "description": "Current epoch of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-BLOCK-HEIGHT": { + "description": "Current block height of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-OLDEST-BLOCK-HEIGHT": { + "description": "Oldest non-pruned block height of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-GAS-USED": { + "description": "The cost of the call in terms of gas", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AptosError" + } + } + }, + "headers": { + "X-APTOS-CHAIN-ID": { + "description": "Chain ID of the current chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint8" + } + }, + "X-APTOS-LEDGER-VERSION": { + "description": "Current ledger version of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-LEDGER-OLDEST-VERSION": { + "description": "Oldest non-pruned ledger version of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-LEDGER-TIMESTAMPUSEC": { + "description": "Current timestamp of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-EPOCH": { + "description": "Current epoch of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-BLOCK-HEIGHT": { + "description": "Current block height of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-OLDEST-BLOCK-HEIGHT": { + "description": "Oldest non-pruned block height of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-GAS-USED": { + "description": "The cost of the call in terms of gas", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + } + } + }, + "410": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AptosError" + } + } + }, + "headers": { + "X-APTOS-CHAIN-ID": { + "description": "Chain ID of the current chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint8" + } + }, + "X-APTOS-LEDGER-VERSION": { + "description": "Current ledger version of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-LEDGER-OLDEST-VERSION": { + "description": "Oldest non-pruned ledger version of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-LEDGER-TIMESTAMPUSEC": { + "description": "Current timestamp of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-EPOCH": { + "description": "Current epoch of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-BLOCK-HEIGHT": { + "description": "Current block height of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-OLDEST-BLOCK-HEIGHT": { + "description": "Oldest non-pruned block height of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-GAS-USED": { + "description": "The cost of the call in terms of gas", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AptosError" + } + } + }, + "headers": { + "X-APTOS-CHAIN-ID": { + "description": "Chain ID of the current chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint8" + } + }, + "X-APTOS-LEDGER-VERSION": { + "description": "Current ledger version of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-LEDGER-OLDEST-VERSION": { + "description": "Oldest non-pruned ledger version of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-LEDGER-TIMESTAMPUSEC": { + "description": "Current timestamp of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-EPOCH": { + "description": "Current epoch of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-BLOCK-HEIGHT": { + "description": "Current block height of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-OLDEST-BLOCK-HEIGHT": { + "description": "Oldest non-pruned block height of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-GAS-USED": { + "description": "The cost of the call in terms of gas", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + } + } + }, + "503": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AptosError" + } + } + }, + "headers": { + "X-APTOS-CHAIN-ID": { + "description": "Chain ID of the current chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint8" + } + }, + "X-APTOS-LEDGER-VERSION": { + "description": "Current ledger version of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-LEDGER-OLDEST-VERSION": { + "description": "Oldest non-pruned ledger version of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-LEDGER-TIMESTAMPUSEC": { + "description": "Current timestamp of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-EPOCH": { + "description": "Current epoch of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-BLOCK-HEIGHT": { + "description": "Current block height of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-OLDEST-BLOCK-HEIGHT": { + "description": "Oldest non-pruned block height of the chain", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + }, + "X-APTOS-GAS-USED": { + "description": "The cost of the call in terms of gas", + "deprecated": false, + "schema": { + "type": "integer", + "format": "uint64" + } + } + } + } + }, + "operationId": "get_account_balance" + } + }, "/accounts/{address}/modules": { "get": { "tags": [ @@ -14212,6 +14814,12 @@ "api_disabled" ] }, + "AssetType": { + "type": "string", + "format": "hex", + "description": "A hex encoded 32 byte Aptos account address or a struct tag.\n\nThis is represented in a string as a 64 character hex string, sometimes\nshortened by stripping leading 0s, and adding a 0x or\nFormat: `{address}::{module name}::{struct name}`\n", + "example": "0x1::aptos_coin::AptosCoin" + }, "Block": { "type": "object", "description": "A Block with or without transactions\n\nThis contains the information about a transactions along with\nassociated transactions if requested", diff --git a/api/doc/spec.yaml b/api/doc/spec.yaml index dcf362d6f4fb0..5364dfa20571a 100644 --- a/api/doc/spec.yaml +++ b/api/doc/spec.yaml @@ -933,6 +933,453 @@ paths: type: integer format: uint64 operationId: get_account_resources + /accounts/{address}/balance/{asset_type}: + get: + tags: + - Accounts + summary: Get account resources + description: |- + Retrieves all account resources for a given account and a specific ledger version. If the + ledger version is not specified in the request, the latest ledger version is used. + + The Aptos nodes prune account state history, via a configurable time window. + If the requested ledger version has been pruned, the server responds with a 410. + parameters: + - name: address + schema: + $ref: '#/components/schemas/Address' + in: path + description: Address of account with or without a `0x` prefix + required: true + deprecated: false + explode: true + - name: asset_type + schema: + $ref: '#/components/schemas/AssetType' + in: path + required: true + deprecated: false + explode: true + - name: ledger_version + schema: + $ref: '#/components/schemas/U64' + in: query + description: |- + Ledger version to get state of account + + If not provided, it will be the latest version + required: false + deprecated: false + explode: true + responses: + '200': + description: '' + content: + application/json: + schema: + type: integer + format: uint64 + application/x-bcs: + schema: + type: array + items: + type: integer + format: uint8 + headers: + X-APTOS-CHAIN-ID: + description: Chain ID of the current chain + required: true + deprecated: false + schema: + type: integer + format: uint8 + X-APTOS-LEDGER-VERSION: + description: Current ledger version of the chain + required: true + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-LEDGER-OLDEST-VERSION: + description: Oldest non-pruned ledger version of the chain + required: true + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-LEDGER-TIMESTAMPUSEC: + description: Current timestamp of the chain + required: true + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-EPOCH: + description: Current epoch of the chain + required: true + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-BLOCK-HEIGHT: + description: Current block height of the chain + required: true + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-OLDEST-BLOCK-HEIGHT: + description: Oldest non-pruned block height of the chain + required: true + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-GAS-USED: + description: The cost of the call in terms of gas + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-CURSOR: + description: |- + Cursor to be used for endpoints that support cursor-based + pagination. Pass this to the `start` field of the endpoint + on the next call to get the next page of results. + deprecated: false + schema: + type: string + '400': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/AptosError' + headers: + X-APTOS-CHAIN-ID: + description: Chain ID of the current chain + deprecated: false + schema: + type: integer + format: uint8 + X-APTOS-LEDGER-VERSION: + description: Current ledger version of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-LEDGER-OLDEST-VERSION: + description: Oldest non-pruned ledger version of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-LEDGER-TIMESTAMPUSEC: + description: Current timestamp of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-EPOCH: + description: Current epoch of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-BLOCK-HEIGHT: + description: Current block height of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-OLDEST-BLOCK-HEIGHT: + description: Oldest non-pruned block height of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-GAS-USED: + description: The cost of the call in terms of gas + deprecated: false + schema: + type: integer + format: uint64 + '403': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/AptosError' + headers: + X-APTOS-CHAIN-ID: + description: Chain ID of the current chain + deprecated: false + schema: + type: integer + format: uint8 + X-APTOS-LEDGER-VERSION: + description: Current ledger version of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-LEDGER-OLDEST-VERSION: + description: Oldest non-pruned ledger version of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-LEDGER-TIMESTAMPUSEC: + description: Current timestamp of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-EPOCH: + description: Current epoch of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-BLOCK-HEIGHT: + description: Current block height of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-OLDEST-BLOCK-HEIGHT: + description: Oldest non-pruned block height of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-GAS-USED: + description: The cost of the call in terms of gas + deprecated: false + schema: + type: integer + format: uint64 + '404': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/AptosError' + headers: + X-APTOS-CHAIN-ID: + description: Chain ID of the current chain + deprecated: false + schema: + type: integer + format: uint8 + X-APTOS-LEDGER-VERSION: + description: Current ledger version of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-LEDGER-OLDEST-VERSION: + description: Oldest non-pruned ledger version of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-LEDGER-TIMESTAMPUSEC: + description: Current timestamp of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-EPOCH: + description: Current epoch of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-BLOCK-HEIGHT: + description: Current block height of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-OLDEST-BLOCK-HEIGHT: + description: Oldest non-pruned block height of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-GAS-USED: + description: The cost of the call in terms of gas + deprecated: false + schema: + type: integer + format: uint64 + '410': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/AptosError' + headers: + X-APTOS-CHAIN-ID: + description: Chain ID of the current chain + deprecated: false + schema: + type: integer + format: uint8 + X-APTOS-LEDGER-VERSION: + description: Current ledger version of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-LEDGER-OLDEST-VERSION: + description: Oldest non-pruned ledger version of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-LEDGER-TIMESTAMPUSEC: + description: Current timestamp of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-EPOCH: + description: Current epoch of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-BLOCK-HEIGHT: + description: Current block height of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-OLDEST-BLOCK-HEIGHT: + description: Oldest non-pruned block height of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-GAS-USED: + description: The cost of the call in terms of gas + deprecated: false + schema: + type: integer + format: uint64 + '500': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/AptosError' + headers: + X-APTOS-CHAIN-ID: + description: Chain ID of the current chain + deprecated: false + schema: + type: integer + format: uint8 + X-APTOS-LEDGER-VERSION: + description: Current ledger version of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-LEDGER-OLDEST-VERSION: + description: Oldest non-pruned ledger version of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-LEDGER-TIMESTAMPUSEC: + description: Current timestamp of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-EPOCH: + description: Current epoch of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-BLOCK-HEIGHT: + description: Current block height of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-OLDEST-BLOCK-HEIGHT: + description: Oldest non-pruned block height of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-GAS-USED: + description: The cost of the call in terms of gas + deprecated: false + schema: + type: integer + format: uint64 + '503': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/AptosError' + headers: + X-APTOS-CHAIN-ID: + description: Chain ID of the current chain + deprecated: false + schema: + type: integer + format: uint8 + X-APTOS-LEDGER-VERSION: + description: Current ledger version of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-LEDGER-OLDEST-VERSION: + description: Oldest non-pruned ledger version of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-LEDGER-TIMESTAMPUSEC: + description: Current timestamp of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-EPOCH: + description: Current epoch of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-BLOCK-HEIGHT: + description: Current block height of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-OLDEST-BLOCK-HEIGHT: + description: Oldest non-pruned block height of the chain + deprecated: false + schema: + type: integer + format: uint64 + X-APTOS-GAS-USED: + description: The cost of the call in terms of gas + deprecated: false + schema: + type: integer + format: uint64 + operationId: get_account_balance /accounts/{address}/modules: get: tags: @@ -10616,6 +11063,16 @@ components: - web_framework_error - bcs_not_supported - api_disabled + AssetType: + type: string + format: hex + description: | + A hex encoded 32 byte Aptos account address or a struct tag. + + This is represented in a string as a 64 character hex string, sometimes + shortened by stripping leading 0s, and adding a 0x or + Format: `{address}::{module name}::{struct name}` + example: 0x1::aptos_coin::AptosCoin Block: type: object description: |- diff --git a/api/src/accounts.rs b/api/src/accounts.rs index d94454f6b34e3..4427f01b2f2c7 100644 --- a/api/src/accounts.rs +++ b/api/src/accounts.rs @@ -15,11 +15,15 @@ use crate::{ }; use anyhow::Context as AnyhowContext; use aptos_api_types::{ - AccountData, Address, AptosErrorCode, AsConverter, LedgerInfo, MoveModuleBytecode, + AccountData, Address, AptosErrorCode, AsConverter, AssetType, LedgerInfo, MoveModuleBytecode, MoveModuleId, MoveResource, MoveStructTag, StateKeyWrapper, U64, }; +use aptos_sdk::types::{get_paired_fa_metadata_address, get_paired_fa_primary_store_address}; use aptos_types::{ - account_config::{AccountResource, ObjectGroupResource}, + account_config::{ + AccountResource, CoinStoreResourceUntyped, ConcurrentFungibleBalanceResource, + FungibleStoreResource, ObjectGroupResource, + }, event::{EventHandle, EventKey}, state_store::state_key::StateKey, }; @@ -30,7 +34,7 @@ use poem_openapi::{ param::{Path, Query}, OpenApi, }; -use std::{collections::BTreeMap, convert::TryInto, sync::Arc}; +use std::{collections::BTreeMap, convert::TryInto, str::FromStr, sync::Arc}; /// API for accounts, their associated resources, and modules pub struct AccountsApi { @@ -124,6 +128,42 @@ impl AccountsApi { .await } + /// Get account resources + /// + /// Retrieves all account resources for a given account and a specific ledger version. If the + /// ledger version is not specified in the request, the latest ledger version is used. + /// + /// The Aptos nodes prune account state history, via a configurable time window. + /// If the requested ledger version has been pruned, the server responds with a 410. + #[oai( + path = "/accounts/:address/balance/:asset_type", + method = "get", + operation_id = "get_account_balance", + tag = "ApiTags::Accounts" + )] + async fn get_account_balance( + &self, + accept_type: AcceptType, + /// Address of account with or without a `0x` prefix + address: Path
, + asset_type: Path, + /// Ledger version to get state of account + /// + /// If not provided, it will be the latest version + ledger_version: Query>, + ) -> BasicResultWith404 { + fail_point_poem("endpoint_get_account_balance")?; + self.context + .check_api_output_enabled("Get account balance", &accept_type)?; + + let context = self.context.clone(); + api_spawn_blocking(move || { + let account = Account::new(context, address.0, ledger_version.0, None, None)?; + account.balance(asset_type.0, &accept_type) + }) + .await + } + /// Get account modules /// /// Retrieves all account modules' bytecode for a given account at a specific ledger version. @@ -251,6 +291,113 @@ impl Account { } } + pub fn balance( + &self, + asset_type: AssetType, + accept_type: &AcceptType, + ) -> BasicResultWith404 { + let (fa_metadata_address, mut balance) = match asset_type { + AssetType::Coin(move_struct_tag) => { + let coin_store_type_tag = + StructTag::from_str(&format!("0x1::coin::CoinStore<{}>", move_struct_tag)) + .map_err(|err| { + BasicErrorWith404::internal_with_code( + err, + AptosErrorCode::InternalError, + &self.latest_ledger_info, + ) + })?; + // query coin balance + let state_value = self.context.get_state_value_poem( + &StateKey::resource(&self.address.into(), &coin_store_type_tag).map_err( + |err| { + BasicErrorWith404::internal_with_code( + err, + AptosErrorCode::InternalError, + &self.latest_ledger_info, + ) + }, + )?, + self.ledger_version, + &self.latest_ledger_info, + )?; + let coin_balance = match state_value { + None => 0, + Some(bytes) => bcs::from_bytes::(&bytes) + .map_err(|err| { + BasicErrorWith404::internal_with_code( + err, + AptosErrorCode::InternalError, + &self.latest_ledger_info, + ) + })? + .coin(), + }; + ( + get_paired_fa_metadata_address(&move_struct_tag), + coin_balance, + ) + }, + AssetType::FungibleAsset(fa_metadata_adddress) => (fa_metadata_adddress.into(), 0), + }; + let primary_fungible_store_address = + get_paired_fa_primary_store_address(self.address.into(), fa_metadata_address); + if let Some(data_blob) = self.context.get_state_value_poem( + &StateKey::resource_group( + &primary_fungible_store_address, + &ObjectGroupResource::struct_tag(), + ), + self.ledger_version, + &self.latest_ledger_info, + )? { + if let Ok(object_group) = bcs::from_bytes::(&data_blob) { + if let Some(fa_store) = object_group.group.get(&FungibleStoreResource::struct_tag()) + { + let fa_store_resource = bcs::from_bytes::(fa_store) + .map_err(|err| { + BasicErrorWith404::internal_with_code( + err, + AptosErrorCode::InternalError, + &self.latest_ledger_info, + ) + })?; + if fa_store_resource.balance != 0 { + balance += fa_store_resource.balance(); + } else if let Some(concurrent_fa_balance) = object_group + .group + .get(&ConcurrentFungibleBalanceResource::struct_tag()) + { + // query potential concurrent fa balance + let concurrent_fa_balance_resource = + bcs::from_bytes::( + concurrent_fa_balance, + ) + .map_err(|err| { + BasicErrorWith404::internal_with_code( + err, + AptosErrorCode::InternalError, + &self.latest_ledger_info, + ) + })?; + balance += concurrent_fa_balance_resource.balance(); + } + } + } + } + match accept_type { + AcceptType::Json => BasicResponse::try_from_json(( + balance, + &self.latest_ledger_info, + BasicResponseStatus::Ok, + )), + AcceptType::Bcs => BasicResponse::try_from_encoded(( + bcs::to_bytes(&balance).unwrap(), + &self.latest_ledger_info, + BasicResponseStatus::Ok, + )), + } + } + pub fn get_account_resource(&self) -> Result, BasicErrorWith404> { let state_key = StateKey::resource_typed::(self.address.inner()).map_err(|e| { diff --git a/api/src/tests/accounts_test.rs b/api/src/tests/accounts_test.rs index b5bac410dc0c9..676dede993396 100644 --- a/api/src/tests/accounts_test.rs +++ b/api/src/tests/accounts_test.rs @@ -7,6 +7,18 @@ use crate::tests::new_test_context_with_db_sharding_and_internal_indexer; use aptos_api_test_context::{current_function_name, find_value, TestContext}; use aptos_api_types::{MoveModuleBytecode, MoveResource, MoveStructTag, StateKeyWrapper}; use aptos_cached_packages::aptos_stdlib; +use aptos_sdk::types::APTOS_COIN_TYPE_STR; +use aptos_types::{ + account_config::{primary_apt_store, ObjectCoreResource}, + transaction::{EntryFunction, TransactionPayload}, + AptosCoinType, CoinType, +}; +use move_core_types::{ + account_address::AccountAddress, + identifier::Identifier, + language_storage::{ModuleId, TypeTag}, + move_resource::MoveStructType, +}; use serde_json::json; use std::str::FromStr; @@ -179,6 +191,56 @@ async fn test_get_account_resources_by_invalid_ledger_version() { context.check_golden_output(resp); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_account_balance() { + let mut context = new_test_context(current_function_name!()); + let root_account = context.root_account().await; + let coin_balance_before = context + .get(&account_balance( + &root_account.address().to_hex_literal(), + APTOS_COIN_TYPE_STR, + )) + .await; + let txn = root_account.sign_with_transaction_builder(context.transaction_factory().payload( + aptos_stdlib::coin_migrate_to_fungible_store(AptosCoinType::type_tag()), + )); + context.commit_block(&vec![txn.clone()]).await; + let coin_balance_after = context + .get(&account_balance( + &root_account.address().to_hex_literal(), + APTOS_COIN_TYPE_STR, + )) + .await; + assert_eq!(coin_balance_before, coin_balance_after); + let fa_balance = context + .get(&account_balance( + &root_account.address().to_hex_literal(), + &AccountAddress::TEN.to_hex_literal(), + )) + .await; + assert_eq!(coin_balance_after, fa_balance); + // upgrade to concurrent store + let txn = root_account.sign_with_transaction_builder(context.transaction_factory().payload( + TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::TEN, + Identifier::new("fungible_asset").unwrap(), + ), + Identifier::new("upgrade_store_to_concurrent").unwrap(), + vec![TypeTag::Struct(Box::new(ObjectCoreResource::struct_tag()))], + vec![bcs::to_bytes(&primary_apt_store(root_account.address())).unwrap()], + )), + )); + context.commit_block(&vec![txn.clone()]).await; + let concurrent_fa_balance = context + .get(&account_balance( + &root_account.address().to_hex_literal(), + &AccountAddress::TEN.to_hex_literal(), + )) + .await; + assert_eq!(concurrent_fa_balance, fa_balance); +} + async fn test_get_account_modules_by_ledger_version_with_context(mut context: TestContext) { let payload = aptos_stdlib::publish_module_source("test_module", "module 0xa550c18::test_module {}"); @@ -425,6 +487,10 @@ fn account_modules(address: &str) -> String { format!("/accounts/{}/modules", address) } +fn account_balance(address: &str, coin_type: &str) -> String { + format!("/accounts/{}/balance/{}", address, coin_type) +} + fn account_modules_with_ledger_version(address: &str, ledger_version: i128) -> String { format!( "{}?ledger_version={}", diff --git a/api/types/src/account.rs b/api/types/src/account.rs index f232248e3af37..9a2f75efcd10a 100644 --- a/api/types/src/account.rs +++ b/api/types/src/account.rs @@ -2,10 +2,11 @@ // Parts of the project are originally copyright © Meta Platforms, Inc. // SPDX-License-Identifier: Apache-2.0 -use crate::{HexEncodedBytes, U64}; +use crate::{Address, HexEncodedBytes, MoveStructTag, U64}; use aptos_types::account_config::AccountResource; use poem_openapi::Object; -use serde::{Deserialize, Serialize}; +use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; +use std::{fmt::Debug, str::FromStr}; /// Account data /// @@ -25,3 +26,42 @@ impl From for AccountData { } } } + +/// An Enum for referencing an asset type, either coin or fungible asset. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum AssetType { + Coin(MoveStructTag), + FungibleAsset(Address), +} + +impl FromStr for AssetType { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match Address::from_str(s) { + Ok(address) => Ok(AssetType::FungibleAsset(address)), + Err(_) => match MoveStructTag::from_str(s) { + Ok(tag) => Ok(AssetType::Coin(tag)), + Err(e) => Err(e), + }, + } + } +} +impl Serialize for AssetType { + fn serialize(&self, serializer: S) -> Result { + match self { + Self::Coin(struct_tag) => MoveStructTag::serialize(struct_tag, serializer), + Self::FungibleAsset(addr) => Address::serialize(addr, serializer), + } + } +} + +impl<'de> Deserialize<'de> for AssetType { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let data = ::deserialize(deserializer)?; + data.parse().map_err(D::Error::custom) + } +} diff --git a/api/types/src/derives.rs b/api/types/src/derives.rs index 8964633db43ef..06a3db2596fcd 100644 --- a/api/types/src/derives.rs +++ b/api/types/src/derives.rs @@ -20,8 +20,8 @@ use crate::{ move_types::{MoveAbility, MoveStructValue}, - Address, EntryFunctionId, HashValue, HexEncodedBytes, IdentifierWrapper, MoveModuleId, - MoveStructTag, MoveType, StateKeyWrapper, U128, U256, U64, + Address, AssetType, EntryFunctionId, HashValue, HexEncodedBytes, IdentifierWrapper, + MoveModuleId, MoveStructTag, MoveType, StateKeyWrapper, U128, U256, U64, }; use aptos_openapi::{impl_poem_parameter, impl_poem_type}; use indoc::indoc; @@ -46,6 +46,24 @@ impl_poem_type!( ) ); +impl_poem_type!( + AssetType, + "string", + ( + example = Some(serde_json::Value::String( + "0x1::aptos_coin::AptosCoin".to_string() + )), + format = Some("hex"), + description = Some(indoc! {" + A hex encoded 32 byte Aptos account address or a struct tag. + + This is represented in a string as a 64 character hex string, sometimes + shortened by stripping leading 0s, and adding a 0x or + Format: `{address}::{module name}::{struct name}` + "}) + ) +); + impl_poem_type!( EntryFunctionId, "string", @@ -293,6 +311,7 @@ impl_poem_type!( impl_poem_parameter!( Address, + AssetType, HashValue, IdentifierWrapper, HexEncodedBytes, diff --git a/api/types/src/lib.rs b/api/types/src/lib.rs index 5c9abbfcc26c3..0fc27da27a0a0 100644 --- a/api/types/src/lib.rs +++ b/api/types/src/lib.rs @@ -23,7 +23,7 @@ pub mod transaction; mod view; mod wrappers; -pub use account::AccountData; +pub use account::{AccountData, AssetType}; pub use address::Address; pub use block::{BcsBlock, Block}; pub use bytecode::Bytecode; diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 2bcd8106bca34..3fdb5c0417232 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -24,6 +24,7 @@ base64 = { workspace = true } bcs = { workspace = true } ed25519-dalek-bip32 = { workspace = true } hex = { workspace = true } +lazy_static = "1.4.0" move-core-types = { workspace = true } rand = { workspace = true } rand_core = { workspace = true } diff --git a/sdk/src/types.rs b/sdk/src/types.rs index 47f1bd6cd2671..d60374bd0b96f 100644 --- a/sdk/src/types.rs +++ b/sdk/src/types.rs @@ -16,9 +16,9 @@ use crate::{ }, }; use anyhow::{Context, Result}; -use aptos_crypto::{ed25519::Ed25519Signature, secp256r1_ecdsa, PrivateKey, SigningKey}; +use aptos_crypto::{ed25519::Ed25519Signature, secp256r1_ecdsa, HashValue, PrivateKey, SigningKey}; use aptos_ledger::AptosLedgerError; -use aptos_rest_client::{Client, PepperRequest, ProverRequest}; +use aptos_rest_client::{aptos_api_types::MoveStructTag, Client, PepperRequest, ProverRequest}; pub use aptos_types::*; use aptos_types::{ event::EventKey, @@ -35,6 +35,7 @@ use aptos_types::{ use bip39::{Language, Mnemonic, Seed}; use ed25519_dalek_bip32::{DerivationPath, ExtendedSecretKey}; use keyless::FederatedKeylessPublicKey; +use lazy_static::lazy_static; use rand::Rng; use serde::{Deserialize, Serialize}; use std::{ @@ -47,6 +48,15 @@ use std::{ time::{Duration, SystemTime, UNIX_EPOCH}, }; +pub const APTOS_COIN_TYPE_STR: &str = "0x1::aptos_coin::AptosCoin"; +lazy_static! { + pub static ref APT_METADATA_ADDRESS: AccountAddress = { + let mut addr = [0u8; 32]; + addr[31] = 10u8; + AccountAddress::new(addr) + }; +} + #[derive(Debug)] enum LocalAccountAuthenticator { PrivateKey(AccountKey), @@ -128,6 +138,28 @@ pub fn get_apt_primary_store_address(address: AccountAddress) -> AccountAddress AccountAddress::from_bytes(aptos_crypto::hash::HashValue::sha3_256_of(&bytes).to_vec()).unwrap() } +pub fn get_paired_fa_primary_store_address( + address: AccountAddress, + fa_metadata_address: AccountAddress, +) -> AccountAddress { + let mut bytes = address.to_vec(); + bytes.append(&mut fa_metadata_address.to_vec()); + bytes.push(0xFC); + AccountAddress::from_bytes(aptos_crypto::hash::HashValue::sha3_256_of(&bytes).to_vec()).unwrap() +} + +pub fn get_paired_fa_metadata_address(coin_type_name: &MoveStructTag) -> AccountAddress { + let coin_type_name = coin_type_name.to_string(); + if coin_type_name == APTOS_COIN_TYPE_STR { + *APT_METADATA_ADDRESS + } else { + let mut preimage = APT_METADATA_ADDRESS.to_vec(); + preimage.extend(coin_type_name.as_bytes()); + preimage.push(0xFE); + AccountAddress::from_bytes(HashValue::sha3_256_of(&preimage).to_vec()).unwrap() + } +} + impl LocalAccount { /// Create a new representation of an account locally. Note: This function /// does not actually create an account on the Aptos blockchain, just a