diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index b518511a54..0000000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,19 +0,0 @@ -# Contributing - -Thank you for considering contributing to this product! We welcome any contributions, whether it's bug fixes, new features, or improvements to the existing codebase. - -### Your First Pull Request - -Working on your first Pull Request? You can learn how from this free video series: - -[How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github) - -To help you get familiar with our contribution process, we have a list of [good first issues](../../issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) that contain bugs that have a relatively limited scope. This is a great place to get started. - -If you decide to fix an issue, please be sure to check the comment thread in case somebody is already working on a fix. If nobody is working on it at the moment, please leave a comment stating that you intend to work on it so other people don’t accidentally duplicate your effort. - -If somebody claims an issue but doesn’t follow up for more than two weeks, it’s fine to take it over but you should still leave a comment. **Issues won't be assigned to anyone outside the core team**. - -### Contribution Prerequisites - -... 🚧 Work in progress 🚧 ... diff --git a/README.md b/README.md index 511d436f9d..8e54838bcd 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,24 @@ Development of this product happens in the open on GitHub, and we are grateful t Please read our [Code of Conduct](../../../.github/blob/main/CODE_OF_CONDUCT.md) since we expect project participants to adhere to it. ### Contributing Guide -Read our [contributing guide](.github/CONTRIBUTING.md) to learn about our development process, how to propose bug fixes and improvements, and how to build and test your changes. + +Hiro welcomes all contributions to Hiro documentation. These contributions come in two forms: issues and pull requests. + +#### Issues + +Bugs, feature requests, and development-related questions should be directed to our [GitHub issues tracker](https://github.com/hirosystems/stacks-blockchain-api/issues/new). + +If reporting a bug, try to provide as much context as possible and anything else that might be relevant to the describe the issue. If possible include a simple test case that we can use to reproduce the problem on our own. + +For feature requests, please explain what you're trying to do, and how the requested feature would be a complement to the project. + +The two most important pieces of information we need in order to properly evaluate an issue or a feature request is a clear description of the behavior being reported. + +#### Pull requests + +A pull request allows anyone to suggest changes to a repository on GitHub that can be easily reviewed by others. Once a pull request is opened, you can discuss and review the potential changes with collaborators and add follow-up commits before your changes are merged into the base branch. + +Keep in mind that pull requests are not merged directly into the `master` branch. It must have `develop` as the base branch. ### Community diff --git a/docs/api/address/get-address-transaction-events.example.json b/docs/api/address/get-address-transaction-events.example.json new file mode 100644 index 0000000000..6e0410d542 --- /dev/null +++ b/docs/api/address/get-address-transaction-events.example.json @@ -0,0 +1,49 @@ +{ + "limit": 20, + "offset": 0, + "total": 4, + "results": [ + { + "type": "stx", + "event_index": 0, + "data": { + "type": "transfer", + "amount": "200", + "sender": "SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE", + "recipient": "SP24Q9PK9DNTA2V89APY8WNBJ1QYKKSW9SWB04RJP" + } + }, + { + "type": "stx", + "event_index": 1, + "data": { + "type": "transfer", + "amount": "150", + "sender": "SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE", + "recipient": "SP26066SDPP4NXKGCVYZQDR5GX2QPE8KADZ0YK2J7" + } + }, + { + "type": "ft", + "event_index": 5, + "data": { + "type": "transfer", + "amount": "103", + "asset_identifier": "SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-token::miamicoin", + "sender": "SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE", + "recipient": "SP24Q9PK9DNTA2V89APY8WNBJ1QYKKSW9SWB04RJP" + } + }, + { + "type": "nft", + "event_index": 6, + "data": { + "type": "transfer", + "asset_identifier": "SP497E7RX3233ATBS2AB9G4WTHB63X5PBSP5VGAQ.boom-nfts::boom", + "value": { "hex": "0x00", "repr": "0" }, + "sender": "SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE", + "recipient": "SP24Q9PK9DNTA2V89APY8WNBJ1QYKKSW9SWB04RJP" + } + } + ] +} diff --git a/docs/api/address/get-address-transaction-events.schema.json b/docs/api/address/get-address-transaction-events.schema.json new file mode 100644 index 0000000000..10ca2eba52 --- /dev/null +++ b/docs/api/address/get-address-transaction-events.schema.json @@ -0,0 +1,25 @@ +{ + "description": "GET Address Transaction Events", + "title": "AddressTransactionEventListResponse", + "type": "object", + "additionalProperties": false, + "required": ["results", "limit", "offset", "total"], + "properties": { + "limit": { + "type": "integer", + "maximum": 30 + }, + "offset": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "results": { + "type": "array", + "items": { + "$ref": "../../entities/address/address-transaction-event.schema.json" + } + } + } +} diff --git a/docs/api/address/get-v2-address-transactions.example.json b/docs/api/address/get-v2-address-transactions.example.json new file mode 100644 index 0000000000..da6f6f043f --- /dev/null +++ b/docs/api/address/get-v2-address-transactions.example.json @@ -0,0 +1,130 @@ +{ + "limit": 20, + "offset": 0, + "total": 2, + "results": [ + { + "tx": { + "tx_id": "0x34d79c7cfc2fe525438736733e501a4bf0308a5556e3e080d1e2c0858aad7448", + "tx_type": "contract_call", + "nonce": 11, + "fee_rate": "346", + "sender_address": "SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE", + "sponsored": false, + "post_condition_mode": "deny", + "tx_status": "success", + "block_hash": "0x13d1b4ad35c95bca209397420fb8af104d2929d91993ba056d7a1ca5470095f9", + "block_height": 3246, + "burn_block_time": 1613009951, + "burn_block_time_iso": "2021-02-11T02:19:11.000Z", + "canonical": true, + "is_unanchored": false, + "microblock_hash": "0x590a1bb1d7bcbeafce0a9fc8f8a69e369486192d14687fe95fbe4dc1c71d49df", + "microblock_sequence": 5, + "microblock_canonical": true, + "tx_index": 1, + "tx_result": { + "hex": "0x0703", + "repr": "(ok true)" + }, + "post_conditions": [ + { + "type": "stx", + "condition_code": "sent_equal_to", + "amount": "350", + "principal": { + "type_id": "principal_standard", + "address": "SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE" + } + } + ], + "contract_call": { + "contract_id": "SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.send-many-memo", + "function_name": "send-many", + "function_signature": "(define-public (send-many (recipients (list 200 (tuple (memo (buff 34)) (to principal) (ustx uint))))))", + "function_args": [ + { + "hex": "0x0b000000020c00000003046d656d6f020000000966697273746d656d6f02746f05168c031b2db5895ece0cdfbf76e0b0e8af67226a6f047573747801000000000000000000000000000000960c00000003046d656d6f020000000a7365636f6e646d656d6f02746f05168974da696d74a16d0955bc8e55720dfd39e789cf047573747801000000000000000000000000000000c8", + "repr": "(list (tuple (memo 0x66697273746d656d6f) (to SP26066SDPP4NXKGCVYZQDR5GX2QPE8KADZ0YK2J7) (ustx u150)) (tuple (memo 0x7365636f6e646d656d6f) (to SP24Q9PK9DNTA2V89APY8WNBJ1QYKKSW9SWB04RJP) (ustx u200)))", + "name": "recipients", + "type": "(list 200 (tuple (memo (buff 34)) (to principal) (ustx uint)))" + } + ] + }, + "events": [], + "event_count": 4 + }, + "stx_sent": "696", + "stx_received": "0", + "events": { + "stx": { + "transfer": 2, + "mint": 0, + "burn": 0 + }, + "ft": { + "transfer": 1, + "mint": 0, + "burn": 0 + }, + "nft": { + "transfer": 1, + "mint": 0, + "burn": 0 + } + } + }, + { + "tx": { + "tx_id": "0x628045bff13658396277d618e9a3e4d468a4b3876eff4941d2f13ed88cd7abb7", + "tx_type": "token_transfer", + "nonce": 8, + "fee_rate": "180", + "sender_address": "SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE", + "sponsored": false, + "post_condition_mode": "deny", + "tx_status": "success", + "block_hash": "0x2b8599696f64e2456c67b1ab5e63078f99d87bd1d903c37fdcfd73b1890a7551", + "block_height": 1761, + "burn_block_time": 1611968237, + "burn_block_time_iso": "2021-01-30T00:57:17.000Z", + "canonical": true, + "is_unanchored": false, + "microblock_hash": "", + "microblock_sequence": 2147483647, + "microblock_canonical": true, + "tx_index": 2, + "tx_result": { + "hex": "0x0703", + "repr": "(ok true)" + }, + "token_transfer": { + "recipient_address": "SPRSM0R2JZWBCZ39NQBARWTMX9TE99K3JK8D5KMX", + "amount": "100000", + "memo": "0x57656c636f6d6520746f20426f6f6d2e000000000000000000000000000000000000" + }, + "events": [], + "event_count": 1 + }, + "stx_sent": "100180", + "stx_received": "0", + "events": { + "stx": { + "transfer": 1, + "mint": 0, + "burn": 0 + }, + "ft": { + "transfer": 0, + "mint": 0, + "burn": 0 + }, + "nft": { + "transfer": 0, + "mint": 0, + "burn": 0 + } + } + } + ] +} diff --git a/docs/api/address/get-v2-address-transactions.schema.json b/docs/api/address/get-v2-address-transactions.schema.json new file mode 100644 index 0000000000..d3f504dfd9 --- /dev/null +++ b/docs/api/address/get-v2-address-transactions.schema.json @@ -0,0 +1,25 @@ +{ + "description": "GET Address Transactions", + "title": "AddressTransactionsV2ListResponse", + "type": "object", + "additionalProperties": false, + "required": ["results", "limit", "offset", "total"], + "properties": { + "limit": { + "type": "integer", + "maximum": 30 + }, + "offset": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "results": { + "type": "array", + "items": { + "$ref": "../../entities/address/address-transaction.schema.json" + } + } + } +} diff --git a/docs/entities/address/address-transaction-event.schema.json b/docs/entities/address/address-transaction-event.schema.json new file mode 100644 index 0000000000..ae39ccb959 --- /dev/null +++ b/docs/entities/address/address-transaction-event.schema.json @@ -0,0 +1,142 @@ +{ + "description": "Address Transaction Event", + "title": "AddressTransactionEvent", + "type": "object", + "anyOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_index", "data"], + "properties": { + "type": { + "type": "string", + "enum": ["stx"] + }, + "event_index": { + "type": "integer" + }, + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "amount", "type" + ], + "properties": { + "type": { + "type": "string", + "enum": ["transfer", "mint", "burn"] + }, + "amount": { + "type": "string", + "description": "Amount transferred in micro-STX as an integer string." + }, + "sender": { + "type": "string", + "description": "Principal that sent STX. This is unspecified if the STX were minted." + }, + "recipient": { + "type": "string", + "description": "Principal that received STX. This is unspecified if the STX were burned." + } + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_index", "data"], + "properties": { + "type": { + "type": "string", + "enum": ["ft"] + }, + "event_index": { + "type": "integer" + }, + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "amount", "asset_identifier", "type" + ], + "properties": { + "type": { + "type": "string", + "enum": ["transfer", "mint", "burn"] + }, + "asset_identifier": { + "type": "string", + "description": "Fungible Token asset identifier." + }, + "amount": { + "type": "string", + "description": "Amount transferred as an integer string. This balance does not factor in possible SIP-010 decimals." + }, + "sender": { + "type": "string", + "description": "Principal that sent the asset." + }, + "recipient": { + "type": "string", + "description": "Principal that received the asset." + } + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_index", "data"], + "properties": { + "type": { + "type": "string", + "enum": ["nft"] + }, + "event_index": { + "type": "integer" + }, + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "asset_identifier", "value", "type" + ], + "properties": { + "type": { + "type": "string", + "enum": ["transfer", "mint", "burn"] + }, + "asset_identifier": { + "type": "string", + "description": "Non Fungible Token asset identifier." + }, + "value": { + "type": "object", + "description": "Non Fungible Token asset value.", + "additionalProperties": false, + "required": ["hex", "repr"], + "properties": { + "hex": { + "type": "string" + }, + "repr": { + "type": "string" + } + } + }, + "sender": { + "type": "string", + "description": "Principal that sent the asset." + }, + "recipient": { + "type": "string", + "description": "Principal that received the asset." + } + } + } + } + } + ] +} diff --git a/docs/entities/address/address-transaction.schema.json b/docs/entities/address/address-transaction.schema.json new file mode 100644 index 0000000000..4ad5eb676a --- /dev/null +++ b/docs/entities/address/address-transaction.schema.json @@ -0,0 +1,81 @@ +{ + "title": "AddressTransaction", + "description": "Address transaction with STX, FT and NFT transfer summaries", + "type": "object", + "additionalProperties": false, + "required": [ + "tx", + "stx_sent", + "stx_received", + "stx_transfers", + "ft_transfers", + "nft_transfers" + ], + "properties": { + "tx": { + "$ref": "../transactions/transaction.schema.json" + }, + "stx_sent": { + "type": "string", + "description": "Total sent from the given address, including the tx fee, in micro-STX as an integer string." + }, + "stx_received": { + "type": "string", + "description": "Total received by the given address in micro-STX as an integer string." + }, + "events": { + "type": "object", + "required": ["stx", "ft", "nft"], + "properties": { + "stx": { + "type": "object", + "required": ["transfer", "mint", "burn"], + "additionalProperties": false, + "properties": { + "transfer": { + "type": "integer" + }, + "mint": { + "type": "integer" + }, + "burn": { + "type": "integer" + } + } + }, + "ft": { + "type": "object", + "required": ["transfer", "mint", "burn"], + "additionalProperties": false, + "properties": { + "transfer": { + "type": "integer" + }, + "mint": { + "type": "integer" + }, + "burn": { + "type": "integer" + } + } + }, + "nft": { + "type": "object", + "required": ["transfer", "mint", "burn"], + "additionalProperties": false, + "properties": { + "transfer": { + "type": "integer" + }, + "mint": { + "type": "integer" + }, + "burn": { + "type": "integer" + } + } + } + } + } + } +} diff --git a/docs/generated.d.ts b/docs/generated.d.ts index 095800439f..e883ea4646 100644 --- a/docs/generated.d.ts +++ b/docs/generated.d.ts @@ -9,8 +9,10 @@ export type SchemaMergeRootStub = | AddressBalanceResponse | AddressStxBalanceResponse | AddressStxInboundListResponse + | AddressTransactionEventListResponse | AddressTransactionsWithTransfersListResponse | AddressTransactionsListResponse + | AddressTransactionsV2ListResponse | BlockListResponse | BurnBlockListResponse | NakamotoBlockListResponse @@ -110,6 +112,8 @@ export type SchemaMergeRootStub = | TransactionResults | PostCoreNodeTransactionsError | AddressNonces + | AddressTransactionEvent + | AddressTransaction | AddressTokenOfferingLocked | AddressTransactionWithTransfers | AddressUnlockSchedule @@ -323,6 +327,78 @@ export type TransactionEventNonFungibleAsset = AbstractTransactionEvent & { export type AddressStxBalanceResponse = StxBalance & { token_offering_locked?: AddressTokenOfferingLocked; }; +/** + * Address Transaction Event + */ +export type AddressTransactionEvent = + | { + type: "stx"; + event_index: number; + data: { + type: "transfer" | "mint" | "burn"; + /** + * Amount transferred in micro-STX as an integer string. + */ + amount: string; + /** + * Principal that sent STX. This is unspecified if the STX were minted. + */ + sender?: string; + /** + * Principal that received STX. This is unspecified if the STX were burned. + */ + recipient?: string; + }; + } + | { + type: "ft"; + event_index: number; + data: { + type: "transfer" | "mint" | "burn"; + /** + * Fungible Token asset identifier. + */ + asset_identifier: string; + /** + * Amount transferred as an integer string. This balance does not factor in possible SIP-010 decimals. + */ + amount: string; + /** + * Principal that sent the asset. + */ + sender?: string; + /** + * Principal that received the asset. + */ + recipient?: string; + }; + } + | { + type: "nft"; + event_index: number; + data: { + type: "transfer" | "mint" | "burn"; + /** + * Non Fungible Token asset identifier. + */ + asset_identifier: string; + /** + * Non Fungible Token asset value. + */ + value: { + hex: string; + repr: string; + }; + /** + * Principal that sent the asset. + */ + sender?: string; + /** + * Principal that received the asset. + */ + recipient?: string; + }; + }; /** * Describes all transaction types on Stacks 2.0 blockchain */ @@ -892,6 +968,15 @@ export interface InboundStxTransfer { */ tx_index: number; } +/** + * GET Address Transaction Events + */ +export interface AddressTransactionEventListResponse { + limit: number; + offset: number; + total: number; + results: AddressTransactionEvent[]; +} /** * GET request that returns account transactions */ @@ -1153,6 +1238,47 @@ export interface AddressTransactionsListResponse { total: number; results: (MempoolTransaction | Transaction)[]; } +/** + * GET Address Transactions + */ +export interface AddressTransactionsV2ListResponse { + limit: number; + offset: number; + total: number; + results: AddressTransaction[]; +} +/** + * Address transaction with STX, FT and NFT transfer summaries + */ +export interface AddressTransaction { + tx: Transaction; + /** + * Total sent from the given address, including the tx fee, in micro-STX as an integer string. + */ + stx_sent: string; + /** + * Total received by the given address in micro-STX as an integer string. + */ + stx_received: string; + events?: { + stx: { + transfer: number; + mint: number; + burn: number; + }; + ft: { + transfer: number; + mint: number; + burn: number; + }; + nft: { + transfer: number; + mint: number; + burn: number; + }; + [k: string]: unknown | undefined; + }; +} /** * GET request that returns blocks */ diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 67c6917c63..1ffcd7e779 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -10,7 +10,7 @@ info: title: Stacks Blockchain API version: 'STACKS_API_VERSION' description: | - Welcome to the API reference overview for the [Stacks Blockchain API](https://docs.hiro.so/get-started/stacks-blockchain-api). + Welcome to the API reference overview for the [Stacks Blockchain API](https://docs.hiro.so/stacks-blockchain-api). [Download Postman collection](https://hirosystems.github.io/stacks-blockchain-api/collection.json) tags: @@ -834,6 +834,95 @@ paths: example: $ref: ./api/transaction/get-transactions.example.json + /extended/v2/addresses/{address}/transactions: + get: + summary: Get address transactions + description: | + Retrieves a paginated list of confirmed transactions sent or received by a STX address or Smart Contract ID, alongside the total amount of STX sent or received and the number of STX, FT and NFT transfers contained within each transaction. + + More information on Transaction types can be found [here](https://docs.stacks.co/understand-stacks/transactions#types). + tags: + - Transactions + operationId: get_address_transactions + parameters: + - name: address + in: path + description: STX address or Smart Contract ID + required: true + schema: + type: string + example: "SP31DA6FTSJX2WGTZ69SFY11BH51NZMB0ZW97B5P0" + - name: limit + in: query + description: Number of transactions to fetch + required: false + schema: + type: integer + example: 20 + - name: offset + in: query + description: Index of first transaction to fetch + required: false + schema: + type: integer + example: 10 + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: ./api/address/get-v2-address-transactions.schema.json + example: + $ref: ./api/address/get-v2-address-transactions.example.json + + /extended/v2/addresses/{address}/transactions/{tx_id}/events: + get: + summary: Get events for an address transaction + description: | + Retrieves a paginated list of all STX, FT and NFT events concerning a STX address or Smart Contract ID within a specific transaction. + tags: + - Transactions + operationId: get_address_transaction_events + parameters: + - name: address + in: path + description: STX address or Smart Contract ID + required: true + schema: + type: string + example: "SP31DA6FTSJX2WGTZ69SFY11BH51NZMB0ZW97B5P0" + - name: tx_id + in: path + description: Transaction ID + required: true + schema: + type: string + example: "0x0a411719e3bfde95f9e227a2d7f8fac3d6c646b1e6cc186db0e2838a2c6cd9c0" + - name: limit + in: query + description: Number of events to fetch + required: false + schema: + type: integer + example: 20 + - name: offset + in: query + description: Index of first event to fetch + required: false + schema: + type: integer + example: 10 + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: ./api/address/get-address-transaction-events.schema.json + example: + $ref: ./api/address/get-address-transaction-events.example.json + /extended/v2/smart-contracts/status: get: summary: Get smart contracts status @@ -869,7 +958,7 @@ paths: summary: Get recent blocks deprecated: true description: | - **NOTE:** This endpoint is deprecated in favor of [Get blocks](#operation/get_blocks). + **NOTE:** This endpoint is deprecated in favor of [Get blocks](/api/get-blocks). Retrieves a list of recently mined blocks @@ -915,7 +1004,7 @@ paths: deprecated: true summary: Get block by hash description: | - **NOTE:** This endpoint is deprecated in favor of [Get block](#operation/get_block). + **NOTE:** This endpoint is deprecated in favor of [Get block](/api/get-block). Retrieves block details of a specific block for a given chain height. You can use the hash from your latest block ('get_block_list' API) to get your block details. tags: @@ -949,7 +1038,7 @@ paths: deprecated: true summary: Get block by height description: | - **NOTE:** This endpoint is deprecated in favor of [Get block](#operation/get_block). + **NOTE:** This endpoint is deprecated in favor of [Get block](/api/get-block). Retrieves block details of a specific block at a given block height tags: @@ -983,7 +1072,7 @@ paths: summary: Get block by burnchain block hash deprecated: true description: | - **NOTE:** This endpoint is deprecated in favor of [Get blocks](#operation/get_blocks). + **NOTE:** This endpoint is deprecated in favor of [Get blocks](/api/get-blocks). Retrieves block details of a specific block for a given burnchain block hash tags: @@ -1018,7 +1107,7 @@ paths: summary: Get block by burnchain height deprecated: true description: | - **NOTE:** This endpoint is deprecated in favor of [Get blocks](#operation/get_blocks). + **NOTE:** This endpoint is deprecated in favor of [Get blocks](/api/get-blocks). Retrieves block details of a specific block for a given burn chain height tags: @@ -1618,6 +1707,7 @@ paths: Retrieves a list of all Transactions for a given Address or Contract Identifier. More information on Transaction types can be found [here](https://docs.stacks.co/understand-stacks/transactions#types). If you need to actively monitor new transactions for an address or contract id, we highly recommend subscribing to [WebSockets or Socket.io](https://github.com/hirosystems/stacks-blockchain-api/tree/master/client) for real-time updates. + deprecated: true tags: - Accounts operationId: get_account_transactions @@ -1679,6 +1769,7 @@ paths: get: summary: Get account transaction information for specific transaction description: Retrieves transaction details for a given Transaction Id `tx_id`, for a given account or contract Identifier. + deprecated: true tags: - Accounts operationId: get_single_transaction_with_transfers @@ -1717,6 +1808,7 @@ paths: get: summary: Get account transactions including STX transfers for each transaction. description: Retrieve all transactions for an account or contract identifier including STX transfers for each transaction. + deprecated: true tags: - Accounts operationId: get_account_transactions_with_transfers @@ -2003,7 +2095,7 @@ paths: example: $ref: ./api/core-node/get-info.example.json - /extended/v1/status: + /extended: get: summary: API status description: Retrieves the running status of the Stacks Blockchain API, including the server version and current chain tip information. @@ -3009,7 +3101,7 @@ paths: operationId: get_transactions_by_block_hash summary: Transactions by block hash description: | - **NOTE:** This endpoint is deprecated in favor of [Get transactions by block](#operation/get_transactions_by_block). + **NOTE:** This endpoint is deprecated in favor of [Get transactions by block](/api/get-transactions-by-block). Retrieves a list of all transactions within a block for a given block hash. tags: @@ -3052,7 +3144,7 @@ paths: operationId: get_transactions_by_block_height summary: Transactions by block height description: | - **NOTE:** This endpoint is deprecated in favor of [Get transactions by block](#operation/get_transactions_by_block). + **NOTE:** This endpoint is deprecated in favor of [Get transactions by block](/api/get-transactions-by-block). Retrieves all transactions within a block at a given height tags: @@ -3358,7 +3450,7 @@ paths: summary: Fetch fee rate deprecated: true description: | - **NOTE:** This endpoint is deprecated in favor of [Get approximate fees for a given transaction](#operation/post_fee_transaction). + **NOTE:** This endpoint is deprecated in favor of [Get approximate fees for a given transaction](/api/get-approximate-fees-for-a-given-transaction). Retrieves estimated fee rate. tags: diff --git a/src/api/init.ts b/src/api/init.ts index 6c7915a095..2b7e78b01d 100644 --- a/src/api/init.ts +++ b/src/api/init.ts @@ -54,6 +54,7 @@ import { getReqQuery } from './query-helpers'; import { createV2BurnBlocksRouter } from './routes/v2/burn-blocks'; import { createMempoolRouter } from './routes/v2/mempool'; import { createV2SmartContractsRouter } from './routes/v2/smart-contracts'; +import { createV2AddressesRouter } from './routes/v2/addresses'; export interface ApiServer { expressApp: express.Express; @@ -150,7 +151,7 @@ export async function startApiServer(opts: { app.set('etag', false); app.get('/', (req, res) => { - res.redirect(`/extended/v1/status`); + res.redirect(`/extended/`); }); app.use('/doc', (req, res) => { @@ -189,6 +190,7 @@ export async function startApiServer(opts: { res.set('Cache-Control', 'no-store'); next(); }); + router.use('/', createStatusRouter(datastore)); router.use( '/v1', (() => { @@ -203,7 +205,9 @@ export async function startApiServer(opts: { v1.use('/info', createInfoRouter(datastore)); v1.use('/stx_supply', createStxSupplyRouter(datastore)); v1.use('/debug', createDebugRouter(datastore)); - v1.use('/status', createStatusRouter(datastore)); + v1.use('/status', (req, res) => + res.redirect(`${req.baseUrl.replace(/v1\/status/, '')}${getReqQuery(req)}`) + ); v1.use('/fee_rate', createFeeRateRouter(datastore)); v1.use('/tokens', createTokenRouter(datastore)); @@ -236,6 +240,7 @@ export async function startApiServer(opts: { v2.use('/burn-blocks', createV2BurnBlocksRouter(datastore)); v2.use('/smart-contracts', createV2SmartContractsRouter(datastore)); v2.use('/mempool', createMempoolRouter(datastore)); + v2.use('/addresses', createV2AddressesRouter(datastore)); return v2; })() ); diff --git a/src/api/routes/address.ts b/src/api/routes/address.ts index d074f78270..a35fad8ebd 100644 --- a/src/api/routes/address.ts +++ b/src/api/routes/address.ts @@ -239,6 +239,9 @@ export function createAddressRouter(db: PgStore, chainId: ChainID): express.Rout }) ); + /** + * @deprecated See `/v2/addresses/:address/transactions/:tx_id` + */ router.get( '/:stx_address/:tx_id/with_transfers', cacheHandler, @@ -282,6 +285,9 @@ export function createAddressRouter(db: PgStore, chainId: ChainID): express.Rout }) ); + /** + * @deprecated See `/v2/addresses/:address/transactions` + */ router.get( '/:stx_address/transactions_with_transfers', cacheHandler, diff --git a/src/api/routes/v2/addresses.ts b/src/api/routes/v2/addresses.ts new file mode 100644 index 0000000000..16b4436d95 --- /dev/null +++ b/src/api/routes/v2/addresses.ts @@ -0,0 +1,100 @@ +import * as express from 'express'; +import { PgStore } from '../../../datastore/pg-store'; +import { + getETagCacheHandler, + setETagCacheHeaders, +} from '../../../api/controllers/cache-controller'; +import { asyncHandler } from '../../async-handler'; +import { + AddressParams, + AddressTransactionParams, + CompiledAddressParams, + CompiledAddressTransactionParams, + CompiledTransactionPaginationQueryParams, + TransactionPaginationQueryParams, + validRequestParams, + validRequestQuery, +} from './schemas'; +import { parseDbAddressTransactionTransfer, parseDbTxWithAccountTransferSummary } from './helpers'; +import { + AddressTransactionEventListResponse, + AddressTransactionsV2ListResponse, +} from '../../../../docs/generated'; +import { InvalidRequestError } from '../../../errors'; + +export function createV2AddressesRouter(db: PgStore): express.Router { + const router = express.Router(); + const cacheHandler = getETagCacheHandler(db); + + router.get( + '/:address/transactions', + cacheHandler, + asyncHandler(async (req, res) => { + if ( + !validRequestParams(req, res, CompiledAddressParams) || + !validRequestQuery(req, res, CompiledTransactionPaginationQueryParams) + ) + return; + const params = req.params as AddressParams; + const query = req.query as TransactionPaginationQueryParams; + + try { + const { limit, offset, results, total } = await db.v2.getAddressTransactions({ + ...params, + ...query, + }); + const response: AddressTransactionsV2ListResponse = { + limit, + offset, + total, + results: results.map(r => parseDbTxWithAccountTransferSummary(r)), + }; + setETagCacheHeaders(res); + res.json(response); + } catch (error) { + if (error instanceof InvalidRequestError) { + res.status(404).json({ errors: error.message }); + return; + } + throw error; + } + }) + ); + + router.get( + '/:address/transactions/:tx_id/events', + cacheHandler, + asyncHandler(async (req, res) => { + if ( + !validRequestParams(req, res, CompiledAddressTransactionParams) || + !validRequestQuery(req, res, CompiledTransactionPaginationQueryParams) + ) + return; + const params = req.params as AddressTransactionParams; + const query = req.query as TransactionPaginationQueryParams; + + try { + const { limit, offset, results, total } = await db.v2.getAddressTransactionEvents({ + ...params, + ...query, + }); + const response: AddressTransactionEventListResponse = { + limit, + offset, + total, + results: results.map(r => parseDbAddressTransactionTransfer(r)), + }; + setETagCacheHeaders(res); + res.json(response); + } catch (error) { + if (error instanceof InvalidRequestError) { + res.status(404).json({ errors: error.message }); + return; + } + throw error; + } + }) + ); + + return router; +} diff --git a/src/api/routes/v2/helpers.ts b/src/api/routes/v2/helpers.ts index 23ff0d18d3..4018f8bc60 100644 --- a/src/api/routes/v2/helpers.ts +++ b/src/api/routes/v2/helpers.ts @@ -1,8 +1,26 @@ -import { BurnBlock, NakamotoBlock, SmartContractsStatusResponse } from 'docs/generated'; -import { DbBlock, DbBurnBlock, DbSmartContractStatus } from '../../../datastore/common'; +import { + AddressTransaction, + AddressTransactionEvent, + BurnBlock, + NakamotoBlock, + SmartContractsStatusResponse, +} from 'docs/generated'; +import { + DbAddressTransactionEvent, + DbBlock, + DbBurnBlock, + DbEventTypeId, + DbSmartContractStatus, + DbTxWithAddressTransfers, +} from '../../../datastore/common'; import { unixEpochToIso } from '../../../helpers'; import { SmartContractStatusParams } from './schemas'; -import { getTxStatusString } from '../../../api/controllers/db-controller'; +import { + getAssetEventTypeString, + getTxStatusString, + parseDbTx, +} from '../../../api/controllers/db-controller'; +import { decodeClarityValueToRepr } from 'stacks-encoding-native-js'; export function parseDbNakamotoBlock(block: DbBlock): NakamotoBlock { const apiBlock: NakamotoBlock = { @@ -61,3 +79,76 @@ export function parseDbSmartContractStatusArray( for (const missingId of ids) response[missingId] = { found: false }; return response; } + +export function parseDbTxWithAccountTransferSummary( + tx: DbTxWithAddressTransfers +): AddressTransaction { + return { + tx: parseDbTx(tx), + stx_sent: tx.stx_sent.toString(), + stx_received: tx.stx_received.toString(), + events: { + stx: { + transfer: tx.stx_transfer, + mint: tx.stx_mint, + burn: tx.stx_burn, + }, + ft: { + transfer: tx.ft_transfer, + mint: tx.ft_mint, + burn: tx.ft_burn, + }, + nft: { + transfer: tx.nft_transfer, + mint: tx.nft_mint, + burn: tx.nft_burn, + }, + }, + }; +} + +export function parseDbAddressTransactionTransfer( + transfer: DbAddressTransactionEvent +): AddressTransactionEvent { + switch (transfer.event_type_id) { + case DbEventTypeId.FungibleTokenAsset: + return { + type: 'ft', + event_index: transfer.event_index, + data: { + type: getAssetEventTypeString(transfer.asset_event_type_id), + amount: transfer.amount, + asset_identifier: transfer.asset_identifier ?? '', + sender: transfer.sender ?? undefined, + recipient: transfer.recipient ?? undefined, + }, + }; + case DbEventTypeId.NonFungibleTokenAsset: + return { + type: 'nft', + event_index: transfer.event_index, + data: { + type: getAssetEventTypeString(transfer.asset_event_type_id), + asset_identifier: transfer.asset_identifier ?? '', + value: { + hex: transfer.value ?? '', + repr: decodeClarityValueToRepr(transfer.value ?? ''), + }, + sender: transfer.sender ?? undefined, + recipient: transfer.recipient ?? undefined, + }, + }; + case DbEventTypeId.StxAsset: + return { + type: 'stx', + event_index: transfer.event_index, + data: { + type: getAssetEventTypeString(transfer.asset_event_type_id), + amount: transfer.amount, + sender: transfer.sender ?? undefined, + recipient: transfer.recipient ?? undefined, + }, + }; + } + throw Error('Invalid address transaction transfer'); +} diff --git a/src/api/routes/v2/schemas.ts b/src/api/routes/v2/schemas.ts index 689c9bf7e7..6f9bc2f6b8 100644 --- a/src/api/routes/v2/schemas.ts +++ b/src/api/routes/v2/schemas.ts @@ -85,6 +85,27 @@ const BurnBlockHeightParamSchema = Type.RegExp(/^[0-9]+$/, { examples: ['777678'], }); +const AddressParamSchema = Type.RegExp(/^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}/, { + title: 'STX Address', + description: 'STX Address', + examples: ['SP318Q55DEKHRXJK696033DQN5C54D9K2EE6DHRWP'], +}); + +const SmartContractIdParamSchema = Type.RegExp( + /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}\.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}$/, + { + title: 'Smart Contract ID', + description: 'Smart Contract ID', + examples: ['SP000000000000000000002Q6VF78.pox-3'], + } +); + +const TransactionIdParamSchema = Type.RegExp(/^(0x)?[a-fA-F0-9]{64}$/i, { + title: 'Transaction ID', + description: 'Transaction ID', + examples: ['0xf6bd5f4a7b26184a3466340b2e99fd003b4962c0e382a7e4b6a13df3dd7a91c6'], +}); + // ========================== // Query and path params // TODO: Migrate these to each endpoint after switching from Express to Fastify @@ -126,14 +147,28 @@ const BlockParamsSchema = Type.Object( export type BlockParams = Static; export const CompiledBlockParams = ajv.compile(BlockParamsSchema); -const SmartContractPrincipal = Type.RegExp( - /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}\.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}$/ -); const SmartContractStatusParamsSchema = Type.Object( { - contract_id: Type.Union([Type.Array(SmartContractPrincipal), SmartContractPrincipal]), + contract_id: Type.Union([Type.Array(SmartContractIdParamSchema), SmartContractIdParamSchema]), }, { additionalProperties: false } ); export type SmartContractStatusParams = Static; export const CompiledSmartContractStatusParams = ajv.compile(SmartContractStatusParamsSchema); + +const AddressParamsSchema = Type.Object( + { address: Type.Union([AddressParamSchema, SmartContractIdParamSchema]) }, + { additionalProperties: false } +); +export type AddressParams = Static; +export const CompiledAddressParams = ajv.compile(AddressParamsSchema); + +const AddressTransactionParamsSchema = Type.Object( + { + address: Type.Union([AddressParamSchema, SmartContractIdParamSchema]), + tx_id: TransactionIdParamSchema, + }, + { additionalProperties: false } +); +export type AddressTransactionParams = Static; +export const CompiledAddressTransactionParams = ajv.compile(AddressTransactionParamsSchema); diff --git a/src/datastore/common.ts b/src/datastore/common.ts index 7e6bb5abad..46d308883c 100644 --- a/src/datastore/common.ts +++ b/src/datastore/common.ts @@ -225,6 +225,20 @@ export interface DbTxRaw extends DbTx { raw_tx: string; } +export interface DbTxWithAddressTransfers extends DbTx { + stx_sent: bigint; + stx_received: bigint; + stx_transfer: number; + stx_mint: number; + stx_burn: number; + ft_transfer: number; + ft_mint: number; + ft_burn: number; + nft_transfer: number; + nft_mint: number; + nft_burn: number; +} + export interface DbTxGlobalStatus { status: DbTxStatus; index_block_hash?: string; @@ -974,6 +988,31 @@ export interface ContractTxQueryResult extends TxQueryResult { abi?: unknown | null; } +export interface AddressTransfersTxQueryResult extends TxQueryResult { + stx_sent: bigint; + stx_received: bigint; + stx_transfer: number; + stx_mint: number; + stx_burn: number; + ft_transfer: number; + ft_mint: number; + ft_burn: number; + nft_transfer: number; + nft_mint: number; + nft_burn: number; +} + +export interface DbAddressTransactionEvent { + event_index: number; + amount: string; + event_type_id: DbEventTypeId; + asset_event_type_id: DbAssetEventTypeId; + sender: string | null; + recipient: string | null; + asset_identifier: string | null; + value: string | null; +} + export interface FaucetRequestQueryResult { currency: string; ip: string; diff --git a/src/datastore/helpers.ts b/src/datastore/helpers.ts index fba14fdaa8..b103b59225 100644 --- a/src/datastore/helpers.ts +++ b/src/datastore/helpers.ts @@ -44,6 +44,8 @@ import { TxQueryResult, DbPoxSyntheticRevokeDelegateStxEvent, ReOrgUpdatedEntities, + AddressTransfersTxQueryResult, + DbTxWithAddressTransfers, } from './common'; import { CoreNodeDropMempoolTxReasonType, @@ -328,6 +330,26 @@ function parseAbiColumn(abi: unknown | null): string | undefined { } } +export function parseAccountTransferSummaryTxQueryResult( + result: AddressTransfersTxQueryResult +): DbTxWithAddressTransfers { + const tx = parseTxQueryResult(result); + return { + ...tx, + stx_sent: result.stx_sent, + stx_received: result.stx_received, + stx_transfer: result.stx_transfer, + stx_mint: result.stx_mint, + stx_burn: result.stx_burn, + ft_transfer: result.ft_transfer, + ft_mint: result.ft_mint, + ft_burn: result.ft_burn, + nft_transfer: result.nft_transfer, + nft_mint: result.nft_mint, + nft_burn: result.nft_burn, + }; +} + export function parseTxQueryResult(result: ContractTxQueryResult): DbTx { const tx: DbTx = { tx_id: result.tx_id, diff --git a/src/datastore/pg-store-v2.ts b/src/datastore/pg-store-v2.ts index 1e7554594b..010e5b8c05 100644 --- a/src/datastore/pg-store-v2.ts +++ b/src/datastore/pg-store-v2.ts @@ -1,4 +1,4 @@ -import { BasePgStoreModule } from '@hirosystems/api-toolkit'; +import { BasePgStoreModule, PgSqlClient } from '@hirosystems/api-toolkit'; import { BlockLimitParamSchema, CompiledBurnBlockHashParam, @@ -7,6 +7,8 @@ import { BlockParams, BlockPaginationQueryParams, SmartContractStatusParams, + AddressParams, + AddressTransactionParams, } from '../api/routes/v2/schemas'; import { InvalidRequestError, InvalidRequestErrorType } from '../errors'; import { normalizeHashString } from '../helpers'; @@ -19,9 +21,32 @@ import { DbBurnBlock, DbTxTypeId, DbSmartContractStatus, - DbTxStatus, + AddressTransfersTxQueryResult, + DbTxWithAddressTransfers, + DbEventTypeId, + DbAddressTransactionEvent, + DbAssetEventTypeId, } from './common'; -import { BLOCK_COLUMNS, parseBlockQueryResult, TX_COLUMNS, parseTxQueryResult } from './helpers'; +import { + BLOCK_COLUMNS, + parseBlockQueryResult, + TX_COLUMNS, + parseTxQueryResult, + parseAccountTransferSummaryTxQueryResult, +} from './helpers'; + +async function assertAddressExists(sql: PgSqlClient, address: string) { + const addressCheck = + await sql`SELECT principal FROM principal_stx_txs WHERE principal = ${address} LIMIT 1`; + if (addressCheck.count === 0) + throw new InvalidRequestError(`Address not found`, InvalidRequestErrorType.invalid_param); +} + +async function assertTxIdExists(sql: PgSqlClient, tx_id: string) { + const txCheck = await sql`SELECT tx_id FROM txs WHERE tx_id = ${tx_id} LIMIT 1`; + if (txCheck.count === 0) + throw new InvalidRequestError(`Transaction not found`, InvalidRequestErrorType.invalid_param); +} export class PgStoreV2 extends BasePgStoreModule { async getBlocks(args: BlockPaginationQueryParams): Promise> { @@ -269,4 +294,152 @@ export class PgStoreV2 extends BasePgStoreModule { return statusArray; }); } + + async getAddressTransactions( + args: AddressParams & TransactionPaginationQueryParams + ): Promise> { + return await this.sqlTransaction(async sql => { + await assertAddressExists(sql, args.address); + const limit = args.limit ?? TransactionLimitParamSchema.default; + const offset = args.offset ?? 0; + + const eventCond = sql` + tx_id = stx_txs.tx_id + AND index_block_hash = stx_txs.index_block_hash + AND microblock_hash = stx_txs.microblock_hash + `; + const eventAcctCond = sql` + ${eventCond} AND (sender = ${args.address} OR recipient = ${args.address}) + `; + const resultQuery = await sql<(AddressTransfersTxQueryResult & { count: number })[]>` + WITH stx_txs AS ( + SELECT tx_id, index_block_hash, microblock_hash, (COUNT(*) OVER())::int AS count + FROM principal_stx_txs + WHERE principal = ${args.address} + AND canonical = TRUE + AND microblock_canonical = TRUE + ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC + LIMIT ${limit} + OFFSET ${offset} + ) + SELECT + ${sql(TX_COLUMNS)}, + ( + SELECT COALESCE(SUM(amount), 0) + FROM stx_events + WHERE ${eventCond} AND sender = ${args.address} + ) + txs.fee_rate AS stx_sent, + ( + SELECT COALESCE(SUM(amount), 0) + FROM stx_events + WHERE ${eventCond} AND recipient = ${args.address} + ) AS stx_received, + ( + SELECT COUNT(*)::int FROM stx_events + WHERE ${eventAcctCond} AND asset_event_type_id = ${DbAssetEventTypeId.Transfer} + ) AS stx_transfer, + ( + SELECT COUNT(*)::int FROM stx_events + WHERE ${eventAcctCond} AND asset_event_type_id = ${DbAssetEventTypeId.Mint} + ) AS stx_mint, + ( + SELECT COUNT(*)::int FROM stx_events + WHERE ${eventAcctCond} AND asset_event_type_id = ${DbAssetEventTypeId.Burn} + ) AS stx_burn, + ( + SELECT COUNT(*)::int FROM ft_events + WHERE ${eventAcctCond} AND asset_event_type_id = ${DbAssetEventTypeId.Transfer} + ) AS ft_transfer, + ( + SELECT COUNT(*)::int FROM ft_events + WHERE ${eventAcctCond} AND asset_event_type_id = ${DbAssetEventTypeId.Mint} + ) AS ft_mint, + ( + SELECT COUNT(*)::int FROM ft_events + WHERE ${eventAcctCond} AND asset_event_type_id = ${DbAssetEventTypeId.Burn} + ) AS ft_burn, + ( + SELECT COUNT(*)::int FROM nft_events + WHERE ${eventAcctCond} AND asset_event_type_id = ${DbAssetEventTypeId.Transfer} + ) AS nft_transfer, + ( + SELECT COUNT(*)::int FROM nft_events + WHERE ${eventAcctCond} AND asset_event_type_id = ${DbAssetEventTypeId.Mint} + ) AS nft_mint, + ( + SELECT COUNT(*)::int FROM nft_events + WHERE ${eventAcctCond} AND asset_event_type_id = ${DbAssetEventTypeId.Burn} + ) AS nft_burn, + count + FROM stx_txs + INNER JOIN txs USING (tx_id, index_block_hash, microblock_hash) + `; + const total = resultQuery.length > 0 ? resultQuery[0].count : 0; + const parsed = resultQuery.map(r => parseAccountTransferSummaryTxQueryResult(r)); + return { + total, + limit, + offset, + results: parsed, + }; + }); + } + + async getAddressTransactionEvents( + args: AddressTransactionParams & TransactionPaginationQueryParams + ): Promise> { + return await this.sqlTransaction(async sql => { + await assertAddressExists(sql, args.address); + await assertTxIdExists(sql, args.tx_id); + const limit = args.limit ?? TransactionLimitParamSchema.default; + const offset = args.offset ?? 0; + + const eventCond = sql` + canonical = true + AND microblock_canonical = true + AND tx_id = ${args.tx_id} + AND (sender = ${args.address} OR recipient = ${args.address}) + `; + const results = await sql<(DbAddressTransactionEvent & { count: number })[]>` + WITH events AS ( + ( + SELECT + sender, recipient, event_index, amount, NULL as asset_identifier, + NULL::bytea as value, ${DbEventTypeId.StxAsset}::int as event_type_id, + asset_event_type_id + FROM stx_events + WHERE ${eventCond} + ) + UNION + ( + SELECT + sender, recipient, event_index, amount, asset_identifier, NULL::bytea as value, + ${DbEventTypeId.FungibleTokenAsset}::int as event_type_id, asset_event_type_id + FROM ft_events + WHERE ${eventCond} + ) + UNION + ( + SELECT + sender, recipient, event_index, 0 as amount, asset_identifier, value, + ${DbEventTypeId.NonFungibleTokenAsset}::int as event_type_id, asset_event_type_id + FROM nft_events + WHERE ${eventCond} + ) + ) + SELECT *, COUNT(*) OVER()::int AS count + FROM events + ORDER BY event_index ASC + LIMIT ${limit} + OFFSET ${offset} + `; + const total = results.length > 0 ? results[0].count : 0; + return { + total, + limit, + offset, + results, + }; + }); + } } diff --git a/src/datastore/pg-store.ts b/src/datastore/pg-store.ts index 6e2648840c..84aaa7982d 100644 --- a/src/datastore/pg-store.ts +++ b/src/datastore/pg-store.ts @@ -2745,6 +2745,9 @@ export class PgStore extends BasePgStore { return { results: parsed, total: count }; } + /** + * @deprecated See `/v2/addresses/:address/transactions/:tx_id` + */ async getInformationTxsWithStxTransfers({ stxAddress, tx_id, @@ -2813,6 +2816,9 @@ export class PgStore extends BasePgStore { return txTransfers[0]; } + /** + * @deprecated See `/v2/addresses/:address/transactions` + */ async getAddressTxsWithAssetTransfers(args: { stxAddress: string; blockHeight: number; diff --git a/src/index.ts b/src/index.ts index 822951e4de..b4af319b38 100644 --- a/src/index.ts +++ b/src/index.ts @@ -110,7 +110,7 @@ async function init(): Promise { if (isProdEnv && !fs.existsSync('.git-info')) { throw new Error( 'File not found: .git-info. This generated file is required to display the running API version in the ' + - '`/extended/v1/status` endpoint. Please execute `npm run build` to regenerate it.' + '`/extended/` endpoint. Please execute `npm run build` to regenerate it.' ); } chainIdConfigurationCheck(); diff --git a/src/tests/address-tests.ts b/src/tests/address-tests.ts index 64b197c841..7d1b5f88d3 100644 --- a/src/tests/address-tests.ts +++ b/src/tests/address-tests.ts @@ -69,7 +69,7 @@ describe('address tests', () => { const testAddr2 = 'ST1HB64MAJ1MBV4CQ80GF01DZS4T1DSMX20ADCRA4'; const testContractAddr = 'ST27W5M8BRKA7C5MZE2R1S1F4XTPHFWFRNHA9M04Y.hello-world'; const testAddr4 = 'ST3DWSXBPYDB484QXFTR81K4AWG4ZB5XZNFF3H70C'; - const testTxId = '0x12340006'; + const testTxId = '0x03807fdb726b3cb843e0330c564a4974037be8f9ea58ec7f8ebe03c34b890006'; const block: DbBlock = { block_hash: '0x1234', @@ -102,7 +102,9 @@ describe('address tests', () => { nftEventCount = 1 ): [DbTxRaw, DbStxEvent[], DbFtEvent[], DbNftEvent[]] => { const tx: DbTxRaw = { - tx_id: '0x1234' + (++indexIdIndex).toString().padStart(4, '0'), + tx_id: + '0x03807fdb726b3cb843e0330c564a4974037be8f9ea58ec7f8ebe03c34b89' + + (++indexIdIndex).toString().padStart(4, '0'), tx_index: indexIdIndex, anchor_mode: 3, nonce: 0, @@ -137,13 +139,14 @@ describe('address tests', () => { execution_cost_write_count: 4, execution_cost_write_length: 5, }; + let eventIndex = 0; const stxEvents: DbStxEvent[] = []; for (let i = 0; i < stxEventCount; i++) { const stxEvent: DbStxEvent = { canonical, event_type: DbEventTypeId.StxAsset, asset_event_type_id: DbAssetEventTypeId.Transfer, - event_index: i, + event_index: eventIndex++, tx_id: tx.tx_id, tx_index: tx.tx_index, block_height: tx.block_height, @@ -160,7 +163,7 @@ describe('address tests', () => { event_type: DbEventTypeId.FungibleTokenAsset, asset_event_type_id: DbAssetEventTypeId.Transfer, asset_identifier: 'usdc', - event_index: i, + event_index: eventIndex++, tx_id: tx.tx_id, tx_index: tx.tx_index, block_height: tx.block_height, @@ -177,7 +180,7 @@ describe('address tests', () => { event_type: DbEventTypeId.NonFungibleTokenAsset, asset_event_type_id: DbAssetEventTypeId.Transfer, asset_identifier: 'punk1', - event_index: i, + event_index: eventIndex++, tx_id: tx.tx_id, tx_index: tx.tx_index, block_height: tx.block_height, @@ -229,7 +232,7 @@ describe('address tests', () => { results: [ { tx: { - tx_id: '0x12340006', + tx_id: '0x03807fdb726b3cb843e0330c564a4974037be8f9ea58ec7f8ebe03c34b890006', tx_type: 'token_transfer', nonce: 0, anchor_mode: 'any', @@ -316,7 +319,7 @@ describe('address tests', () => { }, { tx: { - tx_id: '0x12340003', + tx_id: '0x03807fdb726b3cb843e0330c564a4974037be8f9ea58ec7f8ebe03c34b890003', tx_type: 'token_transfer', nonce: 0, anchor_mode: 'any', @@ -377,7 +380,7 @@ describe('address tests', () => { }, { tx: { - tx_id: '0x12340002', + tx_id: '0x03807fdb726b3cb843e0330c564a4974037be8f9ea58ec7f8ebe03c34b890002', tx_type: 'token_transfer', nonce: 0, anchor_mode: 'any', @@ -453,6 +456,162 @@ describe('address tests', () => { }; expect(JSON.parse(fetch1.text)).toEqual(expected1); + // Test v2 endpoints + const v2Fetch1 = await supertest(api.server).get( + `/extended/v2/addresses/${testAddr2}/transactions` + ); + expect(v2Fetch1.status).toBe(200); + expect(v2Fetch1.type).toBe('application/json'); + const v2Fetch1Json = JSON.parse(v2Fetch1.text); + expect(v2Fetch1Json.results[0].tx).toStrictEqual(expected1.results[0].tx); + expect(v2Fetch1Json.results[0].stx_sent).toBe('1339'); + expect(v2Fetch1Json.results[0].stx_received).toBe('0'); + expect(v2Fetch1Json.results[0].events.stx).toStrictEqual({ + transfer: 3, + mint: 0, + burn: 0, + }); + expect(v2Fetch1Json.results[0].events.ft).toStrictEqual({ + transfer: 1, + mint: 0, + burn: 0, + }); + expect(v2Fetch1Json.results[0].events.nft).toStrictEqual({ + transfer: 2, + mint: 0, + burn: 0, + }); + expect(v2Fetch1Json.results[1].tx).toStrictEqual(expected1.results[1].tx); + expect(v2Fetch1Json.results[1].stx_sent).toBe('1484'); + expect(v2Fetch1Json.results[1].stx_received).toBe('0'); + expect(v2Fetch1Json.results[1].events.stx).toStrictEqual({ + transfer: 1, + mint: 0, + burn: 0, + }); + expect(v2Fetch1Json.results[1].events.ft).toStrictEqual({ + transfer: 0, + mint: 0, + burn: 0, + }); + expect(v2Fetch1Json.results[1].events.nft).toStrictEqual({ + transfer: 1, + mint: 0, + burn: 0, + }); + expect(v2Fetch1Json.results[2].tx).toStrictEqual(expected1.results[2].tx); + expect(v2Fetch1Json.results[2].stx_sent).toBe('1334'); + expect(v2Fetch1Json.results[2].stx_received).toBe('0'); + expect(v2Fetch1Json.results[2].events.stx).toStrictEqual({ + transfer: 1, + mint: 0, + burn: 0, + }); + expect(v2Fetch1Json.results[2].events.ft).toStrictEqual({ + transfer: 2, + mint: 0, + burn: 0, + }); + expect(v2Fetch1Json.results[2].events.nft).toStrictEqual({ + transfer: 1, + mint: 0, + burn: 0, + }); + + const v2Fetch2 = await supertest(api.server).get( + `/extended/v2/addresses/${testAddr2}/transactions/${v2Fetch1Json.results[0].tx.tx_id}/events?limit=3` + ); + expect(v2Fetch2.status).toBe(200); + expect(v2Fetch2.type).toBe('application/json'); + expect(JSON.parse(v2Fetch2.text)).toStrictEqual({ + limit: 3, + offset: 0, + results: [ + { + data: { + type: 'transfer', + amount: '35', + recipient: 'ST3DWSXBPYDB484QXFTR81K4AWG4ZB5XZNFF3H70C', + sender: 'ST1HB64MAJ1MBV4CQ80GF01DZS4T1DSMX20ADCRA4', + }, + event_index: 0, + type: 'stx', + }, + { + data: { + type: 'transfer', + amount: '35', + recipient: 'ST3DWSXBPYDB484QXFTR81K4AWG4ZB5XZNFF3H70C', + sender: 'ST1HB64MAJ1MBV4CQ80GF01DZS4T1DSMX20ADCRA4', + }, + event_index: 1, + type: 'stx', + }, + { + data: { + type: 'transfer', + amount: '35', + recipient: 'ST3DWSXBPYDB484QXFTR81K4AWG4ZB5XZNFF3H70C', + sender: 'ST1HB64MAJ1MBV4CQ80GF01DZS4T1DSMX20ADCRA4', + }, + event_index: 2, + type: 'stx', + }, + ], + total: 6, + }); + const v2Fetch3 = await supertest(api.server).get( + `/extended/v2/addresses/${testAddr2}/transactions/${v2Fetch1Json.results[0].tx.tx_id}/events?offset=3&limit=3` + ); + expect(v2Fetch3.status).toBe(200); + expect(v2Fetch3.type).toBe('application/json'); + expect(JSON.parse(v2Fetch3.text)).toStrictEqual({ + limit: 3, + offset: 3, + results: [ + { + data: { + type: 'transfer', + amount: '35', + asset_identifier: 'usdc', + recipient: 'ST3DWSXBPYDB484QXFTR81K4AWG4ZB5XZNFF3H70C', + sender: 'ST1HB64MAJ1MBV4CQ80GF01DZS4T1DSMX20ADCRA4', + }, + event_index: 3, + type: 'ft', + }, + { + data: { + type: 'transfer', + asset_identifier: 'punk1', + recipient: 'ST3DWSXBPYDB484QXFTR81K4AWG4ZB5XZNFF3H70C', + sender: 'ST1HB64MAJ1MBV4CQ80GF01DZS4T1DSMX20ADCRA4', + value: { + hex: '0x0100000000000000000000000000000023', + repr: 'u35', + }, + }, + event_index: 4, + type: 'nft', + }, + { + data: { + type: 'transfer', + asset_identifier: 'punk1', + recipient: 'ST3DWSXBPYDB484QXFTR81K4AWG4ZB5XZNFF3H70C', + sender: 'ST1HB64MAJ1MBV4CQ80GF01DZS4T1DSMX20ADCRA4', + value: { + hex: '0x0100000000000000000000000000000023', + repr: 'u35', + }, + }, + event_index: 5, + type: 'nft', + }, + ], + total: 6, + }); + // testing single txs information based on given tx_id const fetchSingleTxInformation = await supertest(api.server).get( `/extended/v1/address/${testAddr4}/${testTxId}/with_transfers` @@ -461,7 +620,7 @@ describe('address tests', () => { expect(fetchSingleTxInformation.type).toBe('application/json'); const expectedSingleTxInformation = { tx: { - tx_id: '0x12340006', + tx_id: '0x03807fdb726b3cb843e0330c564a4974037be8f9ea58ec7f8ebe03c34b890006', tx_type: 'token_transfer', nonce: 0, anchor_mode: 'any', @@ -533,7 +692,7 @@ describe('address tests', () => { results: [ { tx: { - tx_id: '0x12340006', + tx_id: '0x03807fdb726b3cb843e0330c564a4974037be8f9ea58ec7f8ebe03c34b890006', tx_type: 'token_transfer', nonce: 0, anchor_mode: 'any', @@ -620,7 +779,7 @@ describe('address tests', () => { }, { tx: { - tx_id: '0x12340005', + tx_id: '0x03807fdb726b3cb843e0330c564a4974037be8f9ea58ec7f8ebe03c34b890005', tx_type: 'token_transfer', nonce: 0, anchor_mode: 'any', diff --git a/src/tests/other-tests.ts b/src/tests/other-tests.ts index 19489d4d47..f1276b170b 100644 --- a/src/tests/other-tests.ts +++ b/src/tests/other-tests.ts @@ -302,7 +302,7 @@ describe('other tests', () => { }); test('active status', async () => { - const result = await supertest(api.server).get(`/extended/v1/status/`); + const result = await supertest(api.server).get(`/extended/`); expect(result.body.status).toBe('ready'); });