From fa267c9f805d46cf018b81efad3ef7a9c78ea93d Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Fri, 15 Dec 2023 13:11:12 +0800 Subject: [PATCH 1/5] feat: Added inner transaction support Work in progress --- README.md | 1 - src/subscriptions.ts | 4 +- src/transform.ts | 51 ++++--- src/types/block.ts | 31 ++++ tests/contract/client.ts | 65 +++++++- tests/contract/contract.py | 12 ++ tests/scenarios/inner_transactions.spec.ts | 164 +++++++++++++++++++++ 7 files changed, 302 insertions(+), 26 deletions(-) create mode 100644 tests/scenarios/inner_transactions.spec.ts diff --git a/README.md b/README.md index e0f4f7f5..daf3333f 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,6 @@ subscriber.start() ## Roadmap -- Automated test coverage - Subscribe to contract events ([ARC-28](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0028.md)) - Inner transaction processing - Multiple filters diff --git a/src/subscriptions.ts b/src/subscriptions.ts index dea69cf7..5f51911e 100644 --- a/src/subscriptions.ts +++ b/src/subscriptions.ts @@ -5,7 +5,7 @@ import type SearchForTransactions from 'algosdk/dist/types/client/v2/indexer/sea import sha512 from 'js-sha512' import { algodOnCompleteToIndexerOnComplete, - getAlgodTransactionFromBlockTransaction, + getAlgodTransactionsFromBlockTransaction, getIndexerTransactionFromAlgodTransaction, } from './transform' import type { Block } from './types/block' @@ -105,7 +105,7 @@ export async function getSubscribedTransactions( currentRound, subscribedTransactions: catchupTransactions.concat( blocks - .flatMap((b) => b.block.txns?.map((t) => getAlgodTransactionFromBlockTransaction(t, b.block)).filter((t) => !!t) ?? []) + .flatMap((b) => b.block.txns?.flatMap((t) => getAlgodTransactionsFromBlockTransaction(t, b.block)).filter((t) => !!t) ?? []) .filter((t) => transactionFilter(filter, t!.createdAssetId, t!.createdAppId)(t!)) .map((t) => getIndexerTransactionFromAlgodTransaction( diff --git a/src/transform.ts b/src/transform.ts index c594f6c6..ddeaac93 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -25,20 +25,19 @@ function removeNulls(obj: any) { * @param block The block the transaction belongs to * @returns The `algosdk.Transaction` object along with key secondary information from the block. */ -export function getAlgodTransactionFromBlockTransaction( +export function getAlgodTransactionsFromBlockTransaction( blockTransaction: BlockTransaction, block: Block, -): - | { - transaction: Transaction - createdAssetId?: number - createdAppId?: number - assetCloseAmount?: number - closeAmount?: number - block: Block - blockOffset: number - } - | undefined { + blockOffset?: number, +): { + transaction: Transaction + createdAssetId?: number + createdAppId?: number + assetCloseAmount?: number + closeAmount?: number + block: Block + blockOffset: number +}[] { const txn = blockTransaction.txn // https://github.com/algorand/js-algorand-sdk/blob/develop/examples/block_fetcher/index.ts @@ -49,21 +48,29 @@ export function getAlgodTransactionFromBlockTransaction( // Unset gen if `hgi` isn't set // eslint-disable-next-line @typescript-eslint/no-explicit-any if (!blockTransaction.hgi) txn.gen = null as any + // Unset gen if `hgh` is set to false + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (blockTransaction.hgh === false) txn.gh = null as any // todo: support these? if (txn.type === 'stpf' || txn.type === 'keyreg') { - return undefined + return [] } - return { - transaction: Transaction.from_obj_for_encoding(txn), - createdAssetId: blockTransaction.caid, - createdAppId: blockTransaction.apid, - assetCloseAmount: blockTransaction.aca, - closeAmount: blockTransaction.ca, - block: block, - blockOffset: block.txns.indexOf(blockTransaction), - } + return [ + { + transaction: Transaction.from_obj_for_encoding(txn), + createdAssetId: blockTransaction.caid, + createdAppId: blockTransaction.apid, + assetCloseAmount: blockTransaction.aca, + closeAmount: blockTransaction.ca, + block: block, + blockOffset: blockOffset ?? block.txns.indexOf(blockTransaction), + }, + ...(blockTransaction.dt?.itx?.flatMap((bt) => + getAlgodTransactionsFromBlockTransaction({ ...bt, hgi: false, hgh: false }, block, blockOffset), + ) ?? []), + ] } /** diff --git a/src/types/block.ts b/src/types/block.ts index ccfc89c1..98acf167 100644 --- a/src/types/block.ts +++ b/src/types/block.ts @@ -58,6 +58,8 @@ export interface Block { export interface BlockTransaction { /** The encoded transaction data */ txn: EncodedTransaction + /** The eval deltas for the block */ + dt?: BlockTransactionEvalDelta /** Asset ID when an asset is created by the transaction */ caid?: number /** App ID when an app is created by the transaction */ @@ -68,4 +70,33 @@ export interface BlockTransaction { ca?: number /** Has genesis id */ hgi: boolean + /** Has genesis hash */ + hgh?: boolean +} + +/** Eval deltas for a block */ +export interface BlockTransactionEvalDelta { + /** The delta of global state, keyed by key */ + gd: Record + /** The delta of local state keyed by account ID offset in [txn.Sender, ...txn.Accounts] and then keyed by key */ + ld: Record> + /** Logs */ + lg: string[] + /** Inner transactions */ + itx?: Omit[] +} + +export interface BlockValueDelta { + /** DeltaAction is an enum of actions that may be performed when applying a delta to a TEAL key/value store: + * * `1`: SetBytesAction indicates that a TEAL byte slice should be stored at a key + * * `2`: SetUintAction indicates that a Uint should be stored at a key + * * `3`: DeleteAction indicates that the value for a particular key should be deleted + **/ + at: number + + /** Bytes value */ + bs?: Uint8Array + + /** Uint64 value */ + ui?: number } diff --git a/tests/contract/client.ts b/tests/contract/client.ts index 22b25692..0d12b888 100644 --- a/tests/contract/client.ts +++ b/tests/contract/client.ts @@ -49,6 +49,11 @@ export const APP_SPEC: AppSpec = { "no_op": "CALL" } }, + "issue_transfer_to_sender(uint64)void": { + "call_config": { + "no_op": "CALL" + } + }, "set_box(byte[4],string)void": { "call_config": { "no_op": "CALL" @@ -66,7 +71,7 @@ export const APP_SPEC: AppSpec = { } }, "source": { - "approval": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDAgMSAxMApieXRlY2Jsb2NrIDB4IDB4MTUxZjdjNzUKdHhuIE51bUFwcEFyZ3MKaW50Y18wIC8vIDAKPT0KYm56IG1haW5fbDE2CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4ZjE3ZTgwYTUgLy8gImNhbGxfYWJpKHN0cmluZylzdHJpbmciCj09CmJueiBtYWluX2wxNQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGFkNzU2MDJjIC8vICJjYWxsX2FiaV9mb3JlaWduX3JlZnMoKXN0cmluZyIKPT0KYm56IG1haW5fbDE0CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4YTRjZjhkZWEgLy8gInNldF9nbG9iYWwodWludDY0LHVpbnQ2NCxzdHJpbmcsYnl0ZVs0XSl2b2lkIgo9PQpibnogbWFpbl9sMTMKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhjZWMyODM0YSAvLyAic2V0X2xvY2FsKHVpbnQ2NCx1aW50NjQsc3RyaW5nLGJ5dGVbNF0pdm9pZCIKPT0KYm56IG1haW5fbDEyCnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4YTRiNGEyMzAgLy8gInNldF9ib3goYnl0ZVs0XSxzdHJpbmcpdm9pZCIKPT0KYm56IG1haW5fbDExCnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4NDRkMGRhMGQgLy8gImVycm9yKCl2b2lkIgo9PQpibnogbWFpbl9sMTAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgzMGM2ZDU4YSAvLyAib3B0X2luKCl2b2lkIgo9PQpibnogbWFpbl9sOQplcnIKbWFpbl9sOToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzEgLy8gT3B0SW4KPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgb3B0aW5jYXN0ZXJfMTcKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDEwOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGVycm9yY2FzdGVyXzE2CmludGNfMSAvLyAxCnJldHVybgptYWluX2wxMToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBzZXRib3hjYXN0ZXJfMTUKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDEyOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHNldGxvY2FsY2FzdGVyXzE0CmludGNfMSAvLyAxCnJldHVybgptYWluX2wxMzoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBzZXRnbG9iYWxjYXN0ZXJfMTMKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDE0Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGNhbGxhYmlmb3JlaWducmVmc2Nhc3Rlcl8xMgppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTU6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgY2FsbGFiaWNhc3Rlcl8xMQppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTY6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KYm56IG1haW5fbDI0CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE9wdEluCj09CmJueiBtYWluX2wyMwp0eG4gT25Db21wbGV0aW9uCnB1c2hpbnQgNCAvLyBVcGRhdGVBcHBsaWNhdGlvbgo9PQpibnogbWFpbl9sMjIKdHhuIE9uQ29tcGxldGlvbgpwdXNoaW50IDUgLy8gRGVsZXRlQXBwbGljYXRpb24KPT0KYm56IG1haW5fbDIxCmVycgptYWluX2wyMToKdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KYXNzZXJ0CmNhbGxzdWIgZGVsZXRlXzkKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDIyOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiB1cGRhdGVfOAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjM6CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCj09CmFzc2VydApjYWxsc3ViIGNyZWF0ZV83CmludGNfMSAvLyAxCnJldHVybgptYWluX2wyNDoKdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KYXNzZXJ0CmNhbGxzdWIgY3JlYXRlXzcKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBjYWxsX2FiaQpjYWxsYWJpXzA6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCnB1c2hieXRlcyAweDQ4NjU2YzZjNmYyYzIwIC8vICJIZWxsbywgIgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKY29uY2F0CmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApsZW4KaXRvYgpleHRyYWN0IDYgMApmcmFtZV9kaWcgMApjb25jYXQKZnJhbWVfYnVyeSAwCnJldHN1YgoKLy8gaXRvYQppdG9hXzE6CnByb3RvIDEgMQpmcmFtZV9kaWcgLTEKaW50Y18wIC8vIDAKPT0KYm56IGl0b2FfMV9sNQpmcmFtZV9kaWcgLTEKaW50Y18yIC8vIDEwCi8KaW50Y18wIC8vIDAKPgpibnogaXRvYV8xX2w0CmJ5dGVjXzAgLy8gIiIKaXRvYV8xX2wzOgpwdXNoYnl0ZXMgMHgzMDMxMzIzMzM0MzUzNjM3MzgzOSAvLyAiMDEyMzQ1Njc4OSIKZnJhbWVfZGlnIC0xCmludGNfMiAvLyAxMAolCmludGNfMSAvLyAxCmV4dHJhY3QzCmNvbmNhdApiIGl0b2FfMV9sNgppdG9hXzFfbDQ6CmZyYW1lX2RpZyAtMQppbnRjXzIgLy8gMTAKLwpjYWxsc3ViIGl0b2FfMQpiIGl0b2FfMV9sMwppdG9hXzFfbDU6CnB1c2hieXRlcyAweDMwIC8vICIwIgppdG9hXzFfbDY6CnJldHN1YgoKLy8gY2FsbF9hYmlfZm9yZWlnbl9yZWZzCmNhbGxhYmlmb3JlaWducmVmc18yOgpwcm90byAwIDEKYnl0ZWNfMCAvLyAiIgpwdXNoYnl0ZXMgMHg0MTcwNzAzYTIwIC8vICJBcHA6ICIKdHhuYSBBcHBsaWNhdGlvbnMgMQpjYWxsc3ViIGl0b2FfMQpjb25jYXQKcHVzaGJ5dGVzIDB4MmMyMDQxNzM3MzY1NzQzYTIwIC8vICIsIEFzc2V0OiAiCmNvbmNhdAp0eG5hIEFzc2V0cyAwCmNhbGxzdWIgaXRvYV8xCmNvbmNhdApwdXNoYnl0ZXMgMHgyYzIwNDE2MzYzNmY3NTZlNzQzYTIwIC8vICIsIEFjY291bnQ6ICIKY29uY2F0CnR4bmEgQWNjb3VudHMgMAppbnRjXzAgLy8gMApnZXRieXRlCmNhbGxzdWIgaXRvYV8xCmNvbmNhdApwdXNoYnl0ZXMgMHgzYSAvLyAiOiIKY29uY2F0CnR4bmEgQWNjb3VudHMgMAppbnRjXzEgLy8gMQpnZXRieXRlCmNhbGxzdWIgaXRvYV8xCmNvbmNhdApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIHNldF9nbG9iYWwKc2V0Z2xvYmFsXzM6CnByb3RvIDQgMApwdXNoYnl0ZXMgMHg2OTZlNzQzMSAvLyAiaW50MSIKZnJhbWVfZGlnIC00CmFwcF9nbG9iYWxfcHV0CnB1c2hieXRlcyAweDY5NmU3NDMyIC8vICJpbnQyIgpmcmFtZV9kaWcgLTMKYXBwX2dsb2JhbF9wdXQKcHVzaGJ5dGVzIDB4NjI3OTc0NjU3MzMxIC8vICJieXRlczEiCmZyYW1lX2RpZyAtMgpleHRyYWN0IDIgMAphcHBfZ2xvYmFsX3B1dApwdXNoYnl0ZXMgMHg2Mjc5NzQ2NTczMzIgLy8gImJ5dGVzMiIKZnJhbWVfZGlnIC0xCmFwcF9nbG9iYWxfcHV0CnJldHN1YgoKLy8gc2V0X2xvY2FsCnNldGxvY2FsXzQ6CnByb3RvIDQgMAp0eG4gU2VuZGVyCnB1c2hieXRlcyAweDZjNmY2MzYxNmM1ZjY5NmU3NDMxIC8vICJsb2NhbF9pbnQxIgpmcmFtZV9kaWcgLTQKYXBwX2xvY2FsX3B1dAp0eG4gU2VuZGVyCnB1c2hieXRlcyAweDZjNmY2MzYxNmM1ZjY5NmU3NDMyIC8vICJsb2NhbF9pbnQyIgpmcmFtZV9kaWcgLTMKYXBwX2xvY2FsX3B1dAp0eG4gU2VuZGVyCnB1c2hieXRlcyAweDZjNmY2MzYxNmM1ZjYyNzk3NDY1NzMzMSAvLyAibG9jYWxfYnl0ZXMxIgpmcmFtZV9kaWcgLTIKZXh0cmFjdCAyIDAKYXBwX2xvY2FsX3B1dAp0eG4gU2VuZGVyCnB1c2hieXRlcyAweDZjNmY2MzYxNmM1ZjYyNzk3NDY1NzMzMiAvLyAibG9jYWxfYnl0ZXMyIgpmcmFtZV9kaWcgLTEKYXBwX2xvY2FsX3B1dApyZXRzdWIKCi8vIHNldF9ib3gKc2V0Ym94XzU6CnByb3RvIDIgMApmcmFtZV9kaWcgLTIKYm94X2RlbApwb3AKZnJhbWVfZGlnIC0yCmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApib3hfcHV0CnJldHN1YgoKLy8gZXJyb3IKZXJyb3JfNjoKcHJvdG8gMCAwCmludGNfMCAvLyAwCi8vIERlbGliZXJhdGUgZXJyb3IKYXNzZXJ0CnJldHN1YgoKLy8gY3JlYXRlCmNyZWF0ZV83Ogpwcm90byAwIDAKaW50Y18xIC8vIDEKcmV0dXJuCgovLyB1cGRhdGUKdXBkYXRlXzg6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmdsb2JhbCBDcmVhdG9yQWRkcmVzcwo9PQovLyB1bmF1dGhvcml6ZWQKYXNzZXJ0CmludGNfMSAvLyAxCnJldHVybgoKLy8gZGVsZXRlCmRlbGV0ZV85Ogpwcm90byAwIDAKdHhuIFNlbmRlcgpnbG9iYWwgQ3JlYXRvckFkZHJlc3MKPT0KLy8gdW5hdXRob3JpemVkCmFzc2VydAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIG9wdF9pbgpvcHRpbl8xMDoKcHJvdG8gMCAwCmludGNfMSAvLyAxCnJldHVybgoKLy8gY2FsbF9hYmlfY2FzdGVyCmNhbGxhYmljYXN0ZXJfMTE6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpjYWxsc3ViIGNhbGxhYmlfMApmcmFtZV9idXJ5IDAKYnl0ZWNfMSAvLyAweDE1MWY3Yzc1CmZyYW1lX2RpZyAwCmNvbmNhdApsb2cKcmV0c3ViCgovLyBjYWxsX2FiaV9mb3JlaWduX3JlZnNfY2FzdGVyCmNhbGxhYmlmb3JlaWducmVmc2Nhc3Rlcl8xMjoKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gIiIKY2FsbHN1YiBjYWxsYWJpZm9yZWlnbnJlZnNfMgpmcmFtZV9idXJ5IDAKYnl0ZWNfMSAvLyAweDE1MWY3Yzc1CmZyYW1lX2RpZyAwCmNvbmNhdApsb2cKcmV0c3ViCgovLyBzZXRfZ2xvYmFsX2Nhc3RlcgpzZXRnbG9iYWxjYXN0ZXJfMTM6CnByb3RvIDAgMAppbnRjXzAgLy8gMApkdXAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpidG9pCmZyYW1lX2J1cnkgMAp0eG5hIEFwcGxpY2F0aW9uQXJncyAyCmJ0b2kKZnJhbWVfYnVyeSAxCnR4bmEgQXBwbGljYXRpb25BcmdzIDMKZnJhbWVfYnVyeSAyCnR4bmEgQXBwbGljYXRpb25BcmdzIDQKZnJhbWVfYnVyeSAzCmZyYW1lX2RpZyAwCmZyYW1lX2RpZyAxCmZyYW1lX2RpZyAyCmZyYW1lX2RpZyAzCmNhbGxzdWIgc2V0Z2xvYmFsXzMKcmV0c3ViCgovLyBzZXRfbG9jYWxfY2FzdGVyCnNldGxvY2FsY2FzdGVyXzE0Ogpwcm90byAwIDAKaW50Y18wIC8vIDAKZHVwCmJ5dGVjXzAgLy8gIiIKZHVwCnR4bmEgQXBwbGljYXRpb25BcmdzIDEKYnRvaQpmcmFtZV9idXJ5IDAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgpidG9pCmZyYW1lX2J1cnkgMQp0eG5hIEFwcGxpY2F0aW9uQXJncyAzCmZyYW1lX2J1cnkgMgp0eG5hIEFwcGxpY2F0aW9uQXJncyA0CmZyYW1lX2J1cnkgMwpmcmFtZV9kaWcgMApmcmFtZV9kaWcgMQpmcmFtZV9kaWcgMgpmcmFtZV9kaWcgMwpjYWxsc3ViIHNldGxvY2FsXzQKcmV0c3ViCgovLyBzZXRfYm94X2Nhc3RlcgpzZXRib3hjYXN0ZXJfMTU6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMAp0eG5hIEFwcGxpY2F0aW9uQXJncyAyCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMApmcmFtZV9kaWcgMQpjYWxsc3ViIHNldGJveF81CnJldHN1YgoKLy8gZXJyb3JfY2FzdGVyCmVycm9yY2FzdGVyXzE2Ogpwcm90byAwIDAKY2FsbHN1YiBlcnJvcl82CnJldHN1YgoKLy8gb3B0X2luX2Nhc3RlcgpvcHRpbmNhc3Rlcl8xNzoKcHJvdG8gMCAwCmNhbGxzdWIgb3B0aW5fMTAKcmV0c3Vi", + "approval": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDAgMSAxMApieXRlY2Jsb2NrIDB4IDB4MTUxZjdjNzUKdHhuIE51bUFwcEFyZ3MKaW50Y18wIC8vIDAKPT0KYm56IG1haW5fbDE4CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4ZjE3ZTgwYTUgLy8gImNhbGxfYWJpKHN0cmluZylzdHJpbmciCj09CmJueiBtYWluX2wxNwp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGFkNzU2MDJjIC8vICJjYWxsX2FiaV9mb3JlaWduX3JlZnMoKXN0cmluZyIKPT0KYm56IG1haW5fbDE2CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4YTRjZjhkZWEgLy8gInNldF9nbG9iYWwodWludDY0LHVpbnQ2NCxzdHJpbmcsYnl0ZVs0XSl2b2lkIgo9PQpibnogbWFpbl9sMTUKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhjZWMyODM0YSAvLyAic2V0X2xvY2FsKHVpbnQ2NCx1aW50NjQsc3RyaW5nLGJ5dGVbNF0pdm9pZCIKPT0KYm56IG1haW5fbDE0CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4OThiZTIyMTkgLy8gImlzc3VlX3RyYW5zZmVyX3RvX3NlbmRlcih1aW50NjQpdm9pZCIKPT0KYm56IG1haW5fbDEzCnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4YTRiNGEyMzAgLy8gInNldF9ib3goYnl0ZVs0XSxzdHJpbmcpdm9pZCIKPT0KYm56IG1haW5fbDEyCnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4NDRkMGRhMGQgLy8gImVycm9yKCl2b2lkIgo9PQpibnogbWFpbl9sMTEKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgzMGM2ZDU4YSAvLyAib3B0X2luKCl2b2lkIgo9PQpibnogbWFpbl9sMTAKZXJyCm1haW5fbDEwOgp0eG4gT25Db21wbGV0aW9uCmludGNfMSAvLyBPcHRJbgo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBvcHRpbmNhc3Rlcl8xOQppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTE6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgZXJyb3JjYXN0ZXJfMTgKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDEyOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHNldGJveGNhc3Rlcl8xNwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTM6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgaXNzdWV0cmFuc2ZlcnRvc2VuZGVyY2FzdGVyXzE2CmludGNfMSAvLyAxCnJldHVybgptYWluX2wxNDoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBzZXRsb2NhbGNhc3Rlcl8xNQppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTU6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgc2V0Z2xvYmFsY2FzdGVyXzE0CmludGNfMSAvLyAxCnJldHVybgptYWluX2wxNjoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjYWxsYWJpZm9yZWlnbnJlZnNjYXN0ZXJfMTMKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDE3Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGNhbGxhYmljYXN0ZXJfMTIKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDE4Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CmJueiBtYWluX2wyNgp0eG4gT25Db21wbGV0aW9uCmludGNfMSAvLyBPcHRJbgo9PQpibnogbWFpbl9sMjUKdHhuIE9uQ29tcGxldGlvbgpwdXNoaW50IDQgLy8gVXBkYXRlQXBwbGljYXRpb24KPT0KYm56IG1haW5fbDI0CnR4biBPbkNvbXBsZXRpb24KcHVzaGludCA1IC8vIERlbGV0ZUFwcGxpY2F0aW9uCj09CmJueiBtYWluX2wyMwplcnIKbWFpbl9sMjM6CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CmFzc2VydApjYWxsc3ViIGRlbGV0ZV8xMAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjQ6CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CmFzc2VydApjYWxsc3ViIHVwZGF0ZV85CmludGNfMSAvLyAxCnJldHVybgptYWluX2wyNToKdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KYXNzZXJ0CmNhbGxzdWIgY3JlYXRlXzgKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDI2Ogp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAo9PQphc3NlcnQKY2FsbHN1YiBjcmVhdGVfOAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNhbGxfYWJpCmNhbGxhYmlfMDoKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKcHVzaGJ5dGVzIDB4NDg2NTZjNmM2ZjJjMjAgLy8gIkhlbGxvLCAiCmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApjb25jYXQKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBpdG9hCml0b2FfMToKcHJvdG8gMSAxCmZyYW1lX2RpZyAtMQppbnRjXzAgLy8gMAo9PQpibnogaXRvYV8xX2w1CmZyYW1lX2RpZyAtMQppbnRjXzIgLy8gMTAKLwppbnRjXzAgLy8gMAo+CmJueiBpdG9hXzFfbDQKYnl0ZWNfMCAvLyAiIgppdG9hXzFfbDM6CnB1c2hieXRlcyAweDMwMzEzMjMzMzQzNTM2MzczODM5IC8vICIwMTIzNDU2Nzg5IgpmcmFtZV9kaWcgLTEKaW50Y18yIC8vIDEwCiUKaW50Y18xIC8vIDEKZXh0cmFjdDMKY29uY2F0CmIgaXRvYV8xX2w2Cml0b2FfMV9sNDoKZnJhbWVfZGlnIC0xCmludGNfMiAvLyAxMAovCmNhbGxzdWIgaXRvYV8xCmIgaXRvYV8xX2wzCml0b2FfMV9sNToKcHVzaGJ5dGVzIDB4MzAgLy8gIjAiCml0b2FfMV9sNjoKcmV0c3ViCgovLyBjYWxsX2FiaV9mb3JlaWduX3JlZnMKY2FsbGFiaWZvcmVpZ25yZWZzXzI6CnByb3RvIDAgMQpieXRlY18wIC8vICIiCnB1c2hieXRlcyAweDQxNzA3MDNhMjAgLy8gIkFwcDogIgp0eG5hIEFwcGxpY2F0aW9ucyAxCmNhbGxzdWIgaXRvYV8xCmNvbmNhdApwdXNoYnl0ZXMgMHgyYzIwNDE3MzczNjU3NDNhMjAgLy8gIiwgQXNzZXQ6ICIKY29uY2F0CnR4bmEgQXNzZXRzIDAKY2FsbHN1YiBpdG9hXzEKY29uY2F0CnB1c2hieXRlcyAweDJjMjA0MTYzNjM2Zjc1NmU3NDNhMjAgLy8gIiwgQWNjb3VudDogIgpjb25jYXQKdHhuYSBBY2NvdW50cyAwCmludGNfMCAvLyAwCmdldGJ5dGUKY2FsbHN1YiBpdG9hXzEKY29uY2F0CnB1c2hieXRlcyAweDNhIC8vICI6Igpjb25jYXQKdHhuYSBBY2NvdW50cyAwCmludGNfMSAvLyAxCmdldGJ5dGUKY2FsbHN1YiBpdG9hXzEKY29uY2F0CmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApsZW4KaXRvYgpleHRyYWN0IDYgMApmcmFtZV9kaWcgMApjb25jYXQKZnJhbWVfYnVyeSAwCnJldHN1YgoKLy8gc2V0X2dsb2JhbApzZXRnbG9iYWxfMzoKcHJvdG8gNCAwCnB1c2hieXRlcyAweDY5NmU3NDMxIC8vICJpbnQxIgpmcmFtZV9kaWcgLTQKYXBwX2dsb2JhbF9wdXQKcHVzaGJ5dGVzIDB4Njk2ZTc0MzIgLy8gImludDIiCmZyYW1lX2RpZyAtMwphcHBfZ2xvYmFsX3B1dApwdXNoYnl0ZXMgMHg2Mjc5NzQ2NTczMzEgLy8gImJ5dGVzMSIKZnJhbWVfZGlnIC0yCmV4dHJhY3QgMiAwCmFwcF9nbG9iYWxfcHV0CnB1c2hieXRlcyAweDYyNzk3NDY1NzMzMiAvLyAiYnl0ZXMyIgpmcmFtZV9kaWcgLTEKYXBwX2dsb2JhbF9wdXQKcmV0c3ViCgovLyBzZXRfbG9jYWwKc2V0bG9jYWxfNDoKcHJvdG8gNCAwCnR4biBTZW5kZXIKcHVzaGJ5dGVzIDB4NmM2ZjYzNjE2YzVmNjk2ZTc0MzEgLy8gImxvY2FsX2ludDEiCmZyYW1lX2RpZyAtNAphcHBfbG9jYWxfcHV0CnR4biBTZW5kZXIKcHVzaGJ5dGVzIDB4NmM2ZjYzNjE2YzVmNjk2ZTc0MzIgLy8gImxvY2FsX2ludDIiCmZyYW1lX2RpZyAtMwphcHBfbG9jYWxfcHV0CnR4biBTZW5kZXIKcHVzaGJ5dGVzIDB4NmM2ZjYzNjE2YzVmNjI3OTc0NjU3MzMxIC8vICJsb2NhbF9ieXRlczEiCmZyYW1lX2RpZyAtMgpleHRyYWN0IDIgMAphcHBfbG9jYWxfcHV0CnR4biBTZW5kZXIKcHVzaGJ5dGVzIDB4NmM2ZjYzNjE2YzVmNjI3OTc0NjU3MzMyIC8vICJsb2NhbF9ieXRlczIiCmZyYW1lX2RpZyAtMQphcHBfbG9jYWxfcHV0CnJldHN1YgoKLy8gaXNzdWVfdHJhbnNmZXJfdG9fc2VuZGVyCmlzc3VldHJhbnNmZXJ0b3NlbmRlcl81Ogpwcm90byAxIDAKaXR4bl9iZWdpbgppbnRjXzEgLy8gcGF5Cml0eG5fZmllbGQgVHlwZUVudW0KZnJhbWVfZGlnIC0xCml0eG5fZmllbGQgQW1vdW50CnR4biBTZW5kZXIKaXR4bl9maWVsZCBSZWNlaXZlcgppbnRjXzAgLy8gMAppdHhuX2ZpZWxkIEZlZQppdHhuX3N1Ym1pdApyZXRzdWIKCi8vIHNldF9ib3gKc2V0Ym94XzY6CnByb3RvIDIgMApmcmFtZV9kaWcgLTIKYm94X2RlbApwb3AKZnJhbWVfZGlnIC0yCmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApib3hfcHV0CnJldHN1YgoKLy8gZXJyb3IKZXJyb3JfNzoKcHJvdG8gMCAwCmludGNfMCAvLyAwCi8vIERlbGliZXJhdGUgZXJyb3IKYXNzZXJ0CnJldHN1YgoKLy8gY3JlYXRlCmNyZWF0ZV84Ogpwcm90byAwIDAKaW50Y18xIC8vIDEKcmV0dXJuCgovLyB1cGRhdGUKdXBkYXRlXzk6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmdsb2JhbCBDcmVhdG9yQWRkcmVzcwo9PQovLyB1bmF1dGhvcml6ZWQKYXNzZXJ0CmludGNfMSAvLyAxCnJldHVybgoKLy8gZGVsZXRlCmRlbGV0ZV8xMDoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBvcHRfaW4Kb3B0aW5fMTE6CnByb3RvIDAgMAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNhbGxfYWJpX2Nhc3RlcgpjYWxsYWJpY2FzdGVyXzEyOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiBjYWxsYWJpXzAKZnJhbWVfYnVyeSAwCmJ5dGVjXzEgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gY2FsbF9hYmlfZm9yZWlnbl9yZWZzX2Nhc3RlcgpjYWxsYWJpZm9yZWlnbnJlZnNjYXN0ZXJfMTM6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmNhbGxzdWIgY2FsbGFiaWZvcmVpZ25yZWZzXzIKZnJhbWVfYnVyeSAwCmJ5dGVjXzEgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gc2V0X2dsb2JhbF9jYXN0ZXIKc2V0Z2xvYmFsY2FzdGVyXzE0Ogpwcm90byAwIDAKaW50Y18wIC8vIDAKZHVwCmJ5dGVjXzAgLy8gIiIKZHVwCnR4bmEgQXBwbGljYXRpb25BcmdzIDEKYnRvaQpmcmFtZV9idXJ5IDAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgpidG9pCmZyYW1lX2J1cnkgMQp0eG5hIEFwcGxpY2F0aW9uQXJncyAzCmZyYW1lX2J1cnkgMgp0eG5hIEFwcGxpY2F0aW9uQXJncyA0CmZyYW1lX2J1cnkgMwpmcmFtZV9kaWcgMApmcmFtZV9kaWcgMQpmcmFtZV9kaWcgMgpmcmFtZV9kaWcgMwpjYWxsc3ViIHNldGdsb2JhbF8zCnJldHN1YgoKLy8gc2V0X2xvY2FsX2Nhc3RlcgpzZXRsb2NhbGNhc3Rlcl8xNToKcHJvdG8gMCAwCmludGNfMCAvLyAwCmR1cApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmJ0b2kKZnJhbWVfYnVyeSAwCnR4bmEgQXBwbGljYXRpb25BcmdzIDIKYnRvaQpmcmFtZV9idXJ5IDEKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMwpmcmFtZV9idXJ5IDIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgNApmcmFtZV9idXJ5IDMKZnJhbWVfZGlnIDAKZnJhbWVfZGlnIDEKZnJhbWVfZGlnIDIKZnJhbWVfZGlnIDMKY2FsbHN1YiBzZXRsb2NhbF80CnJldHN1YgoKLy8gaXNzdWVfdHJhbnNmZXJfdG9fc2VuZGVyX2Nhc3Rlcgppc3N1ZXRyYW5zZmVydG9zZW5kZXJjYXN0ZXJfMTY6CnByb3RvIDAgMAppbnRjXzAgLy8gMAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmJ0b2kKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmNhbGxzdWIgaXNzdWV0cmFuc2ZlcnRvc2VuZGVyXzUKcmV0c3ViCgovLyBzZXRfYm94X2Nhc3RlcgpzZXRib3hjYXN0ZXJfMTc6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMAp0eG5hIEFwcGxpY2F0aW9uQXJncyAyCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMApmcmFtZV9kaWcgMQpjYWxsc3ViIHNldGJveF82CnJldHN1YgoKLy8gZXJyb3JfY2FzdGVyCmVycm9yY2FzdGVyXzE4Ogpwcm90byAwIDAKY2FsbHN1YiBlcnJvcl83CnJldHN1YgoKLy8gb3B0X2luX2Nhc3RlcgpvcHRpbmNhc3Rlcl8xOToKcHJvdG8gMCAwCmNhbGxzdWIgb3B0aW5fMTEKcmV0c3Vi", "clear": "I3ByYWdtYSB2ZXJzaW9uIDgKcHVzaGludCAwIC8vIDAKcmV0dXJu" }, "state": { @@ -206,6 +211,18 @@ export const APP_SPEC: AppSpec = { "type": "void" } }, + { + "name": "issue_transfer_to_sender", + "args": [ + { + "type": "uint64", + "name": "amount" + } + ], + "returns": { + "type": "void" + } + }, { "name": "set_box", "args": [ @@ -335,6 +352,13 @@ export type TestingApp = { argsTuple: [int1: bigint | number, int2: bigint | number, bytes1: string, bytes2: Uint8Array] returns: void }> + & Record<'issue_transfer_to_sender(uint64)void' | 'issue_transfer_to_sender', { + argsObj: { + amount: bigint | number + } + argsTuple: [amount: bigint | number] + returns: void + }> & Record<'set_box(byte[4],string)void' | 'set_box', { argsObj: { name: Uint8Array @@ -590,6 +614,20 @@ export abstract class TestingAppCallFactory { ...params, } } + /** + * Constructs a no op call for the issue_transfer_to_sender(uint64)void ABI method + * + * @param args Any args for the contract call + * @param params Any additional parameters for the call + * @returns A TypedCallParams object for the call + */ + static issueTransferToSender(args: MethodArgs<'issue_transfer_to_sender(uint64)void'>, params: AppClientCallCoreParams & CoreAppCallArgs) { + return { + method: 'issue_transfer_to_sender(uint64)void' as const, + methodArgs: Array.isArray(args) ? args : [args.amount], + ...params, + } + } /** * Constructs a no op call for the set_box(byte[4],string)void ABI method * @@ -819,6 +857,17 @@ export class TestingAppClient { return this.call(TestingAppCallFactory.setLocal(args, params)) } + /** + * Calls the issue_transfer_to_sender(uint64)void ABI method. + * + * @param args The arguments for the contract call + * @param params Any additional parameters for the call + * @returns The result of the call + */ + public issueTransferToSender(args: MethodArgs<'issue_transfer_to_sender(uint64)void'>, params: AppClientCallCoreParams & CoreAppCallArgs = {}) { + return this.call(TestingAppCallFactory.issueTransferToSender(args, params)) + } + /** * Calls the set_box(byte[4],string)void ABI method. * @@ -958,6 +1007,11 @@ export class TestingAppClient { resultMappers.push(undefined) return this }, + issueTransferToSender(args: MethodArgs<'issue_transfer_to_sender(uint64)void'>, params?: AppClientCallCoreParams & CoreAppCallArgs) { + promiseChain = promiseChain.then(() => client.issueTransferToSender(args, {...params, sendParams: {...params?.sendParams, skipSending: true, atc}})) + resultMappers.push(undefined) + return this + }, setBox(args: MethodArgs<'set_box(byte[4],string)void'>, params?: AppClientCallCoreParams & CoreAppCallArgs) { promiseChain = promiseChain.then(() => client.setBox(args, {...params, sendParams: {...params?.sendParams, skipSending: true, atc}})) resultMappers.push(undefined) @@ -1059,6 +1113,15 @@ export type TestingAppComposer = { */ setLocal(args: MethodArgs<'set_local(uint64,uint64,string,byte[4])void'>, params?: AppClientCallCoreParams & CoreAppCallArgs): TestingAppComposer<[...TReturns, MethodReturn<'set_local(uint64,uint64,string,byte[4])void'>]> + /** + * Calls the issue_transfer_to_sender(uint64)void ABI method. + * + * @param args The arguments for the contract call + * @param params Any additional parameters for the call + * @returns The typed transaction composer so you can fluently chain multiple calls or call execute to execute all queued up transactions + */ + issueTransferToSender(args: MethodArgs<'issue_transfer_to_sender(uint64)void'>, params?: AppClientCallCoreParams & CoreAppCallArgs): TestingAppComposer<[...TReturns, MethodReturn<'issue_transfer_to_sender(uint64)void'>]> + /** * Calls the set_box(byte[4],string)void ABI method. * diff --git a/tests/contract/contract.py b/tests/contract/contract.py index b82bc7c3..31573f8b 100644 --- a/tests/contract/contract.py +++ b/tests/contract/contract.py @@ -81,6 +81,18 @@ def set_local( ) +@app.external() +def issue_transfer_to_sender(amount: pt.abi.Uint64) -> pt.Expr: + return pt.InnerTxnBuilder.Execute( + { + pt.TxnField.type_enum: pt.TxnType.Payment, + pt.TxnField.amount: amount.get(), + pt.TxnField.receiver: pt.Txn.sender(), + pt.TxnField.fee: pt.Int(0), + } + ) + + @app.external() def set_box(name: pt.abi.StaticBytes[Literal[4]], value: pt.abi.String) -> pt.Expr: return app.state.box[name.get()].set(value.get()) diff --git a/tests/scenarios/inner_transactions.spec.ts b/tests/scenarios/inner_transactions.spec.ts new file mode 100644 index 00000000..ad04e9d1 --- /dev/null +++ b/tests/scenarios/inner_transactions.spec.ts @@ -0,0 +1,164 @@ +import * as algokit from '@algorandfoundation/algokit-utils' +import { algorandFixture } from '@algorandfoundation/algokit-utils/testing' +import { SendAtomicTransactionComposerResults, SendTransactionResult } from '@algorandfoundation/algokit-utils/types/transaction' +import { beforeEach, describe, test } from '@jest/globals' +import algosdk, { Account, Transaction, TransactionType } from 'algosdk' +import { TransactionFilter } from '../../src/types/subscription' +import { TestingAppClient } from '../contract/client' +import { GetSubscribedTransactions, SendXTransactions } from '../transactions' + +describe('Inner transactions', () => { + const localnet = algorandFixture() + let systemAccount: Account + + beforeAll(async () => { + await localnet.beforeEach() + systemAccount = await localnet.context.generateAccount({ initialFunds: (100).algos() }) + }) + + beforeEach(localnet.beforeEach, 10e6) + afterEach(() => { + jest.clearAllMocks() + }) + + const subscribeAndVerifyFilter = async (filter: TransactionFilter, ...result: SendTransactionResult[]) => { + // Ensure there is another transaction so algod subscription can process something + await SendXTransactions(1, systemAccount, localnet.context.algod) + // Wait for indexer to catch up + await localnet.context.waitForIndexerTransaction(result[result.length - 1].transaction.txID()) + // Run the subscription twice - once that will pick up using algod and once using indexer + // this allows the filtering logic for both to be tested + const [algod, indexer] = await Promise.all([ + GetSubscribedTransactions( + { + roundsToSync: 1, + syncBehaviour: 'sync-oldest', + watermark: Number(result[result.length - 1].confirmation?.confirmedRound) - 1, + currentRound: Number(result[result.length - 1].confirmation?.confirmedRound), + filter, + }, + localnet.context.algod, + ), + GetSubscribedTransactions( + { + roundsToSync: 1, + syncBehaviour: 'catchup-with-indexer', + watermark: 0, + currentRound: Number(result[result.length - 1].confirmation?.confirmedRound) + 1, + filter, + }, + localnet.context.algod, + localnet.context.indexer, + ), + ]) + expect(algod.subscribedTransactions.length).toBe(result.length) + expect(algod.subscribedTransactions.map((t) => t.id)).toEqual(result.map((r) => r.transaction.txID())) + expect(indexer.subscribedTransactions.length).toBe(result.length) + expect(indexer.subscribedTransactions.map((t) => t.id)).toEqual(result.map((r) => r.transaction.txID())) + return { algod, indexer } + } + + const extractFromGroupResult = ( + groupResult: Omit, + index: number, + innerTransactionIndex?: number, + ) => { + return { + transaction: innerTransactionIndex + ? Transaction.from_obj_for_encoding(groupResult.confirmations![index].innerTxns![innerTransactionIndex].txn.txn) + : groupResult.transactions[index], + confirmation: groupResult.confirmations?.[index], + } + } + + const createAsset = async (creator?: Account) => { + const create = await algokit.sendTransaction( + { + from: creator ?? systemAccount, + transaction: await createAssetTxn(creator ?? systemAccount), + }, + localnet.context.algod, + ) + + return { + assetId: Number(create.confirmation!.assetIndex!), + ...create, + } + } + + const createAssetTxn = async (creator: Account) => { + return algosdk.makeAssetCreateTxnWithSuggestedParamsFromObject({ + from: creator ? creator.addr : systemAccount.addr, + decimals: 0, + total: 100, + defaultFrozen: false, + suggestedParams: await algokit.getTransactionParams(undefined, localnet.context.algod), + }) + } + + const app = async (config: { create: boolean }, creator?: Account) => { + const app = new TestingAppClient( + { + resolveBy: 'id', + id: 0, + }, + localnet.context.algod, + ) + const creation = await app.create.bare({ + sender: creator ?? systemAccount, + sendParams: { + skipSending: !config.create, + }, + }) + + return { + app, + creation, + } + } + + test('Is processed alongside normal transaction', async () => { + const { testAccount, algod } = localnet.context + const app1 = await app({ create: true }) + const txns = await algokit.sendGroupOfTransactions( + { + transactions: [ + algokit.transferAlgos( + { + amount: (100_001).microAlgos(), + to: (await app1.app.appClient.getAppReference()).appAddress, + from: testAccount, + skipSending: true, + }, + algod, + ), + algokit.transferAlgos( + { + amount: (1).microAlgos(), + to: testAccount, + from: testAccount, + skipSending: true, + }, + algod, + ), + app1.app.issueTransferToSender( + { amount: 1 }, + { sender: testAccount, sendParams: { skipSending: true, fee: (2000).microAlgos() } }, + ), + ], + signer: testAccount, + }, + algod, + ) + + await subscribeAndVerifyFilter( + { + type: TransactionType.pay, + receiver: testAccount.addr, + maxAmount: 1, + }, + extractFromGroupResult(txns, 1), + extractFromGroupResult(txns, 2, 0), + ) + }) +}) From 43297d383cbde9e4e79bffa81343e6ca8aa057fd Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Sun, 21 Jan 2024 17:42:48 +0800 Subject: [PATCH 2/5] Got inner transactions working --- jest.config.ts | 1 + package-lock.json | 17 + package.json | 1 + src/subscriptions.ts | 94 ++- src/transform.ts | 169 +++-- src/types/block.ts | 6 +- tests/scenarios/inner_transactions.spec.ts | 25 +- tests/scenarios/transform-complex-txn.spec.ts | 589 ++++++++++++++++++ 8 files changed, 832 insertions(+), 70 deletions(-) create mode 100644 tests/scenarios/transform-complex-txn.spec.ts diff --git a/jest.config.ts b/jest.config.ts index 753a4ec7..db0a413f 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -17,5 +17,6 @@ const config: Config.InitialOptions = { }, coveragePathIgnorePatterns: ['tests'], testPathIgnorePatterns: ['node_modules'], + prettierPath: require.resolve('prettier-2'), } export default config diff --git a/package-lock.json b/package-lock.json index fc9fd020..0a4f750d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "eslint": "8.54.0", "npm-run-all": "^4.1.5", "prettier": "3.1.0", + "prettier-2": "npm:prettier@^2", "rimraf": "^5.0.5", "semantic-release": "^22.0.8", "ts-jest": "^29.1.1", @@ -11982,6 +11983,22 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-2": { + "name": "prettier", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/prettier-linter-helpers": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", diff --git a/package.json b/package.json index 8429e424..0e0dc6fa 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "eslint": "8.54.0", "npm-run-all": "^4.1.5", "prettier": "3.1.0", + "prettier-2": "npm:prettier@^2", "rimraf": "^5.0.5", "semantic-release": "^22.0.8", "ts-jest": "^29.1.1", diff --git a/src/subscriptions.ts b/src/subscriptions.ts index 5f51911e..79dd5070 100644 --- a/src/subscriptions.ts +++ b/src/subscriptions.ts @@ -3,11 +3,7 @@ import type { TransactionResult } from '@algorandfoundation/algokit-utils/types/ import { Algodv2, Indexer, Transaction, encodeAddress } from 'algosdk' import type SearchForTransactions from 'algosdk/dist/types/client/v2/indexer/searchForTransactions' import sha512 from 'js-sha512' -import { - algodOnCompleteToIndexerOnComplete, - getAlgodTransactionsFromBlockTransaction, - getIndexerTransactionFromAlgodTransaction, -} from './transform' +import { algodOnCompleteToIndexerOnComplete, getBlockTransactions, getIndexerTransactionFromAlgodTransaction } from './transform' import type { Block } from './types/block' import type { TransactionFilter, TransactionSubscriptionParams, TransactionSubscriptionResult } from './types/subscription' import { chunkArray, range } from './utils' @@ -77,6 +73,7 @@ export async function getSubscribedTransactions( catchupTransactions.push( ...(await algokit.searchTransactions(indexer, indexerPreFilter(filter, startRound, algodSyncFromRoundNumber - 1))).transactions + .flatMap((t) => getFilteredIndexerTransactions(t, filter)) .filter(indexerPostFilter(filter)) .sort((a, b) => a['confirmed-round']! - b['confirmed-round']! || a['intra-round-offset']! - b['intra-round-offset']!), ) @@ -105,19 +102,9 @@ export async function getSubscribedTransactions( currentRound, subscribedTransactions: catchupTransactions.concat( blocks - .flatMap((b) => b.block.txns?.flatMap((t) => getAlgodTransactionsFromBlockTransaction(t, b.block)).filter((t) => !!t) ?? []) + .flatMap((b) => getBlockTransactions(b.block)) .filter((t) => transactionFilter(filter, t!.createdAssetId, t!.createdAppId)(t!)) - .map((t) => - getIndexerTransactionFromAlgodTransaction( - t!.transaction, - t!.block, - t!.blockOffset, - t?.createdAssetId, - t?.createdAppId, - t?.assetCloseAmount, - t?.closeAmount, - ), - ), + .map((t) => getIndexerTransactionFromAlgodTransaction(t)), ), } } @@ -128,6 +115,7 @@ function indexerPreFilter( maxRound: number, ): (s: SearchForTransactions) => SearchForTransactions { return (s) => { + // NOTE: everything in this method needs to be mirrored to `indexerPreFilterInMemory` below let filter = s if (subscription.sender) { filter = filter.address(subscription.sender).addressRole('sender') @@ -157,6 +145,51 @@ function indexerPreFilter( } } +function indexerPreFilterInMemory(subscription: TransactionFilter): (t: TransactionResult) => boolean { + return (t) => { + let result = true + if (subscription.sender) { + result &&= t.sender === subscription.sender + } + if (subscription.receiver) { + result &&= + (!!t['asset-transfer-transaction'] && t['asset-transfer-transaction'].receiver === subscription.receiver) || + (!!t['payment-transaction'] && t['payment-transaction'].receiver === subscription.receiver) + } + if (subscription.type) { + result &&= t['tx-type'] === subscription.type + } + if (subscription.notePrefix) { + result &&= t.note ? Buffer.from(t.note, 'base64').toString('utf-8').startsWith(subscription.notePrefix) : false + } + if (subscription.appId) { + result &&= + t['created-application-index'] === subscription.appId || + (!!t['application-transaction'] && t['application-transaction']['application-id'] === subscription.appId) + } + if (subscription.assetId) { + result &&= + t['created-asset-index'] === subscription.assetId || + (!!t['asset-config-transaction'] && t['asset-config-transaction']['asset-id'] === subscription.assetId) || + (!!t['asset-freeze-transaction'] && t['asset-freeze-transaction']['asset-id'] === subscription.assetId) || + (!!t['asset-transfer-transaction'] && t['asset-transfer-transaction']['asset-id'] === subscription.assetId) + } + + if (subscription.minAmount) { + result &&= + (!!t['payment-transaction'] && t['payment-transaction'].amount >= subscription.minAmount) || + (!!t['asset-transfer-transaction'] && t['asset-transfer-transaction'].amount >= subscription.minAmount) + } + if (subscription.maxAmount) { + result &&= + (!!t['payment-transaction'] && t['payment-transaction'].amount <= subscription.maxAmount) || + (!!t['asset-transfer-transaction'] && t['asset-transfer-transaction'].amount <= subscription.maxAmount) + } + + return result + } +} + function indexerPostFilter(subscription: TransactionFilter): (t: TransactionResult) => boolean { return (t) => { let result = true @@ -219,10 +252,10 @@ function transactionFilter( result &&= !!t.note && new TextDecoder().decode(t.note).startsWith(subscription.notePrefix) } if (subscription.appId) { - result &&= t.appIndex === subscription.appId + result &&= t.appIndex === subscription.appId || createdAppId === subscription.appId } if (subscription.assetId) { - result &&= t.assetIndex === subscription.assetId + result &&= t.assetIndex === subscription.assetId || createdAssetId === subscription.assetId } if (subscription.minAmount) { result &&= t.amount >= subscription.minAmount @@ -279,3 +312,26 @@ export async function getBlocksBulk(context: { startRound: number; maxRound: num } return blocks } + +/** Process an indexer transaction and return that transaction or any of it's inner transactions that meet the indexer pre-filter requirements; patching up transaction ID and intra-round-offset on the way through */ +function getFilteredIndexerTransactions(transaction: TransactionResult, filter: TransactionFilter): TransactionResult[] { + let parentOffset = 0 + const getParentOffset = () => parentOffset++ + + const transactions = [transaction, ...getIndexerInnerTransactions(transaction, transaction, getParentOffset)] + return transactions.filter(indexerPreFilterInMemory(filter)) +} + +function getIndexerInnerTransactions(root: TransactionResult, parent: TransactionResult, offset: () => number): TransactionResult[] { + return (parent['inner-txns'] ?? []).flatMap((t) => { + const parentOffset = offset() + return [ + { + ...t, + id: `${root.id}/inner/${parentOffset + 1}`, + 'intra-round-offset': root['intra-round-offset']! + parentOffset + 1, + }, + ...getIndexerInnerTransactions(root, t, offset), + ] + }) +} diff --git a/src/transform.ts b/src/transform.ts index ddeaac93..41a0bb89 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -2,7 +2,7 @@ import type { TransactionResult } from '@algorandfoundation/algokit-utils/types/ import { ApplicationOnComplete } from '@algorandfoundation/algokit-utils/types/indexer' import algosdk, { OnApplicationComplete, Transaction, TransactionType } from 'algosdk' import { Buffer } from 'buffer' -import type { Block, BlockTransaction } from './types/block' +import type { Block, BlockInnerTransaction, BlockTransaction } from './types/block' // Recursively remove all null values from object // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -17,27 +17,112 @@ function removeNulls(obj: any) { } } +export interface TransactionInBlock { + // Raw data + blockTransaction: BlockTransaction | BlockInnerTransaction + block: Block + roundOffset: number + roundIndex: number + parentTransaction?: BlockTransaction + parentTransactionId?: string + parentOffset?: number + + // Processed data + transaction: Transaction + createdAssetId?: number + createdAppId?: number + assetCloseAmount?: number + closeAmount?: number +} + +/** + * Processes a block and returns all transactions from it, including inner transactions, with key information populated. + * @param block An Algorand block + * @returns An array of processed transactions from the block + */ +export function getBlockTransactions(block: Block): TransactionInBlock[] { + let offset = 0 + const getOffset = () => offset++ + + return block.txns.flatMap((blockTransaction, roundIndex) => { + let parentOffset = 0 + const getParentOffset = () => parentOffset++ + const parentData = extractTransactionFromBlockTransaction(blockTransaction, block) + return [ + { + blockTransaction, + block, + roundOffset: getOffset(), + roundIndex, + ...parentData, + } as TransactionInBlock, + ...(blockTransaction.dt?.itx ?? []).flatMap((innerTransaction) => + getBlockInnerTransactions( + innerTransaction, + block, + blockTransaction, + parentData.transaction.txID(), + roundIndex, + getOffset, + getParentOffset, + ), + ), + ] + }) +} + +function getBlockInnerTransactions( + blockTransaction: BlockInnerTransaction, + block: Block, + parentTransaction: BlockTransaction, + parentTransactionId: string, + roundIndex: number, + getRoundOffset: () => number, + getParentOffset: () => number, +): TransactionInBlock[] { + return [ + { + blockTransaction, + block, + roundIndex, + roundOffset: getRoundOffset(), + parentOffset: getParentOffset(), + parentTransaction, + parentTransactionId, + ...extractTransactionFromBlockTransaction(blockTransaction, block), + }, + ...(blockTransaction.dt?.itx ?? []).flatMap((innerInnerTransaction) => + getBlockInnerTransactions( + innerInnerTransaction, + block, + parentTransaction, + parentTransactionId, + roundIndex, + getRoundOffset, + getParentOffset, + ), + ), + ] +} + /** - * Transform a raw block transaction representation into an `algosdk.Transaction` object. + * Transform a raw block transaction representation into a `algosdk.Transaction` object and other key transaction data. * * **Note:** Doesn't currently support `keyreg` (Key Registration) or `stpf` (State Proof) transactions. * @param blockTransaction The raw transaction from a block * @param block The block the transaction belongs to * @returns The `algosdk.Transaction` object along with key secondary information from the block. */ -export function getAlgodTransactionsFromBlockTransaction( - blockTransaction: BlockTransaction, +export function extractTransactionFromBlockTransaction( + blockTransaction: BlockTransaction | BlockInnerTransaction, block: Block, - blockOffset?: number, ): { transaction: Transaction createdAssetId?: number createdAppId?: number assetCloseAmount?: number closeAmount?: number - block: Block - blockOffset: number -}[] { +} { const txn = blockTransaction.txn // https://github.com/algorand/js-algorand-sdk/blob/develop/examples/block_fetcher/index.ts @@ -47,30 +132,24 @@ export function getAlgodTransactionsFromBlockTransaction( txn.gen = block.gen // Unset gen if `hgi` isn't set // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (!blockTransaction.hgi) txn.gen = null as any - // Unset gen if `hgh` is set to false + if ('hgi' in blockTransaction && !blockTransaction.hgi) txn.gen = null as any + // Unset gh if `hgh` is set to false // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (blockTransaction.hgh === false) txn.gh = null as any + if ('hgh' in blockTransaction && blockTransaction.hgh === false) txn.gh = null as any - // todo: support these? + // todo: support these if (txn.type === 'stpf' || txn.type === 'keyreg') { - return [] + throw new Error('TODO') } - return [ - { - transaction: Transaction.from_obj_for_encoding(txn), - createdAssetId: blockTransaction.caid, - createdAppId: blockTransaction.apid, - assetCloseAmount: blockTransaction.aca, - closeAmount: blockTransaction.ca, - block: block, - blockOffset: blockOffset ?? block.txns.indexOf(blockTransaction), - }, - ...(blockTransaction.dt?.itx?.flatMap((bt) => - getAlgodTransactionsFromBlockTransaction({ ...bt, hgi: false, hgh: false }, block, blockOffset), - ) ?? []), - ] + const t = Transaction.from_obj_for_encoding(txn) + return { + transaction: t, + createdAssetId: blockTransaction.caid, + createdAppId: blockTransaction.apid, + assetCloseAmount: blockTransaction.aca, + closeAmount: blockTransaction.ca, + } } /** @@ -116,15 +195,20 @@ export function algodOnCompleteToIndexerOnComplete(appOnComplete: OnApplicationC * @param closeAmount The amount of microAlgos that were transferred if the transaction had a close * @returns The indexer transaction formation (`TransactionResult`) */ -export function getIndexerTransactionFromAlgodTransaction( - transaction: Transaction, - block: Block, - blockOffset: number, - createdAssetId?: number, - createdAppId?: number, - assetCloseAmount?: number, - closeAmount?: number, -): TransactionResult { +export function getIndexerTransactionFromAlgodTransaction(t: TransactionInBlock): TransactionResult { + const { + transaction, + createdAssetId, + blockTransaction, + assetCloseAmount, + closeAmount, + createdAppId, + block, + roundOffset, + parentOffset, + parentTransactionId, + } = t + if (!transaction.type) { throw new Error(`Received no transaction type for transaction ${transaction.txID()}`) } @@ -132,8 +216,9 @@ export function getIndexerTransactionFromAlgodTransaction( const encoder = new TextEncoder() const decoder = new TextDecoder() + // https://github.com/algorand/indexer/blob/main/api/converter_utils.go#L249 return { - id: transaction.txID(), + id: parentTransactionId ? `${parentTransactionId}/inner/${parentOffset! + 1}` : transaction.txID(), 'asset-config-transaction': transaction.type === TransactionType.acfg ? { @@ -158,7 +243,7 @@ export function getIndexerTransactionFromAlgodTransaction( clawback: transaction.assetClawback ? algosdk.encodeAddress(transaction.assetClawback.publicKey) : undefined, freeze: transaction.assetFreeze ? algosdk.encodeAddress(transaction.assetFreeze.publicKey) : undefined, } - : 'apar' in block.txns[blockOffset].txn + : 'apar' in blockTransaction.txn ? { manager: transaction.assetManager ? algosdk.encodeAddress(transaction.assetManager.publicKey) : undefined, reserve: transaction.assetReserve ? algosdk.encodeAddress(transaction.assetReserve.publicKey) : undefined, @@ -195,17 +280,17 @@ export function getIndexerTransactionFromAlgodTransaction( 'approval-program': decoder.decode(transaction.appApprovalProgram), 'clear-state-program': decoder.decode(transaction.appClearProgram), 'on-completion': algodOnCompleteToIndexerOnComplete(transaction.appOnComplete), - 'application-args': transaction.appArgs?.map((a) => decoder.decode(a)), + 'application-args': transaction.appArgs?.map((a) => Buffer.from(a).toString('base64')), 'extra-program-pages': transaction.extraPages, 'foreign-apps': transaction.appForeignApps, 'foreign-assets': transaction.appForeignAssets, - 'global-state-schema': block.txns[blockOffset].txn.apgs + 'global-state-schema': blockTransaction.txn.apgs ? { 'num-byte-slice': transaction.appGlobalByteSlices, 'num-uint': transaction.appGlobalInts, } : undefined, - 'local-state-schema': block.txns[blockOffset].txn.apls + 'local-state-schema': blockTransaction.txn.apls ? { 'num-byte-slice': transaction.appLocalByteSlices, 'num-uint': transaction.appLocalInts, @@ -230,7 +315,7 @@ export function getIndexerTransactionFromAlgodTransaction( sender: algosdk.encodeAddress(transaction.from.publicKey), 'confirmed-round': block.rnd, 'round-time': block.ts, - 'intra-round-offset': blockOffset, + 'intra-round-offset': roundOffset, 'created-asset-index': createdAssetId, 'genesis-hash': Buffer.from(transaction.genesisHash).toString('base64'), 'genesis-id': transaction.genesisID, diff --git a/src/types/block.ts b/src/types/block.ts index 98acf167..f09b955c 100644 --- a/src/types/block.ts +++ b/src/types/block.ts @@ -55,6 +55,7 @@ export interface Block { txns: BlockTransaction[] } +/** Data that is returned in a raw Algorand block for a single transaction */ export interface BlockTransaction { /** The encoded transaction data */ txn: EncodedTransaction @@ -74,6 +75,9 @@ export interface BlockTransaction { hgh?: boolean } +/** Data that is returned in a raw Algorand block for a single inner transaction */ +export type BlockInnerTransaction = Omit + /** Eval deltas for a block */ export interface BlockTransactionEvalDelta { /** The delta of global state, keyed by key */ @@ -83,7 +87,7 @@ export interface BlockTransactionEvalDelta { /** Logs */ lg: string[] /** Inner transactions */ - itx?: Omit[] + itx?: BlockInnerTransaction[] } export interface BlockValueDelta { diff --git a/tests/scenarios/inner_transactions.spec.ts b/tests/scenarios/inner_transactions.spec.ts index ad04e9d1..f1171b06 100644 --- a/tests/scenarios/inner_transactions.spec.ts +++ b/tests/scenarios/inner_transactions.spec.ts @@ -21,11 +21,11 @@ describe('Inner transactions', () => { jest.clearAllMocks() }) - const subscribeAndVerifyFilter = async (filter: TransactionFilter, ...result: SendTransactionResult[]) => { + const subscribeAndVerifyFilter = async (filter: TransactionFilter, ...result: (SendTransactionResult & { id: string })[]) => { // Ensure there is another transaction so algod subscription can process something await SendXTransactions(1, systemAccount, localnet.context.algod) // Wait for indexer to catch up - await localnet.context.waitForIndexerTransaction(result[result.length - 1].transaction.txID()) + await localnet.context.waitForIndexer() // Run the subscription twice - once that will pick up using algod and once using indexer // this allows the filtering logic for both to be tested const [algod, indexer] = await Promise.all([ @@ -51,10 +51,11 @@ describe('Inner transactions', () => { localnet.context.indexer, ), ]) + expect(algod.subscribedTransactions.length).toBe(result.length) - expect(algod.subscribedTransactions.map((t) => t.id)).toEqual(result.map((r) => r.transaction.txID())) + expect(algod.subscribedTransactions.map((t) => t.id)).toEqual(result.map((r) => r.id)) expect(indexer.subscribedTransactions.length).toBe(result.length) - expect(indexer.subscribedTransactions.map((t) => t.id)).toEqual(result.map((r) => r.transaction.txID())) + expect(indexer.subscribedTransactions.map((t) => t.id)).toEqual(result.map((r) => r.id)) return { algod, indexer } } @@ -64,9 +65,18 @@ describe('Inner transactions', () => { innerTransactionIndex?: number, ) => { return { - transaction: innerTransactionIndex - ? Transaction.from_obj_for_encoding(groupResult.confirmations![index].innerTxns![innerTransactionIndex].txn.txn) - : groupResult.transactions[index], + id: + innerTransactionIndex !== undefined + ? `${groupResult.transactions[index].txID()}/inner/${innerTransactionIndex + 1}` + : groupResult.transactions[index].txID(), + transaction: + innerTransactionIndex !== undefined + ? Transaction.from_obj_for_encoding({ + ...groupResult.confirmations![index].innerTxns![innerTransactionIndex].txn.txn, + gen: groupResult.confirmations![index].txn.txn.gen, + gh: groupResult.confirmations![index].txn.txn.gh, + }) + : groupResult.transactions[index], confirmation: groupResult.confirmations?.[index], } } @@ -150,7 +160,6 @@ describe('Inner transactions', () => { }, algod, ) - await subscribeAndVerifyFilter( { type: TransactionType.pay, diff --git a/tests/scenarios/transform-complex-txn.spec.ts b/tests/scenarios/transform-complex-txn.spec.ts new file mode 100644 index 00000000..1b584867 --- /dev/null +++ b/tests/scenarios/transform-complex-txn.spec.ts @@ -0,0 +1,589 @@ +import * as algokit from '@algorandfoundation/algokit-utils' +import algosdk, { Transaction } from 'algosdk' +import { getBlocksBulk } from '../../src' +import { TransactionInBlock, getBlockTransactions } from '../../src/transform' +import { GetSubscribedTransactions } from '../transactions' + +function getTransactionInBlockForDiff(transaction: TransactionInBlock) { + return { + transaction: getTransactionForDiff(transaction.transaction), + parentOffset: transaction.parentOffset, + parentTransactionId: transaction.parentTransactionId, + roundIndex: transaction.roundIndex, + roundOffset: transaction.roundOffset, + createdAppId: transaction.createdAppId, + createdAssetId: transaction.createdAppId, + assetCloseAmount: transaction.assetCloseAmount, + closeAmount: transaction.closeAmount, + } +} + +function getTransactionForDiff(transaction: Transaction) { + const t = { + ...transaction, + name: undefined, + appAccounts: transaction.appAccounts?.map((a) => algosdk.encodeAddress(a.publicKey)), + from: algosdk.encodeAddress(transaction.from.publicKey), + to: transaction.to ? algosdk.encodeAddress(transaction.to.publicKey) : undefined, + reKeyTo: transaction.reKeyTo ? algosdk.encodeAddress(transaction.reKeyTo.publicKey) : undefined, + appArgs: transaction.appArgs?.map((a) => Buffer.from(a).toString('base64')), + genesisHash: transaction.genesisHash.toString('base64'), + group: transaction.group ? transaction.group.toString('base64') : undefined, + lease: transaction.lease && transaction.lease.length ? Buffer.from(transaction.lease).toString('base64') : undefined, + note: transaction.note && transaction.note.length ? Buffer.from(transaction.note).toString('base64') : undefined, + tag: transaction.tag.toString('base64'), + } + + // Clear out undefined's + Object.keys(t).forEach((k) => { + if ((t as Record)[k] === undefined) { + delete (t as Record)[k] + } + }) + return t +} + +describe('Complex transaction with many nested inner transactions', () => { + const txnId = 'QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q' + const roundNumber = 35214367 + const algod = algokit.getAlgoClient(algokit.getAlgoNodeConfig('mainnet', 'algod')) + const indexer = algokit.getAlgoIndexerClient(algokit.getAlgoNodeConfig('mainnet', 'indexer')) + + it('Can have an inner transaction subscribed correctly from indexer', async () => { + const indexerTxns = await GetSubscribedTransactions( + { + filter: { + appId: 1390675395, + sender: 'AACCDJTFPQR5UQJZ337NFR56CC44T776EWBGVJG5NY2QFTQWBWTALTEN4A', + }, + roundsToSync: 1, + currentRound: roundNumber + 1, + syncBehaviour: 'catchup-with-indexer', + watermark: roundNumber - 1, + }, + algod, + indexer, + ) + + expect(indexerTxns.subscribedTransactions.length).toBe(1) + const txn = indexerTxns.subscribedTransactions[0] + // https://allo.info/tx/QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q/inner/5 + expect(txn.id).toBe(`${txnId}/inner/5`) + expect(txn).toMatchInlineSnapshot(` + { + "application-transaction": { + "accounts": [], + "application-args": [ + "AA==", + "Aw==", + "AAAAAAAAAAA=", + "BAAAAAAABgTFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + ], + "application-id": 1390675395, + "foreign-apps": [], + "foreign-assets": [ + 1390638935, + ], + "global-state-schema": { + "num-byte-slice": 0, + "num-uint": 0, + }, + "local-state-schema": { + "num-byte-slice": 0, + "num-uint": 0, + }, + "on-completion": "noop", + }, + "close-rewards": 0, + "closing-amount": 0, + "confirmed-round": 35214367, + "fee": 2000, + "first-valid": 35214365, + "global-state-delta": [ + { + "key": "", + "value": { + "action": 1, + "bytes": "AAAAAAAAAAQAAAAAAhlUHw==", + "uint": 0, + }, + }, + { + "key": "AA==", + "value": { + "action": 1, + "bytes": "YC4Bj8ZCXdiWg6+eYEL5yV0gvi3ucnEckrGx2BQXDDIAAAAAUuN3VwAAAAAOsZeDAQAAAABS43dXAAAAAFLkB4YAAAAAAAAAAAAAAAAAAAAA/////5S/nq4AAAAAa0BhUQAAAA91+xl0AAAAAALtZZ8AAAAAAwsGTgAAAAAAAA==", + "uint": 0, + }, + }, + { + "key": "AQ==", + "value": { + "action": 1, + "bytes": "h2MAAAAAAAAABQAAAAAAAAAZAAAAAAAAAB6KqC3yOXMVr2XD4nTi43RC3Rv0AGIvri+ssClC+HVNQgAAAAAAAAAAAA==", + "uint": 0, + }, + }, + ], + "group": "6ZssGapPFZ+DyccRludq0YjZigi05/FSeUAOFNDGGlo=", + "id": "QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q/inner/5", + "inner-txns": [ + { + "asset-transfer-transaction": { + "amount": 536012365, + "asset-id": 1390638935, + "close-amount": 0, + "receiver": "AACCDJTFPQR5UQJZ337NFR56CC44T776EWBGVJG5NY2QFTQWBWTALTEN4A", + }, + "close-rewards": 0, + "closing-amount": 0, + "confirmed-round": 35214367, + "fee": 0, + "first-valid": 35214365, + "intra-round-offset": 142, + "last-valid": 35214369, + "receiver-rewards": 0, + "round-time": 1705252440, + "sender": "RS7QNBEPRRIBGI5COVRWFCRUS5NC5NX7UABZSTSFXQ6F74EP3CNLT4CNAM", + "sender-rewards": 0, + "tx-type": "axfer", + }, + ], + "intra-round-offset": 147, + "last-valid": 35214369, + "logs": [ + "R2hHHwQAAAAAAAYExQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "AAAAAAAAYaAAAAAAH/LmTQAAAAAAAAAA", + "PNZU+gAEIaZlfCPaQTne/tLHvhC5yf/+JYJqpN1uNQLOFg2mAAAAAAAAAAAAAAAAAAYExQAAAAAf8uZNAAAAAAAAAAAAAAAPdfsZdAAAAAAC7WWf", + ], + "receiver-rewards": 0, + "round-time": 1705252440, + "sender": "AACCDJTFPQR5UQJZ337NFR56CC44T776EWBGVJG5NY2QFTQWBWTALTEN4A", + "sender-rewards": 0, + "tx-type": "appl", + } + `) + }) + + it('Can have an inner transaction subscribed correctly from algod', async () => { + const algodTxns = await GetSubscribedTransactions( + { + filter: { + appId: 1390675395, + sender: 'AACCDJTFPQR5UQJZ337NFR56CC44T776EWBGVJG5NY2QFTQWBWTALTEN4A', + }, + roundsToSync: 1, + currentRound: roundNumber + 1, + syncBehaviour: 'sync-oldest', + watermark: roundNumber - 1, + }, + algod, + ) + + expect(algodTxns.subscribedTransactions.length).toBe(1) + const txn = algodTxns.subscribedTransactions[0] + // https://allo.info/tx/QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q/inner/5 + expect(txn.id).toBe(`${txnId}/inner/5`) + expect(txn).toMatchInlineSnapshot(` + { + "application-transaction": { + "accounts": undefined, + "application-args": [ + "AA==", + "Aw==", + "AAAAAAAAAAA=", + "BAAAAAAABgTFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + ], + "application-id": 1390675395, + "approval-program": "", + "clear-state-program": "", + "extra-program-pages": undefined, + "foreign-apps": undefined, + "foreign-assets": [ + 1390638935, + ], + "global-state-schema": undefined, + "local-state-schema": undefined, + "on-completion": "update", + }, + "asset-config-transaction": {}, + "asset-freeze-transaction": undefined, + "asset-transfer-transaction": undefined, + "closing-amount": undefined, + "confirmed-round": 35214367, + "created-application-index": undefined, + "created-asset-index": undefined, + "fee": 2000, + "first-valid": 35214365, + "genesis-hash": "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=", + "genesis-id": "mainnet-v1.0", + "group": "6ZssGapPFZ+DyccRludq0YjZigi05/FSeUAOFNDGGlo=", + "id": "QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q/inner/5", + "intra-round-offset": 147, + "last-valid": 35214369, + "lease": "", + "note": "", + "payment-transaction": undefined, + "rekey-to": undefined, + "round-time": 1705252440, + "sender": "AACCDJTFPQR5UQJZ337NFR56CC44T776EWBGVJG5NY2QFTQWBWTALTEN4A", + "tx-type": "appl", + } + `) + }) + + it('Can be processed correctly from algod raw block', async () => { + const txn = await algokit.lookupTransactionById(txnId, indexer) + const b = (await getBlocksBulk({ startRound: roundNumber, maxRound: roundNumber }, algod))[0] + const intraRoundOffset = txn.transaction['intra-round-offset']! + + const transformed = await getBlockTransactions(b.block) + + const receivedTxn = transformed[intraRoundOffset] + expect(receivedTxn.transaction.txID()).toBe(txnId) + + // https://allo.info/tx/QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q/ + expect(getTransactionInBlockForDiff(receivedTxn)).toMatchInlineSnapshot(` + { + "assetCloseAmount": undefined, + "closeAmount": undefined, + "createdAppId": undefined, + "createdAssetId": undefined, + "parentOffset": undefined, + "parentTransactionId": undefined, + "roundIndex": 46, + "roundOffset": 142, + "transaction": { + "appAccounts": [ + "GJQLSF3KJZFRN7PMUYLDAOUVNHQVFMFXUNO6UPXVQH3GJXM5T53PF4TXEE", + "QDNLKZLNM6ZUD4ZI24RSY6O4QHWF3RHDQIYDV7S5AAHKFZSV2MSSULCE4U", + ], + "appArgs": [ + "AAAAAAAXe90=", + "AAAAAAAAAAA=", + "//8=", + "AAAAAAEAAg==", + "BAABAAI=", + "AP//AAEAAQ==", + ], + "appForeignApps": [ + 1002541853, + 1390675395, + ], + "appForeignAssets": [ + 246519683, + 1390638935, + ], + "appIndex": 1201559522, + "fee": 1000, + "firstRound": 35214365, + "from": "AACCDJTFPQR5UQJZ337NFR56CC44T776EWBGVJG5NY2QFTQWBWTALTEN4A", + "genesisHash": "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=", + "genesisID": "mainnet-v1.0", + "group": "cHiEEvBCRGnUhz9409gHl/vn00lYDZnJoppC3YexRr0=", + "lastRound": 35214369, + "lease": "G/BcDWMoEGKAU7T9/w0NETqkoDB/xtSwSSUQIxVFKIM=", + "note": "ABIRHgWWCypehpzUwrbxXIKwEdZxJIiyB+SyTfGUvgXhtJbAjwjsm0eEIHe5p3nB", + "reKeyTo": "GEAW6VVQY2QPYKEI6HAHAH3MNQNMXYOVKYVVI3B7X72CPW74HRVYXWGITU", + "tag": "VFg=", + "type": "appl", + }, + } + `) + + // https://allo.info/tx/QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q/inner/1/ + expect(getTransactionInBlockForDiff(transformed[intraRoundOffset + 1])).toMatchInlineSnapshot(` + { + "assetCloseAmount": undefined, + "closeAmount": undefined, + "createdAppId": undefined, + "createdAssetId": undefined, + "parentOffset": 0, + "parentTransactionId": "QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q", + "roundIndex": 46, + "roundOffset": 143, + "transaction": { + "amount": 1539037, + "fee": 1000, + "firstRound": 35214365, + "from": "AACCDJTFPQR5UQJZ337NFR56CC44T776EWBGVJG5NY2QFTQWBWTALTEN4A", + "genesisHash": "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=", + "genesisID": "mainnet-v1.0", + "group": "bLXdzryB627WoBOJ446eOJsiCi1Kfe/CKPTHRYKDsp0=", + "lastRound": 35214369, + "tag": "VFg=", + "to": "QDNLKZLNM6ZUD4ZI24RSY6O4QHWF3RHDQIYDV7S5AAHKFZSV2MSSULCE4U", + "type": "pay", + }, + } + `) + + // https://allo.info/tx/QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q/inner/2/ + expect(getTransactionInBlockForDiff(transformed[intraRoundOffset + 2])).toMatchInlineSnapshot(` + { + "assetCloseAmount": undefined, + "closeAmount": undefined, + "createdAppId": undefined, + "createdAssetId": undefined, + "parentOffset": 1, + "parentTransactionId": "QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q", + "roundIndex": 46, + "roundOffset": 144, + "transaction": { + "appAccounts": [ + "QDNLKZLNM6ZUD4ZI24RSY6O4QHWF3RHDQIYDV7S5AAHKFZSV2MSSULCE4U", + ], + "appArgs": [ + "c3dhcA==", + "Zml4ZWQtaW5wdXQ=", + "AAAAAAAAAAA=", + ], + "appForeignAssets": [ + 246519683, + ], + "appIndex": 1002541853, + "fee": 2000, + "firstRound": 35214365, + "from": "AACCDJTFPQR5UQJZ337NFR56CC44T776EWBGVJG5NY2QFTQWBWTALTEN4A", + "genesisHash": "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=", + "genesisID": "mainnet-v1.0", + "group": "bLXdzryB627WoBOJ446eOJsiCi1Kfe/CKPTHRYKDsp0=", + "lastRound": 35214369, + "tag": "VFg=", + "type": "appl", + }, + } + `) + + // https://allo.info/tx/QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q/inner/3/ + expect(getTransactionInBlockForDiff(transformed[intraRoundOffset + 3])).toMatchInlineSnapshot(` + { + "assetCloseAmount": undefined, + "closeAmount": undefined, + "createdAppId": undefined, + "createdAssetId": undefined, + "parentOffset": 2, + "parentTransactionId": "QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q", + "roundIndex": 46, + "roundOffset": 145, + "transaction": { + "amount": 394437, + "assetIndex": 246519683, + "firstRound": 35214365, + "from": "QDNLKZLNM6ZUD4ZI24RSY6O4QHWF3RHDQIYDV7S5AAHKFZSV2MSSULCE4U", + "genesisHash": "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=", + "genesisID": "mainnet-v1.0", + "lastRound": 35214369, + "tag": "VFg=", + "to": "AACCDJTFPQR5UQJZ337NFR56CC44T776EWBGVJG5NY2QFTQWBWTALTEN4A", + "type": "axfer", + }, + } + `) + + // https://allo.info/tx/QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q/inner/4/ + expect(getTransactionInBlockForDiff(transformed[intraRoundOffset + 4])).toMatchInlineSnapshot(` + { + "assetCloseAmount": undefined, + "closeAmount": undefined, + "createdAppId": undefined, + "createdAssetId": undefined, + "parentOffset": 3, + "parentTransactionId": "QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q", + "roundIndex": 46, + "roundOffset": 146, + "transaction": { + "amount": 394437, + "assetIndex": 246519683, + "fee": 1000, + "firstRound": 35214365, + "from": "AACCDJTFPQR5UQJZ337NFR56CC44T776EWBGVJG5NY2QFTQWBWTALTEN4A", + "genesisHash": "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=", + "genesisID": "mainnet-v1.0", + "group": "6ZssGapPFZ+DyccRludq0YjZigi05/FSeUAOFNDGGlo=", + "lastRound": 35214369, + "tag": "VFg=", + "to": "RS7QNBEPRRIBGI5COVRWFCRUS5NC5NX7UABZSTSFXQ6F74EP3CNLT4CNAM", + "type": "axfer", + }, + } + `) + + // https://allo.info/tx/QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q/inner/5/ + expect(getTransactionInBlockForDiff(transformed[intraRoundOffset + 5])).toMatchInlineSnapshot(` + { + "assetCloseAmount": undefined, + "closeAmount": undefined, + "createdAppId": undefined, + "createdAssetId": undefined, + "parentOffset": 4, + "parentTransactionId": "QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q", + "roundIndex": 46, + "roundOffset": 147, + "transaction": { + "appArgs": [ + "AA==", + "Aw==", + "AAAAAAAAAAA=", + "BAAAAAAABgTFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + ], + "appForeignAssets": [ + 1390638935, + ], + "appIndex": 1390675395, + "fee": 2000, + "firstRound": 35214365, + "from": "AACCDJTFPQR5UQJZ337NFR56CC44T776EWBGVJG5NY2QFTQWBWTALTEN4A", + "genesisHash": "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=", + "genesisID": "mainnet-v1.0", + "group": "6ZssGapPFZ+DyccRludq0YjZigi05/FSeUAOFNDGGlo=", + "lastRound": 35214369, + "tag": "VFg=", + "type": "appl", + }, + } + `) + + // https://allo.info/tx/QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q/inner/6/ + expect(getTransactionInBlockForDiff(transformed[intraRoundOffset + 6])).toMatchInlineSnapshot(` + { + "assetCloseAmount": undefined, + "closeAmount": undefined, + "createdAppId": undefined, + "createdAssetId": undefined, + "parentOffset": 5, + "parentTransactionId": "QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q", + "roundIndex": 46, + "roundOffset": 148, + "transaction": { + "amount": 536012365, + "assetIndex": 1390638935, + "firstRound": 35214365, + "from": "RS7QNBEPRRIBGI5COVRWFCRUS5NC5NX7UABZSTSFXQ6F74EP3CNLT4CNAM", + "genesisHash": "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=", + "genesisID": "mainnet-v1.0", + "lastRound": 35214369, + "tag": "VFg=", + "to": "AACCDJTFPQR5UQJZ337NFR56CC44T776EWBGVJG5NY2QFTQWBWTALTEN4A", + "type": "axfer", + }, + } + `) + + // https://allo.info/tx/QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q/inner/7/ + expect(getTransactionInBlockForDiff(transformed[intraRoundOffset + 7])).toMatchInlineSnapshot(` + { + "assetCloseAmount": undefined, + "closeAmount": undefined, + "createdAppId": undefined, + "createdAssetId": undefined, + "parentOffset": 6, + "parentTransactionId": "QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q", + "roundIndex": 46, + "roundOffset": 149, + "transaction": { + "amount": 536012365, + "assetIndex": 1390638935, + "fee": 1000, + "firstRound": 35214365, + "from": "AACCDJTFPQR5UQJZ337NFR56CC44T776EWBGVJG5NY2QFTQWBWTALTEN4A", + "genesisHash": "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=", + "genesisID": "mainnet-v1.0", + "group": "dsT4D4kYR3KthS3jbi4rJee2ej8gQChwzsQD8auclWw=", + "lastRound": 35214369, + "tag": "VFg=", + "to": "GJQLSF3KJZFRN7PMUYLDAOUVNHQVFMFXUNO6UPXVQH3GJXM5T53PF4TXEE", + "type": "axfer", + }, + } + `) + + // https://allo.info/tx/QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q/inner/8/ + expect(getTransactionInBlockForDiff(transformed[intraRoundOffset + 8])).toMatchInlineSnapshot(` + { + "assetCloseAmount": undefined, + "closeAmount": undefined, + "createdAppId": undefined, + "createdAssetId": undefined, + "parentOffset": 7, + "parentTransactionId": "QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q", + "roundIndex": 46, + "roundOffset": 150, + "transaction": { + "appAccounts": [ + "GJQLSF3KJZFRN7PMUYLDAOUVNHQVFMFXUNO6UPXVQH3GJXM5T53PF4TXEE", + ], + "appArgs": [ + "c3dhcA==", + "Zml4ZWQtaW5wdXQ=", + "AAAAAAAAAAA=", + ], + "appForeignAssets": [ + 1390638935, + ], + "appIndex": 1002541853, + "fee": 2000, + "firstRound": 35214365, + "from": "AACCDJTFPQR5UQJZ337NFR56CC44T776EWBGVJG5NY2QFTQWBWTALTEN4A", + "genesisHash": "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=", + "genesisID": "mainnet-v1.0", + "group": "dsT4D4kYR3KthS3jbi4rJee2ej8gQChwzsQD8auclWw=", + "lastRound": 35214369, + "tag": "VFg=", + "type": "appl", + }, + } + `) + + // https://allo.info/tx/QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q/inner/9/ + expect(getTransactionInBlockForDiff(transformed[intraRoundOffset + 9])).toMatchInlineSnapshot(` + { + "assetCloseAmount": undefined, + "closeAmount": undefined, + "createdAppId": undefined, + "createdAssetId": undefined, + "parentOffset": 8, + "parentTransactionId": "QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q", + "roundIndex": 46, + "roundOffset": 151, + "transaction": { + "amount": 1556942, + "firstRound": 35214365, + "from": "GJQLSF3KJZFRN7PMUYLDAOUVNHQVFMFXUNO6UPXVQH3GJXM5T53PF4TXEE", + "genesisHash": "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=", + "genesisID": "mainnet-v1.0", + "lastRound": 35214369, + "tag": "VFg=", + "to": "AACCDJTFPQR5UQJZ337NFR56CC44T776EWBGVJG5NY2QFTQWBWTALTEN4A", + "type": "pay", + }, + } + `) + + // https://allo.info/tx/QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q/inner/10/ + expect(getTransactionInBlockForDiff(transformed[intraRoundOffset + 10])).toMatchInlineSnapshot(` + { + "assetCloseAmount": undefined, + "closeAmount": undefined, + "createdAppId": undefined, + "createdAssetId": undefined, + "parentOffset": 9, + "parentTransactionId": "QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q", + "roundIndex": 46, + "roundOffset": 152, + "transaction": { + "fee": 1000, + "firstRound": 35214365, + "from": "AACCDJTFPQR5UQJZ337NFR56CC44T776EWBGVJG5NY2QFTQWBWTALTEN4A", + "genesisHash": "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=", + "genesisID": "mainnet-v1.0", + "lastRound": 35214369, + "reKeyTo": "AACCDJTFPQR5UQJZ337NFR56CC44T776EWBGVJG5NY2QFTQWBWTALTEN4A", + "tag": "VFg=", + "to": "AACCDJTFPQR5UQJZ337NFR56CC44T776EWBGVJG5NY2QFTQWBWTALTEN4A", + "type": "pay", + }, + } + `) + }) +}) From 52a17a2b026f48fe4d6c2f8842e7e6e065bbf6eb Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Sun, 21 Jan 2024 23:28:06 +0800 Subject: [PATCH 3/5] feat: Added support for parsing signatures and inner transactions from algod block --- .../types_block.BlockTransaction.md | 96 +++++++++++++++++-- .../types_block.BlockTransactionEvalDelta.md | 64 +++++++++++++ .../interfaces/types_block.BlockValueDelta.md | 52 ++++++++++ docs/code/interfaces/types_block.LogicSig.md | 68 +++++++++++++ .../interfaces/types_block.MultisigSig.md | 55 +++++++++++ docs/code/modules/index.md | 4 +- docs/code/modules/types_block.md | 20 ++++ package-lock.json | 8 +- package.json | 2 +- src/transform.ts | 66 +++++++++++-- src/types/block.ts | 44 ++++++++- tests/scenarios/transform-complex-txn.spec.ts | 39 ++++++++ 12 files changed, 497 insertions(+), 21 deletions(-) create mode 100644 docs/code/interfaces/types_block.BlockTransactionEvalDelta.md create mode 100644 docs/code/interfaces/types_block.BlockValueDelta.md create mode 100644 docs/code/interfaces/types_block.LogicSig.md create mode 100644 docs/code/interfaces/types_block.MultisigSig.md diff --git a/docs/code/interfaces/types_block.BlockTransaction.md b/docs/code/interfaces/types_block.BlockTransaction.md index dd0887d3..2c0a8fea 100644 --- a/docs/code/interfaces/types_block.BlockTransaction.md +++ b/docs/code/interfaces/types_block.BlockTransaction.md @@ -4,6 +4,12 @@ [types/block](../modules/types_block.md).BlockTransaction +Data that is returned in a raw Algorand block for a single transaction + +**`See`** + +https://github.com/algorand/go-algorand/blob/master/data/transactions/signedtxn.go#L32 + ## Table of contents ### Properties @@ -12,7 +18,13 @@ - [apid](types_block.BlockTransaction.md#apid) - [ca](types_block.BlockTransaction.md#ca) - [caid](types_block.BlockTransaction.md#caid) +- [dt](types_block.BlockTransaction.md#dt) +- [hgh](types_block.BlockTransaction.md#hgh) - [hgi](types_block.BlockTransaction.md#hgi) +- [lsig](types_block.BlockTransaction.md#lsig) +- [msig](types_block.BlockTransaction.md#msig) +- [sgnr](types_block.BlockTransaction.md#sgnr) +- [sig](types_block.BlockTransaction.md#sig) - [txn](types_block.BlockTransaction.md#txn) ## Properties @@ -25,7 +37,7 @@ Asset closing amount in decimal units #### Defined in -[types/block.ts:66](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L66) +[types/block.ts:72](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L72) ___ @@ -37,7 +49,7 @@ App ID when an app is created by the transaction #### Defined in -[types/block.ts:64](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L64) +[types/block.ts:70](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L70) ___ @@ -49,7 +61,7 @@ Algo closing amount in microAlgos #### Defined in -[types/block.ts:68](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L68) +[types/block.ts:74](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L74) ___ @@ -61,7 +73,31 @@ Asset ID when an asset is created by the transaction #### Defined in -[types/block.ts:62](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L62) +[types/block.ts:68](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L68) + +___ + +### dt + +• `Optional` **dt**: [`BlockTransactionEvalDelta`](types_block.BlockTransactionEvalDelta.md) + +The eval deltas for the block + +#### Defined in + +[types/block.ts:66](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L66) + +___ + +### hgh + +• `Optional` **hgh**: `boolean` + +Has genesis hash + +#### Defined in + +[types/block.ts:78](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L78) ___ @@ -73,7 +109,55 @@ Has genesis id #### Defined in -[types/block.ts:70](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L70) +[types/block.ts:76](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L76) + +___ + +### lsig + +• `Optional` **lsig**: [`LogicSig`](types_block.LogicSig.md) + +Logic signature + +#### Defined in + +[types/block.ts:82](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L82) + +___ + +### msig + +• `Optional` **msig**: [`MultisigSig`](types_block.MultisigSig.md) + +Transaction multisig signature + +#### Defined in + +[types/block.ts:84](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L84) + +___ + +### sgnr + +• `Optional` **sgnr**: `Uint8Array` + +The signer, if signing with a different key than the Transaction type `from` property indicates + +#### Defined in + +[types/block.ts:86](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L86) + +___ + +### sig + +• `Optional` **sig**: `Uint8Array` + +Transaction ED25519 signature + +#### Defined in + +[types/block.ts:80](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L80) ___ @@ -85,4 +169,4 @@ The encoded transaction data #### Defined in -[types/block.ts:60](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L60) +[types/block.ts:64](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L64) diff --git a/docs/code/interfaces/types_block.BlockTransactionEvalDelta.md b/docs/code/interfaces/types_block.BlockTransactionEvalDelta.md new file mode 100644 index 00000000..4d446450 --- /dev/null +++ b/docs/code/interfaces/types_block.BlockTransactionEvalDelta.md @@ -0,0 +1,64 @@ +[@algorandfoundation/algokit-subscriber](../README.md) / [types/block](../modules/types_block.md) / BlockTransactionEvalDelta + +# Interface: BlockTransactionEvalDelta + +[types/block](../modules/types_block.md).BlockTransactionEvalDelta + +Eval deltas for a block + +## Table of contents + +### Properties + +- [gd](types_block.BlockTransactionEvalDelta.md#gd) +- [itx](types_block.BlockTransactionEvalDelta.md#itx) +- [ld](types_block.BlockTransactionEvalDelta.md#ld) +- [lg](types_block.BlockTransactionEvalDelta.md#lg) + +## Properties + +### gd + +• **gd**: `Record`\<`string`, [`BlockValueDelta`](types_block.BlockValueDelta.md)\> + +The delta of global state, keyed by key + +#### Defined in + +[types/block.ts:126](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L126) + +___ + +### itx + +• `Optional` **itx**: [`BlockInnerTransaction`](../modules/types_block.md#blockinnertransaction)[] + +Inner transactions + +#### Defined in + +[types/block.ts:132](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L132) + +___ + +### ld + +• **ld**: `Record`\<`number`, `Record`\<`string`, [`BlockValueDelta`](types_block.BlockValueDelta.md)\>\> + +The delta of local state keyed by account ID offset in [txn.Sender, ...txn.Accounts] and then keyed by key + +#### Defined in + +[types/block.ts:128](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L128) + +___ + +### lg + +• **lg**: `string`[] + +Logs + +#### Defined in + +[types/block.ts:130](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L130) diff --git a/docs/code/interfaces/types_block.BlockValueDelta.md b/docs/code/interfaces/types_block.BlockValueDelta.md new file mode 100644 index 00000000..c19daa61 --- /dev/null +++ b/docs/code/interfaces/types_block.BlockValueDelta.md @@ -0,0 +1,52 @@ +[@algorandfoundation/algokit-subscriber](../README.md) / [types/block](../modules/types_block.md) / BlockValueDelta + +# Interface: BlockValueDelta + +[types/block](../modules/types_block.md).BlockValueDelta + +## Table of contents + +### Properties + +- [at](types_block.BlockValueDelta.md#at) +- [bs](types_block.BlockValueDelta.md#bs) +- [ui](types_block.BlockValueDelta.md#ui) + +## Properties + +### at + +• **at**: `number` + +DeltaAction is an enum of actions that may be performed when applying a delta to a TEAL key/value store: + * `1`: SetBytesAction indicates that a TEAL byte slice should be stored at a key + * `2`: SetUintAction indicates that a Uint should be stored at a key + * `3`: DeleteAction indicates that the value for a particular key should be deleted + +#### Defined in + +[types/block.ts:141](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L141) + +___ + +### bs + +• `Optional` **bs**: `Uint8Array` + +Bytes value + +#### Defined in + +[types/block.ts:144](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L144) + +___ + +### ui + +• `Optional` **ui**: `number` + +Uint64 value + +#### Defined in + +[types/block.ts:147](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L147) diff --git a/docs/code/interfaces/types_block.LogicSig.md b/docs/code/interfaces/types_block.LogicSig.md new file mode 100644 index 00000000..ff834953 --- /dev/null +++ b/docs/code/interfaces/types_block.LogicSig.md @@ -0,0 +1,68 @@ +[@algorandfoundation/algokit-subscriber](../README.md) / [types/block](../modules/types_block.md) / LogicSig + +# Interface: LogicSig + +[types/block](../modules/types_block.md).LogicSig + +Data that represents a multisig signature + +**`See`** + +https://github.com/algorand/go-algorand/blob/master/data/transactions/logicsig.go#L32 + +## Table of contents + +### Properties + +- [arg](types_block.LogicSig.md#arg) +- [l](types_block.LogicSig.md#l) +- [msig](types_block.LogicSig.md#msig) +- [sig](types_block.LogicSig.md#sig) + +## Properties + +### arg + +• `Optional` **arg**: `Buffer`[] + +Arguments passed into the logic signature + +#### Defined in + +[types/block.ts:100](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L100) + +___ + +### l + +• **l**: `Uint8Array` + +Logic sig code + +#### Defined in + +[types/block.ts:94](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L94) + +___ + +### msig + +• `Optional` **msig**: [`MultisigSig`](types_block.MultisigSig.md) + +Multisig signature for delegated operations + +#### Defined in + +[types/block.ts:98](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L98) + +___ + +### sig + +• `Optional` **sig**: `Uint8Array` + +ED25519 signature for delegated operations + +#### Defined in + +[types/block.ts:96](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L96) diff --git a/docs/code/interfaces/types_block.MultisigSig.md b/docs/code/interfaces/types_block.MultisigSig.md new file mode 100644 index 00000000..4bdabaf6 --- /dev/null +++ b/docs/code/interfaces/types_block.MultisigSig.md @@ -0,0 +1,55 @@ +[@algorandfoundation/algokit-subscriber](../README.md) / [types/block](../modules/types_block.md) / MultisigSig + +# Interface: MultisigSig + +[types/block](../modules/types_block.md).MultisigSig + +Data that represents a multisig signature + +**`See`** + +https://github.com/algorand/go-algorand/blob/master/crypto/multisig.go#L36 + +## Table of contents + +### Properties + +- [subsig](types_block.MultisigSig.md#subsig) +- [thr](types_block.MultisigSig.md#thr) +- [v](types_block.MultisigSig.md#v) + +## Properties + +### subsig + +• **subsig**: \{ `pk`: `Uint8Array` ; `s`: `Uint8Array` }[] + +Sub-signatures + +#### Defined in + +[types/block.ts:112](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L112) + +___ + +### thr + +• **thr**: `number` + +Multisig threshold + +#### Defined in + +[types/block.ts:110](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L110) + +___ + +### v + +• **v**: `number` + +Multisig version + +#### Defined in + +[types/block.ts:108](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L108) diff --git a/docs/code/modules/index.md b/docs/code/modules/index.md index 931f92d1..0920fea6 100644 --- a/docs/code/modules/index.md +++ b/docs/code/modules/index.md @@ -38,7 +38,7 @@ The blocks #### Defined in -[subscriptions.ts:264](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriptions.ts#L264) +[subscriptions.ts:297](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriptions.ts#L297) ___ @@ -65,4 +65,4 @@ The result of this subscription pull/poll. #### Defined in -[subscriptions.ts:23](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriptions.ts#L23) +[subscriptions.ts:19](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriptions.ts#L19) diff --git a/docs/code/modules/types_block.md b/docs/code/modules/types_block.md index 5ee76e80..eaa00f57 100644 --- a/docs/code/modules/types_block.md +++ b/docs/code/modules/types_block.md @@ -8,3 +8,23 @@ - [Block](../interfaces/types_block.Block.md) - [BlockTransaction](../interfaces/types_block.BlockTransaction.md) +- [BlockTransactionEvalDelta](../interfaces/types_block.BlockTransactionEvalDelta.md) +- [BlockValueDelta](../interfaces/types_block.BlockValueDelta.md) +- [LogicSig](../interfaces/types_block.LogicSig.md) +- [MultisigSig](../interfaces/types_block.MultisigSig.md) + +### Type Aliases + +- [BlockInnerTransaction](types_block.md#blockinnertransaction) + +## Type Aliases + +### BlockInnerTransaction + +Ƭ **BlockInnerTransaction**: `Omit`\<[`BlockTransaction`](../interfaces/types_block.BlockTransaction.md), ``"hgi"`` \| ``"hgh"``\> + +Data that is returned in a raw Algorand block for a single inner transaction + +#### Defined in + +[types/block.ts:121](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/block.ts#L121) diff --git a/package-lock.json b/package-lock.json index 0a4f750d..1299060a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,7 @@ "node": ">=18.0" }, "peerDependencies": { - "@algorandfoundation/algokit-utils": "^5.0.1", + "@algorandfoundation/algokit-utils": "^5.2.2", "algosdk": "^2.7.0" } }, @@ -54,9 +54,9 @@ } }, "node_modules/@algorandfoundation/algokit-utils": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@algorandfoundation/algokit-utils/-/algokit-utils-5.0.1.tgz", - "integrity": "sha512-+mUqrRwPvvrZjBYetH4zvEa0BPh0ph3BFP2N7RDGU2zOU2MdhKuAtNYwHVZGUzcK6bySijzww8ggwzOg4ufrVA==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@algorandfoundation/algokit-utils/-/algokit-utils-5.2.2.tgz", + "integrity": "sha512-fJm0cP3HTCPzjFz8EYyBWpCIQVBVTn1tpEt6sVxxO5rO1zU3h6t9xhIcbtCPF0Kz3OiFYKJ7i/iDUakbAO+Bbg==", "peer": true, "dependencies": { "buffer": "^6.0.3" diff --git a/package.json b/package.json index 0e0dc6fa..6a73f0bf 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "typescript": "^5.2.2" }, "peerDependencies": { - "@algorandfoundation/algokit-utils": "^5.0.1", + "@algorandfoundation/algokit-utils": "^5.2.2", "algosdk": "^2.7.0" }, "publishConfig": { diff --git a/src/transform.ts b/src/transform.ts index 41a0bb89..28f58ce8 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -1,4 +1,4 @@ -import type { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer' +import type { MultisigTransactionSubSignature, TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer' import { ApplicationOnComplete } from '@algorandfoundation/algokit-utils/types/indexer' import algosdk, { OnApplicationComplete, Transaction, TransactionType } from 'algosdk' import { Buffer } from 'buffer' @@ -195,7 +195,7 @@ export function algodOnCompleteToIndexerOnComplete(appOnComplete: OnApplicationC * @param closeAmount The amount of microAlgos that were transferred if the transaction had a close * @returns The indexer transaction formation (`TransactionResult`) */ -export function getIndexerTransactionFromAlgodTransaction(t: TransactionInBlock): TransactionResult { +export function getIndexerTransactionFromAlgodTransaction(t: TransactionInBlock & { getChildOffset?: () => number }): TransactionResult { const { transaction, createdAssetId, @@ -207,12 +207,17 @@ export function getIndexerTransactionFromAlgodTransaction(t: TransactionInBlock) roundOffset, parentOffset, parentTransactionId, + roundIndex, + parentTransaction, } = t if (!transaction.type) { throw new Error(`Received no transaction type for transaction ${transaction.txID()}`) } + let childOffset = roundOffset + const getChildOffset = t.getChildOffset ? t.getChildOffset : () => ++childOffset + const encoder = new TextEncoder() const decoder = new TextDecoder() @@ -325,16 +330,63 @@ export function getIndexerTransactionFromAlgodTransaction(t: TransactionInBlock) 'rekey-to': transaction.reKeyTo ? algosdk.encodeAddress(transaction.reKeyTo.publicKey) : undefined, 'closing-amount': closeAmount, 'created-application-index': createdAppId, + 'auth-addr': blockTransaction.sgnr ? algosdk.encodeAddress(blockTransaction.sgnr) : undefined, + 'inner-txns': blockTransaction.dt?.itx?.map((ibt) => + getIndexerTransactionFromAlgodTransaction({ + block, + blockTransaction: ibt, + roundIndex, + roundOffset: getChildOffset(), + ...extractTransactionFromBlockTransaction(ibt, block), + getChildOffset, + parentOffset, + parentTransaction, + parentTransactionId, + }), + ), + signature: + blockTransaction.sig || blockTransaction.lsig || blockTransaction.msig + ? { + sig: blockTransaction.sig ? Buffer.from(blockTransaction.sig).toString('base64') : undefined, + logicsig: blockTransaction.lsig + ? { + logic: Buffer.from(blockTransaction.lsig.l).toString('base64'), + args: blockTransaction.lsig.arg ? blockTransaction.lsig.arg.map((a) => Buffer.from(a).toString('base64')) : undefined, + signature: blockTransaction.lsig.sig ? Buffer.from(blockTransaction.lsig.sig).toString('base64') : undefined, + 'multisig-signature': blockTransaction.lsig.msig + ? { + version: blockTransaction.lsig.msig.v, + threshold: blockTransaction.lsig.msig.thr, + subsignature: blockTransaction.lsig.msig.subsig.map( + (s) => + ({ + 'public-key': Buffer.from(s.pk).toString('base64'), + signature: Buffer.from(s.s).toString('base64'), + }) as MultisigTransactionSubSignature, + ), + } + : undefined, + } + : undefined, + multisig: blockTransaction.msig + ? { + version: blockTransaction.msig.v, + threshold: blockTransaction.msig.thr, + subsignature: blockTransaction.msig.subsig.map((s) => ({ + 'public-key': Buffer.from(s.pk).toString('base64'), + signature: Buffer.from(s.s).toString('base64'), + })), + } + : undefined, + } + : undefined, // todo: do we need any of these? - //"auth-addr" //"close-rewards" + //"receiver-rewards" + //"sender-rewards" //"global-state-delta" - //"inner-txns" //"keyreg-transaction" //"local-state-delta" - //"receiver-rewards" - //"sender-rewards" //logs - //signature } } diff --git a/src/types/block.ts b/src/types/block.ts index f09b955c..91a0702e 100644 --- a/src/types/block.ts +++ b/src/types/block.ts @@ -55,7 +55,10 @@ export interface Block { txns: BlockTransaction[] } -/** Data that is returned in a raw Algorand block for a single transaction */ +/** Data that is returned in a raw Algorand block for a single transaction + * + * @see https://github.com/algorand/go-algorand/blob/master/data/transactions/signedtxn.go#L32 + */ export interface BlockTransaction { /** The encoded transaction data */ txn: EncodedTransaction @@ -73,6 +76,45 @@ export interface BlockTransaction { hgi: boolean /** Has genesis hash */ hgh?: boolean + /** Transaction ED25519 signature */ + sig?: Uint8Array + /** Logic signature */ + lsig?: LogicSig + /** Transaction multisig signature */ + msig?: MultisigSig + /** The signer, if signing with a different key than the Transaction type `from` property indicates */ + sgnr?: Uint8Array +} + +/** Data that represents a multisig signature + * @see https://github.com/algorand/go-algorand/blob/master/data/transactions/logicsig.go#L32 + */ +export interface LogicSig { + /** Logic sig code */ + l: Uint8Array + /** ED25519 signature for delegated operations */ + sig?: Uint8Array + /** Multisig signature for delegated operations */ + msig?: MultisigSig + /** Arguments passed into the logic signature */ + arg?: Buffer[] +} + +/** Data that represents a multisig signature + * @see https://github.com/algorand/go-algorand/blob/master/crypto/multisig.go#L36 + */ +export interface MultisigSig { + /** Multisig version */ + v: number + /** Multisig threshold */ + thr: number + /** Sub-signatures */ + subsig: { + /** ED25519 public key */ + pk: Uint8Array + /** ED25519 signature */ + s: Uint8Array + }[] } /** Data that is returned in a raw Algorand block for a single inner transaction */ diff --git a/tests/scenarios/transform-complex-txn.spec.ts b/tests/scenarios/transform-complex-txn.spec.ts index 1b584867..e4e0f294 100644 --- a/tests/scenarios/transform-complex-txn.spec.ts +++ b/tests/scenarios/transform-complex-txn.spec.ts @@ -209,6 +209,7 @@ describe('Complex transaction with many nested inner transactions', () => { "asset-config-transaction": {}, "asset-freeze-transaction": undefined, "asset-transfer-transaction": undefined, + "auth-addr": undefined, "closing-amount": undefined, "confirmed-round": 35214367, "created-application-index": undefined, @@ -219,6 +220,43 @@ describe('Complex transaction with many nested inner transactions', () => { "genesis-id": "mainnet-v1.0", "group": "6ZssGapPFZ+DyccRludq0YjZigi05/FSeUAOFNDGGlo=", "id": "QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q/inner/5", + "inner-txns": [ + { + "application-transaction": undefined, + "asset-config-transaction": {}, + "asset-freeze-transaction": undefined, + "asset-transfer-transaction": { + "amount": 536012365, + "asset-id": 1390638935, + "close-amount": undefined, + "close-to": undefined, + "receiver": "AACCDJTFPQR5UQJZ337NFR56CC44T776EWBGVJG5NY2QFTQWBWTALTEN4A", + "sender": "RS7QNBEPRRIBGI5COVRWFCRUS5NC5NX7UABZSTSFXQ6F74EP3CNLT4CNAM", + }, + "auth-addr": undefined, + "closing-amount": undefined, + "confirmed-round": 35214367, + "created-application-index": undefined, + "created-asset-index": undefined, + "fee": undefined, + "first-valid": 35214365, + "genesis-hash": "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=", + "genesis-id": "mainnet-v1.0", + "group": undefined, + "id": "QLYC4KMQW5RZRA7W5GYCJ4CUVWWSZKMK2V4X3XFQYSGYCJH6LI4Q/inner/5", + "inner-txns": undefined, + "intra-round-offset": 148, + "last-valid": 35214369, + "lease": "", + "note": "", + "payment-transaction": undefined, + "rekey-to": undefined, + "round-time": 1705252440, + "sender": "RS7QNBEPRRIBGI5COVRWFCRUS5NC5NX7UABZSTSFXQ6F74EP3CNLT4CNAM", + "signature": undefined, + "tx-type": "axfer", + }, + ], "intra-round-offset": 147, "last-valid": 35214369, "lease": "", @@ -227,6 +265,7 @@ describe('Complex transaction with many nested inner transactions', () => { "rekey-to": undefined, "round-time": 1705252440, "sender": "AACCDJTFPQR5UQJZ337NFR56CC44T776EWBGVJG5NY2QFTQWBWTALTEN4A", + "signature": undefined, "tx-type": "appl", } `) From b3994472d0f2455947929df740a44a27a293ef68 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Sun, 21 Jan 2024 23:29:48 +0800 Subject: [PATCH 4/5] docs: Updated readme future roadmap --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index daf3333f..a9c2c73e 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,6 @@ subscriber.start() ## Roadmap - Subscribe to contract events ([ARC-28](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0028.md)) -- Inner transaction processing - Multiple filters - Dynamic filters (e.g. subscribe to axfer's for assets that you subscribe to the creation of) - GraphQL example ideally with subscriptions From ea6bb8ac1dd00e3382a72e241c618640cf1cfda7 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Sun, 28 Jan 2024 16:18:06 +0800 Subject: [PATCH 5/5] feat: Added supports for parsing logs from algod --- examples/data-history-museum/index.ts | 12 ++++++------ package-lock.json | 8 ++++---- package.json | 2 +- src/subscriptions.ts | 6 +++++- src/transform.ts | 10 +++++++--- tests/scenarios/transform-complex-txn.spec.ts | 10 ++++++++-- 6 files changed, 31 insertions(+), 17 deletions(-) diff --git a/examples/data-history-museum/index.ts b/examples/data-history-museum/index.ts index 1cb8807b..dd6d21ca 100644 --- a/examples/data-history-museum/index.ts +++ b/examples/data-history-museum/index.ts @@ -74,21 +74,21 @@ async function saveDHMTransactions(transactions: TransactionResult[]) { if (t['created-asset-index']) { assets.push({ id: t['created-asset-index'], - name: t['asset-config-transaction'].params.name!, - unit: t['asset-config-transaction'].params['unit-name']!, - mediaUrl: t['asset-config-transaction'].params.url!, + name: t['asset-config-transaction']!.params!.name!, + unit: t['asset-config-transaction']!.params!['unit-name']!, + mediaUrl: t['asset-config-transaction']!.params!.url!, metadata: getArc69Metadata(t), created: new Date(t['round-time']! * 1000).toISOString(), lastModified: new Date(t['round-time']! * 1000).toISOString(), }) } else { - const asset = assets.find((a) => a.id === t['asset-config-transaction']['asset-id']) + const asset = assets.find((a) => a.id === t['asset-config-transaction']!['asset-id']) if (!asset) { // eslint-disable-next-line no-console console.error(t) - throw new Error(`Unable to find existing asset data for ${t['asset-config-transaction']['asset-id']}`) + throw new Error(`Unable to find existing asset data for ${t['asset-config-transaction']!['asset-id']}`) } - if (!t['asset-config-transaction'].params) { + if (!t['asset-config-transaction']!.params) { // Asset was deleted, remove it assets.splice(assets.indexOf(asset), 1) } else { diff --git a/package-lock.json b/package-lock.json index 1299060a..382576cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,7 @@ "node": ">=18.0" }, "peerDependencies": { - "@algorandfoundation/algokit-utils": "^5.2.2", + "@algorandfoundation/algokit-utils": "^5.3.1", "algosdk": "^2.7.0" } }, @@ -54,9 +54,9 @@ } }, "node_modules/@algorandfoundation/algokit-utils": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@algorandfoundation/algokit-utils/-/algokit-utils-5.2.2.tgz", - "integrity": "sha512-fJm0cP3HTCPzjFz8EYyBWpCIQVBVTn1tpEt6sVxxO5rO1zU3h6t9xhIcbtCPF0Kz3OiFYKJ7i/iDUakbAO+Bbg==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@algorandfoundation/algokit-utils/-/algokit-utils-5.3.1.tgz", + "integrity": "sha512-qC28tarAwRGjpyHOkQOhBOiNNMRn0fmf5hu2a1IVOIvfzvVd05pkRSCn6nx4JusVsDy8aArSP4zl0VQetapSHg==", "peer": true, "dependencies": { "buffer": "^6.0.3" diff --git a/package.json b/package.json index 6a73f0bf..8ba923ab 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "typescript": "^5.2.2" }, "peerDependencies": { - "@algorandfoundation/algokit-utils": "^5.2.2", + "@algorandfoundation/algokit-utils": "^5.3.1", "algosdk": "^2.7.0" }, "publishConfig": { diff --git a/src/subscriptions.ts b/src/subscriptions.ts index 79dd5070..005d389e 100644 --- a/src/subscriptions.ts +++ b/src/subscriptions.ts @@ -1,5 +1,6 @@ import * as algokit from '@algorandfoundation/algokit-utils' import type { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer' +import * as msgpack from 'algo-msgpack-with-bigint' import { Algodv2, Indexer, Transaction, encodeAddress } from 'algosdk' import type SearchForTransactions from 'algosdk/dist/types/client/v2/indexer/searchForTransactions' import sha512 from 'js-sha512' @@ -304,7 +305,10 @@ export async function getBlocksBulk(context: { startRound: number; maxRound: num blocks.push( ...(await Promise.all( chunk.map(async (round) => { - return (await client.block(round).do()) as { block: Block } + const response = await client.c.get(`/v2/blocks/${round}`, { format: 'msgpack' }, undefined, undefined, false) + const body = response.body as Uint8Array + const decoded = msgpack.decode(body) as { block: Block } + return decoded }), )), ) diff --git a/src/transform.ts b/src/transform.ts index 28f58ce8..04f05bfa 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -248,17 +248,20 @@ export function getIndexerTransactionFromAlgodTransaction(t: TransactionInBlock clawback: transaction.assetClawback ? algosdk.encodeAddress(transaction.assetClawback.publicKey) : undefined, freeze: transaction.assetFreeze ? algosdk.encodeAddress(transaction.assetFreeze.publicKey) : undefined, } - : 'apar' in blockTransaction.txn + : 'apar' in blockTransaction.txn && blockTransaction.txn.apar ? { manager: transaction.assetManager ? algosdk.encodeAddress(transaction.assetManager.publicKey) : undefined, reserve: transaction.assetReserve ? algosdk.encodeAddress(transaction.assetReserve.publicKey) : undefined, clawback: transaction.assetClawback ? algosdk.encodeAddress(transaction.assetClawback.publicKey) : undefined, freeze: transaction.assetFreeze ? algosdk.encodeAddress(transaction.assetFreeze.publicKey) : undefined, + // These parameters are required in the indexer type so setting to empty values + creator: '', + decimals: 0, + total: 0, } : undefined, } - : // eslint-disable-next-line @typescript-eslint/no-explicit-any - ({} as any), + : undefined, 'asset-transfer-transaction': transaction.type === TransactionType.axfer ? { @@ -380,6 +383,7 @@ export function getIndexerTransactionFromAlgodTransaction(t: TransactionInBlock : undefined, } : undefined, + logs: blockTransaction.dt?.lg ? blockTransaction.dt.lg.map((l) => Buffer.from(l, 'utf-8').toString('base64')) : undefined, // todo: do we need any of these? //"close-rewards" //"receiver-rewards" diff --git a/tests/scenarios/transform-complex-txn.spec.ts b/tests/scenarios/transform-complex-txn.spec.ts index e4e0f294..ba0ad309 100644 --- a/tests/scenarios/transform-complex-txn.spec.ts +++ b/tests/scenarios/transform-complex-txn.spec.ts @@ -206,7 +206,7 @@ describe('Complex transaction with many nested inner transactions', () => { "local-state-schema": undefined, "on-completion": "update", }, - "asset-config-transaction": {}, + "asset-config-transaction": undefined, "asset-freeze-transaction": undefined, "asset-transfer-transaction": undefined, "auth-addr": undefined, @@ -223,7 +223,7 @@ describe('Complex transaction with many nested inner transactions', () => { "inner-txns": [ { "application-transaction": undefined, - "asset-config-transaction": {}, + "asset-config-transaction": undefined, "asset-freeze-transaction": undefined, "asset-transfer-transaction": { "amount": 536012365, @@ -248,6 +248,7 @@ describe('Complex transaction with many nested inner transactions', () => { "intra-round-offset": 148, "last-valid": 35214369, "lease": "", + "logs": undefined, "note": "", "payment-transaction": undefined, "rekey-to": undefined, @@ -260,6 +261,11 @@ describe('Complex transaction with many nested inner transactions', () => { "intra-round-offset": 147, "last-valid": 35214369, "lease": "", + "logs": [ + "R2hHHwQAAAAAAAYExYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "AAAAAAAAYcKgAAAAAB/ypo2AAAAAAAAAAA==", + "PNaUw7oABCHCpmV8I9qBOd6+0ofCvhDCucm/w74lwoJqwqTdrjUCzpYNwqYAAAAAAAAAAAAAAAAABgTFgAAAAB/ypo2AAAAAAAAAAAAAAA91w7sZdAAAAAAC77+9", + ], "note": "", "payment-transaction": undefined, "rekey-to": undefined,