From 6b454b2b5ef732f5e196bd73ffe19e7398adda3d Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Wed, 27 Mar 2024 01:26:03 +0800 Subject: [PATCH 1/3] feat: Added partial OR logic for type, sender, receiver, appId and assetId --- docs/README.md | 12 +- ...s_subscription.AlgorandSubscriberConfig.md | 8 +- ...pes_subscription.SubscriberConfigFilter.md | 2 +- .../types_subscription.TransactionFilter.md | 47 ++--- ...scription.TransactionSubscriptionParams.md | 2 +- docs/code/modules/index.md | 2 +- docs/code/modules/types_subscription.md | 2 +- src/subscriptions.ts | 160 +++++++++++++----- src/types/subscription.ts | 28 ++- tests/filterFixture.ts | 20 ++- tests/scenarios/filters.spec.ts | 118 ++++++++----- 11 files changed, 247 insertions(+), 154 deletions(-) diff --git a/docs/README.md b/docs/README.md index 2a78dfa..6ae0faa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -160,15 +160,15 @@ getSubscribedTransactions({filters: [{name: 'filterName', filter: {/* Filter pro Currently this allows you filter based on any combination (AND logic) of: -- Transaction type e.g. `filter: { type: TransactionType.axfer }` -- Account (sender and receiver) e.g. `filter: { sender: "ABCDE..F" }` and `filter: { receiver: "12345..6" }` +- Transaction type e.g. `filter: { type: TransactionType.axfer }` or `filter: {type: [TransactionType.axfer, TransactionType.pay] }` +- Account (sender and receiver) e.g. `filter: { sender: "ABCDE..F" }` or `filter: { sender: ["ABCDE..F", "ZYXWV..A"] }` and `filter: { receiver: "12345..6" }` or `filter: { receiver: ["ABCDE..F", "ZYXWV..A"] }` - Note prefix e.g. `filter: { notePrefix: "xyz" }` - Apps - - ID e.g. `filter: { appId: 54321 }` + - ID e.g. `filter: { appId: 54321 }` or `filter: { appId: [54321, 12345] }` - Creation e.g. `filter: { appCreate: true }` - - Call on-complete(s) e.g. `filter: { appOnComplete: ApplicationOnComplete.optin }` and `filter: { appOnComplete: [ApplicationOnComplete.optin, ApplicationOnComplete.noop] }` - - ARC4 method signature(s) e.g. `filter: { methodSignature: "MyMethod(uint64,string)" }` and `filter: { methodSignatures: ["MyMethod(uint64,string)uint64", "MyMethod2(unit64)"] }` + - Call on-complete(s) e.g. `filter: { appOnComplete: ApplicationOnComplete.optin }` or `filter: { appOnComplete: [ApplicationOnComplete.optin, ApplicationOnComplete.noop] }` + - ARC4 method signature(s) e.g. `filter: { methodSignature: "MyMethod(uint64,string)" }` or `filter: { methodSignature: ["MyMethod(uint64,string)uint64", "MyMethod2(unit64)"] }` - Call arguments e.g. ```typescript filter: { @@ -187,7 +187,7 @@ Currently this allows you filter based on any combination (AND logic) of: Note: For this to work you need to [specify ARC-28 events in the subscription config](#arc-28-event-subscription-and-reads). - Assets - - ID e.g. `filter: { assetId: 123456 }` + - ID e.g. `filter: { assetId: 123456 }` or `filter: { assetId: [123456, 456789] }` - Creation e.g. `filter: { assetCreate: true }` - Amount transferred (min and/or max) e.g. `filter: { type: TransactionType.axfer, minAmount: 1, maxAmount: 100 }` - Balance changes (asset ID, sender, receiver, close to, min and/or max change) e.g. `filter: { balanceChanges: [{assetId: [15345, 36234], roles: [BalanceChangeRole.sender], address: "ABC...", minAmount: 1, maxAmount: 2}]}` diff --git a/docs/code/interfaces/types_subscription.AlgorandSubscriberConfig.md b/docs/code/interfaces/types_subscription.AlgorandSubscriberConfig.md index 76316b6..9d53d01 100644 --- a/docs/code/interfaces/types_subscription.AlgorandSubscriberConfig.md +++ b/docs/code/interfaces/types_subscription.AlgorandSubscriberConfig.md @@ -55,7 +55,7 @@ The set of filters to subscribe to / emit events for, along with optional data m #### Defined in -[types/subscription.ts:227](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L227) +[types/subscription.ts:225](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L225) ___ @@ -67,7 +67,7 @@ The frequency to poll for new blocks in seconds; defaults to 1s #### Defined in -[types/subscription.ts:229](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L229) +[types/subscription.ts:227](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L227) ___ @@ -155,7 +155,7 @@ Whether to wait via algod `/status/wait-for-block-after` endpoint when at the ti #### Defined in -[types/subscription.ts:231](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L231) +[types/subscription.ts:229](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L229) ___ @@ -175,4 +175,4 @@ its position in the chain #### Defined in -[types/subscription.ts:234](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L234) +[types/subscription.ts:232](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L232) diff --git a/docs/code/interfaces/types_subscription.SubscriberConfigFilter.md b/docs/code/interfaces/types_subscription.SubscriberConfigFilter.md index 6804d79..199cab1 100644 --- a/docs/code/interfaces/types_subscription.SubscriberConfigFilter.md +++ b/docs/code/interfaces/types_subscription.SubscriberConfigFilter.md @@ -70,7 +70,7 @@ Note: if you provide multiple filters with the same name then only the mapper of #### Defined in -[types/subscription.ts:250](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L250) +[types/subscription.ts:248](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L248) ___ diff --git a/docs/code/interfaces/types_subscription.TransactionFilter.md b/docs/code/interfaces/types_subscription.TransactionFilter.md index 11309d0..ce853de 100644 --- a/docs/code/interfaces/types_subscription.TransactionFilter.md +++ b/docs/code/interfaces/types_subscription.TransactionFilter.md @@ -21,7 +21,6 @@ Specify a filter to apply to find transactions of interest. - [customFilter](types_subscription.TransactionFilter.md#customfilter) - [maxAmount](types_subscription.TransactionFilter.md#maxamount) - [methodSignature](types_subscription.TransactionFilter.md#methodsignature) -- [methodSignatures](types_subscription.TransactionFilter.md#methodsignatures) - [minAmount](types_subscription.TransactionFilter.md#minamount) - [notePrefix](types_subscription.TransactionFilter.md#noteprefix) - [receiver](types_subscription.TransactionFilter.md#receiver) @@ -52,7 +51,7 @@ Filter to app transactions that meet the given app arguments predicate. #### Defined in -[types/subscription.ts:183](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L183) +[types/subscription.ts:181](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L181) ___ @@ -70,9 +69,9 @@ ___ ### appId -• `Optional` **appId**: `number` +• `Optional` **appId**: `number` \| `bigint` \| `number`[] \| `bigint`[] -Filter to transactions against the app with the given ID. +Filter to transactions against the app with the given ID(s). #### Defined in @@ -101,7 +100,7 @@ Note: the definitions for these events must be passed in to the subscription con #### Defined in -[types/subscription.ts:187](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L187) +[types/subscription.ts:185](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L185) ___ @@ -119,9 +118,9 @@ ___ ### assetId -• `Optional` **assetId**: `number` +• `Optional` **assetId**: `number` \| `bigint` \| `number`[] \| `bigint`[] -Filter to transactions against the asset with the given ID. +Filter to transactions against the asset with the given ID(s). #### Defined in @@ -131,13 +130,13 @@ ___ ### balanceChanges -• `Optional` **balanceChanges**: \{ `address?`: `string` \| `string`[] ; `assetId?`: `number` \| `number`[] ; `maxAbsoluteAmount?`: `number` \| `bigint` ; `maxAmount?`: `number` \| `bigint` ; `minAbsoluteAmount?`: `number` \| `bigint` ; `minAmount?`: `number` \| `bigint` ; `role?`: [`BalanceChangeRole`](../enums/types_subscription.BalanceChangeRole.md) \| [`BalanceChangeRole`](../enums/types_subscription.BalanceChangeRole.md)[] }[] +• `Optional` **balanceChanges**: \{ `address?`: `string` \| `string`[] ; `assetId?`: `number` \| `bigint` \| `number`[] \| `bigint`[] ; `maxAbsoluteAmount?`: `number` \| `bigint` ; `maxAmount?`: `number` \| `bigint` ; `minAbsoluteAmount?`: `number` \| `bigint` ; `minAmount?`: `number` \| `bigint` ; `role?`: [`BalanceChangeRole`](../enums/types_subscription.BalanceChangeRole.md) \| [`BalanceChangeRole`](../enums/types_subscription.BalanceChangeRole.md)[] }[] Filter to transactions that result in balance changes that match one or more of the given set of balance changes. #### Defined in -[types/subscription.ts:189](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L189) +[types/subscription.ts:187](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L187) ___ @@ -163,7 +162,7 @@ Catch-all custom filter to filter for things that the rest of the filters don't #### Defined in -[types/subscription.ts:206](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L206) +[types/subscription.ts:204](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L204) ___ @@ -182,9 +181,9 @@ ___ ### methodSignature -• `Optional` **methodSignature**: `string` +• `Optional` **methodSignature**: `string` \| `string`[] -Filter to app transactions that have the given ARC-0004 method selector for +Filter to app transactions that have the given ARC-0004 method selector(s) for the given method signature as the first app argument. #### Defined in @@ -193,18 +192,6 @@ the given method signature as the first app argument. ___ -### methodSignatures - -• `Optional` **methodSignatures**: `string`[] - -Filter to app transactions that match one of the given ARC-0004 method selectors as the first app argument. - -#### Defined in - -[types/subscription.ts:181](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L181) - -___ - ### minAmount • `Optional` **minAmount**: `number` \| `bigint` @@ -232,9 +219,9 @@ ___ ### receiver -• `Optional` **receiver**: `string` +• `Optional` **receiver**: `string` \| `string`[] -Filter to transactions being received by the specified address. +Filter to transactions being received by the specified address(es). #### Defined in @@ -244,9 +231,9 @@ ___ ### sender -• `Optional` **sender**: `string` +• `Optional` **sender**: `string` \| `string`[] -Filter to transactions sent from the specified address. +Filter to transactions sent from the specified address(es). #### Defined in @@ -256,9 +243,9 @@ ___ ### type -• `Optional` **type**: `TransactionType` +• `Optional` **type**: `TransactionType` \| `TransactionType`[] -Filter based on the given transaction type. +Filter based on the given transaction type(s). #### Defined in diff --git a/docs/code/interfaces/types_subscription.TransactionSubscriptionParams.md b/docs/code/interfaces/types_subscription.TransactionSubscriptionParams.md index 5789c2a..02d56a2 100644 --- a/docs/code/interfaces/types_subscription.TransactionSubscriptionParams.md +++ b/docs/code/interfaces/types_subscription.TransactionSubscriptionParams.md @@ -168,4 +168,4 @@ will be slow if `onMaxRounds` is `sync-oldest`. #### Defined in -[types/subscription.ts:221](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L221) +[types/subscription.ts:219](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L219) diff --git a/docs/code/modules/index.md b/docs/code/modules/index.md index a1aa3d1..a4074ef 100644 --- a/docs/code/modules/index.md +++ b/docs/code/modules/index.md @@ -38,7 +38,7 @@ The blocks #### Defined in -[subscriptions.ts:792](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriptions.ts#L792) +[subscriptions.ts:858](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriptions.ts#L858) ___ diff --git a/docs/code/modules/types_subscription.md b/docs/code/modules/types_subscription.md index 0a3c7e5..7e047bc 100644 --- a/docs/code/modules/types_subscription.md +++ b/docs/code/modules/types_subscription.md @@ -72,4 +72,4 @@ ___ #### Defined in -[types/subscription.ts:253](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L253) +[types/subscription.ts:251](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L251) diff --git a/src/subscriptions.ts b/src/subscriptions.ts index 3d5844b..ccfd9b6 100644 --- a/src/subscriptions.ts +++ b/src/subscriptions.ts @@ -1,7 +1,7 @@ import * as algokit from '@algorandfoundation/algokit-utils' import type { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer' import * as msgpack from 'algorand-msgpack' -import algosdk from 'algosdk' +import algosdk, { OnApplicationComplete } from 'algosdk' import type SearchForTransactions from 'algosdk/dist/types/client/v2/indexer/searchForTransactions' import sha512, { sha512_256 } from 'js-sha512' import { @@ -528,23 +528,30 @@ function indexerPreFilter( return (s) => { // NOTE: everything in this method needs to be mirrored to `indexerPreFilterInMemory` below let filter = s - if (subscription.sender) { + if (subscription.sender && typeof subscription.sender === 'string') { filter = filter.address(subscription.sender).addressRole('sender') } - if (subscription.receiver) { + if (subscription.receiver && typeof subscription.receiver === 'string') { filter = filter.address(subscription.receiver).addressRole('receiver') } - if (subscription.type) { + if (subscription.type && typeof subscription.type === 'string') { filter = filter.txType(subscription.type.toString()) } if (subscription.notePrefix) { filter = filter.notePrefix(Buffer.from(subscription.notePrefix).toString('base64')) } - if (subscription.appId) { - filter = filter.applicationID(subscription.appId) + if ( + subscription.appId && + (typeof subscription.appId === 'number' || (typeof subscription.appId === 'bigint' && subscription.appId <= Number.MAX_SAFE_INTEGER)) + ) { + filter = filter.applicationID(Number(subscription.appId)) } - if (subscription.assetId) { - filter = filter.assetID(subscription.assetId) + if ( + subscription.assetId && + (typeof subscription.assetId === 'number' || + (typeof subscription.assetId === 'bigint' && subscription.assetId <= Number.MAX_SAFE_INTEGER)) + ) { + filter = filter.assetID(Number(subscription.assetId)) } if (subscription.minAmount) { // Indexer only supports numbers, but even though this is less precise the in-memory indexer pre-filter will remove any false positives @@ -565,30 +572,62 @@ function indexerPreFilterInMemory(subscription: TransactionFilter): (t: Transact return (t) => { let result = true if (subscription.sender) { - result &&= t.sender === subscription.sender + if (typeof subscription.sender === 'string') { + result &&= t.sender === subscription.sender + } else { + result &&= subscription.sender.includes(t.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 (typeof subscription.receiver === 'string') { + result &&= + (!!t['asset-transfer-transaction'] && t['asset-transfer-transaction'].receiver === subscription.receiver) || + (!!t['payment-transaction'] && t['payment-transaction'].receiver === subscription.receiver) + } else { + result &&= + (!!t['asset-transfer-transaction'] && subscription.receiver.includes(t['asset-transfer-transaction'].receiver)) || + (!!t['payment-transaction'] && subscription.receiver.includes(t['payment-transaction'].receiver)) + } } if (subscription.type) { - result &&= t['tx-type'] === subscription.type + if (typeof subscription.type === 'string') { + result &&= t['tx-type'] === subscription.type + } else { + result &&= subscription.type.includes(t['tx-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 (typeof subscription.appId === 'number' || typeof subscription.appId === 'bigint') { + result &&= + t['created-application-index'] === Number(subscription.appId) || + (!!t['application-transaction'] && t['application-transaction']['application-id'] === Number(subscription.appId)) + } else { + result &&= + (t['created-application-index'] && subscription.appId.map((i) => Number(i)).includes(t['created-application-index'])) || + (!!t['application-transaction'] && + subscription.appId.map((i) => Number(i)).includes(t['application-transaction']['application-id'])) + } } 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 (typeof subscription.assetId === 'number' || typeof subscription.assetId === 'bigint') { + 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) + } else { + result &&= + (t['created-asset-index'] && subscription.assetId.map((i) => Number(i)).includes(t['created-asset-index'])) || + (!!t['asset-config-transaction'] && + subscription.assetId.map((i) => Number(i)).includes(t['asset-config-transaction']['asset-id'])) || + (!!t['asset-freeze-transaction'] && + subscription.assetId.map((i) => Number(i)).includes(t['asset-freeze-transaction']['asset-id'])) || + (!!t['asset-transfer-transaction'] && + subscription.assetId.map((i) => Number(i)).includes(t['asset-transfer-transaction']['asset-id'])) + } } if (subscription.minAmount) { @@ -631,20 +670,21 @@ function indexerPostFilter( ) } if (subscription.methodSignature) { - result &&= - !!t['application-transaction'] && - !!t['application-transaction']['application-args'] && - t['application-transaction']['application-args'][0] === getMethodSelectorBase64(subscription.methodSignature) - } - if (subscription.methodSignatures) { - subscription.methodSignatures.filter( - (method) => + if (typeof subscription.methodSignature === 'string') { + result &&= !!t['application-transaction'] && !!t['application-transaction']['application-args'] && - t['application-transaction']['application-args'][0] === getMethodSelectorBase64(method), - ).length > 0 - ? (result &&= true) - : (result &&= false) + t['application-transaction']['application-args'][0] === getMethodSelectorBase64(subscription.methodSignature) + } else { + subscription.methodSignature.filter( + (method) => + !!t['application-transaction'] && + !!t['application-transaction']['application-args'] && + t['application-transaction']['application-args'][0] === getMethodSelectorBase64(method), + ).length > 0 + ? (result &&= true) + : (result &&= false) + } } if (subscription.appCallArgumentsMatch) { result &&= @@ -690,22 +730,46 @@ function transactionFilter( const { transaction: t, createdAppId, createdAssetId, logs } = txn let result = true if (subscription.sender) { - result &&= !!t.from && algosdk.encodeAddress(t.from.publicKey) === subscription.sender + if (typeof subscription.sender === 'string') { + result &&= !!t.from && algosdk.encodeAddress(t.from.publicKey) === subscription.sender + } else { + result &&= !!t.from && subscription.sender.includes(algosdk.encodeAddress(t.from.publicKey)) + } } if (subscription.receiver) { - result &&= !!t.to && algosdk.encodeAddress(t.to.publicKey) === subscription.receiver + if (typeof subscription.receiver === 'string') { + result &&= !!t.to && algosdk.encodeAddress(t.to.publicKey) === subscription.receiver + } else { + result &&= !!t.to && subscription.receiver.includes(algosdk.encodeAddress(t.to.publicKey)) + } } if (subscription.type) { - result &&= t.type === subscription.type + if (typeof subscription.type === 'string') { + result &&= t.type === subscription.type + } else { + result &&= !!t.type && subscription.type.includes(t.type) + } } if (subscription.notePrefix) { result &&= !!t.note && new TextDecoder().decode(t.note).startsWith(subscription.notePrefix) } if (subscription.appId) { - result &&= t.appIndex === subscription.appId || createdAppId === subscription.appId + if (typeof subscription.appId === 'number' || typeof subscription.appId === 'bigint') { + result &&= t.appIndex === Number(subscription.appId) || createdAppId === Number(subscription.appId) + } else { + result &&= + (!!t.appIndex && subscription.appId.map((i) => Number(i)).includes(t.appIndex)) || + (!!createdAppId && subscription.appId.map((i) => Number(i)).includes(createdAppId)) + } } if (subscription.assetId) { - result &&= t.assetIndex === subscription.assetId || createdAssetId === subscription.assetId + if (typeof subscription.assetId === 'number' || typeof subscription.assetId === 'bigint') { + result &&= t.assetIndex === subscription.assetId || createdAssetId === subscription.assetId + } else { + result &&= + (!!t.assetIndex && subscription.assetId.map((i) => Number(i)).includes(t.assetIndex)) || + (!!createdAssetId && subscription.assetId.map((i) => Number(i)).includes(createdAssetId)) + } } if (subscription.minAmount) { result &&= t.amount >= subscription.minAmount @@ -725,18 +789,20 @@ function transactionFilter( } if (subscription.appOnComplete) { result &&= (typeof subscription.appOnComplete === 'string' ? [subscription.appOnComplete] : subscription.appOnComplete).includes( - algodOnCompleteToIndexerOnComplete(t.appOnComplete), + algodOnCompleteToIndexerOnComplete(t.appOnComplete ?? OnApplicationComplete.NoOpOC /* the '0' value comes through as undefined */), ) } if (subscription.methodSignature) { - result &&= !!t.appArgs && Buffer.from(t.appArgs[0] ?? []).toString('base64') === getMethodSelectorBase64(subscription.methodSignature) - } - if (subscription.methodSignatures) { - subscription.methodSignatures.filter( - (method) => !!t.appArgs && Buffer.from(t.appArgs[0] ?? []).toString('base64') === getMethodSelectorBase64(method), - ).length > 0 - ? (result &&= true) - : (result &&= false) + if (typeof subscription.methodSignature === 'string') { + result &&= + !!t.appArgs && Buffer.from(t.appArgs[0] ?? []).toString('base64') === getMethodSelectorBase64(subscription.methodSignature) + } else { + subscription.methodSignature.filter( + (method) => !!t.appArgs && Buffer.from(t.appArgs[0] ?? []).toString('base64') === getMethodSelectorBase64(method), + ).length > 0 + ? (result &&= true) + : (result &&= false) + } } if (subscription.arc28Events) { result &&= diff --git a/src/types/subscription.ts b/src/types/subscription.ts index 76bd2da..4530e47 100644 --- a/src/types/subscription.ts +++ b/src/types/subscription.ts @@ -150,22 +150,22 @@ export interface NamedTransactionFilter { /** Specify a filter to apply to find transactions of interest. */ export interface TransactionFilter { - /** Filter based on the given transaction type. */ - type?: TransactionType - /** Filter to transactions sent from the specified address. */ - sender?: string - /** Filter to transactions being received by the specified address. */ - receiver?: string + /** Filter based on the given transaction type(s). */ + type?: TransactionType | TransactionType[] + /** Filter to transactions sent from the specified address(es). */ + sender?: string | string[] + /** Filter to transactions being received by the specified address(es). */ + receiver?: string | string[] /** Filter to transactions with a note having the given prefix. */ notePrefix?: string - /** Filter to transactions against the app with the given ID. */ - appId?: number + /** Filter to transactions against the app with the given ID(s). */ + appId?: number | number[] | bigint | bigint[] /** Filter to transactions that are creating an app. */ appCreate?: boolean /** Filter to transactions that have given on complete(s). */ appOnComplete?: ApplicationOnComplete | ApplicationOnComplete[] - /** Filter to transactions against the asset with the given ID. */ - assetId?: number + /** Filter to transactions against the asset with the given ID(s). */ + assetId?: number | number[] | bigint | bigint[] /** Filter to transactions that are creating an asset. */ assetCreate?: boolean /** Filter to transactions where the amount being transferred is greater @@ -174,11 +174,9 @@ export interface TransactionFilter { /** Filter to transactions where the amount being transferred is less than * or equal to the given maximum (microAlgos or decimal units of an ASA if type: axfer). */ maxAmount?: number | bigint - /** Filter to app transactions that have the given ARC-0004 method selector for + /** Filter to app transactions that have the given ARC-0004 method selector(s) for * the given method signature as the first app argument. */ - methodSignature?: string - /** Filter to app transactions that match one of the given ARC-0004 method selectors as the first app argument. */ - methodSignatures?: string[] + methodSignature?: string | string[] /** Filter to app transactions that meet the given app arguments predicate. */ appCallArgumentsMatch?: (appCallArguments?: Uint8Array[]) => boolean /** Filter to app transactions that emit the given ARC-28 events. @@ -188,7 +186,7 @@ export interface TransactionFilter { /** Filter to transactions that result in balance changes that match one or more of the given set of balance changes. */ balanceChanges?: { /** Match transactions with balance changes for one of the given asset ID(s), with Algo being `0` */ - assetId?: number | number[] + assetId?: number | number[] | bigint | bigint[] /** Match transactions with balance changes for an account with one of the given role(s) */ role?: BalanceChangeRole | BalanceChangeRole[] /** Match transactions with balance changes affecting one of the given account(s) */ diff --git a/tests/filterFixture.ts b/tests/filterFixture.ts index 8bc8d8b..609405f 100644 --- a/tests/filterFixture.ts +++ b/tests/filterFixture.ts @@ -60,13 +60,21 @@ export function filterFixture(fixtureConfig?: AlgorandFixtureConfig) { return subscribed } - const subscribeAndVerifyFilter = async (filter: TransactionFilter, result: SendTransactionResult, arc28Events?: Arc28EventGroup[]) => { - const [algod, indexer] = await Promise.all([subscribeAlgod(filter, result, arc28Events), subscribeIndexer(filter, result, arc28Events)]) + const subscribeAndVerifyFilter = async ( + filter: TransactionFilter, + result: SendTransactionResult | SendTransactionResult[], + arc28Events?: Arc28EventGroup[], + ) => { + const results = Array.isArray(result) ? result : [result] + const [algod, indexer] = await Promise.all([ + subscribeAlgod(filter, results[0], arc28Events), + subscribeIndexer(filter, results[0], arc28Events), + ]) - expect(algod.subscribedTransactions.length).toBe(1) - expect(algod.subscribedTransactions[0].id).toBe(result.transaction.txID()) - expect(indexer.subscribedTransactions.length).toBe(1) - expect(indexer.subscribedTransactions[0].id).toBe(result.transaction.txID()) + expect(algod.subscribedTransactions.length).toBe(results.length) + expect(algod.subscribedTransactions.map((s) => s.id)).toEqual(results.map((r) => r.transaction.txID())) + expect(indexer.subscribedTransactions.length).toBe(results.length) + expect(indexer.subscribedTransactions.map((s) => s.id)).toEqual(results.map((r) => r.transaction.txID())) return { algod, indexer } } diff --git a/tests/scenarios/filters.spec.ts b/tests/scenarios/filters.spec.ts index cb9e54c..f7fe2f3 100644 --- a/tests/scenarios/filters.spec.ts +++ b/tests/scenarios/filters.spec.ts @@ -57,18 +57,21 @@ describe('Subscribing using various filters', () => { } } - test('Works for receiver', async () => { - const { testAccount, algod } = localnet.context - const account2 = algokit.randomAccount() - const amount = (1).algos() + test('Works for receiver(s) and sender(s)', async () => { + const { testAccount, algod, generateAccount } = localnet.context + const account2 = await generateAccount({ initialFunds: (3).algos() }) const account3 = algokit.randomAccount() + const amount = (1).algos() const txns = await algokit.sendGroupOfTransactions( { transactions: [ - algokit.transferAlgos({ amount, from: testAccount, to: account2, skipSending: true }, algod), - algokit.transferAlgos({ amount, from: testAccount, to: account3, skipSending: true }, algod), - ], - signer: testAccount, + await algokit.transferAlgos({ amount, from: testAccount, to: account2, skipSending: true }, algod), + await algokit.transferAlgos({ amount, from: testAccount, to: account3, skipSending: true }, algod), + await algokit.transferAlgos({ amount, from: account2, to: testAccount, skipSending: true }, algod), + ].map((t) => ({ + transaction: t.transaction, + signer: algosdk.encodeAddress(t.transaction.from.publicKey) === testAccount.addr ? testAccount : account2, + })), }, algod, ) @@ -79,6 +82,27 @@ describe('Subscribing using various filters', () => { }, extractFromGroupResult(txns, 0), ) + + await subscribeAndVerifyFilter( + { + receiver: [account2.addr, account3.addr], + }, + [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 1)], + ) + + await subscribeAndVerifyFilter( + { + sender: account2.addr, + }, + extractFromGroupResult(txns, 2), + ) + + await subscribeAndVerifyFilter( + { + sender: [testAccount.addr, account2.addr], + }, + [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 1), extractFromGroupResult(txns, 2)], + ) }) test('Works for min amount of algos', async () => { @@ -148,7 +172,7 @@ describe('Subscribing using various filters', () => { ) }) - test('Works for asset ID', async () => { + test('Works for asset ID(s)', async () => { const { testAccount, algod } = localnet.context const asset1 = await createAsset() const asset2 = await createAsset() @@ -170,6 +194,14 @@ describe('Subscribing using various filters', () => { }, extractFromGroupResult(txns, 0), ) + + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + assetId: [asset1.assetId, asset2.assetId], + }, + [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 1)], + ) }) test('Works for asset create', async () => { @@ -195,7 +227,7 @@ describe('Subscribing using various filters', () => { ) }) - test('Works for asset config txn', async () => { + test('Works for transaction type(s)', async () => { const { testAccount, algod } = localnet.context const asset1 = await createAsset() const txns = await algokit.sendGroupOfTransactions( @@ -212,32 +244,25 @@ describe('Subscribing using various filters', () => { await subscribeAndVerifyFilter( { sender: testAccount.addr, - type: TransactionType.acfg, + type: TransactionType.axfer, }, - extractFromGroupResult(txns, 1), + extractFromGroupResult(txns, 0), ) - }) - test('Works for asset transfer txn', async () => { - const { testAccount, algod } = localnet.context - const asset1 = await createAsset() - const txns = await algokit.sendGroupOfTransactions( + await subscribeAndVerifyFilter( { - transactions: [ - algokit.assetOptIn({ account: testAccount, assetId: asset1.assetId, skipSending: true }, algod), - await createAssetTxn(testAccount), - ], - signer: testAccount, + sender: testAccount.addr, + type: TransactionType.acfg, }, - algod, + extractFromGroupResult(txns, 1), ) await subscribeAndVerifyFilter( { sender: testAccount.addr, - type: TransactionType.axfer, + type: [TransactionType.acfg, TransactionType.axfer], }, - extractFromGroupResult(txns, 0), + [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 1)], ) }) @@ -312,7 +337,7 @@ describe('Subscribing using various filters', () => { ) }) - test('Works for app ID', async () => { + test('Works for app ID(s)', async () => { const { testAccount, algod } = localnet.context const app1 = await app({ create: true }) const app2 = await app({ create: true }) @@ -334,9 +359,17 @@ describe('Subscribing using various filters', () => { }, extractFromGroupResult(txns, 0), ) + + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + appId: [Number(app1.creation.confirmation!.applicationIndex!), Number(app2.creation.confirmation!.applicationIndex!)], + }, + [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 1)], + ) }) - test('Works for on-complete', async () => { + test('Works for on-complete(s)', async () => { const { testAccount, algod } = localnet.context const app1 = await app({ create: true }) const txns = await algokit.sendGroupOfTransactions( @@ -357,9 +390,17 @@ describe('Subscribing using various filters', () => { }, extractFromGroupResult(txns, 1), ) + + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + appOnComplete: [ApplicationOnComplete.optin, ApplicationOnComplete.noop], + }, + [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 1)], + ) }) - test('Works for method signature', async () => { + test('Works for method signature(s)', async () => { const { testAccount, algod } = localnet.context const app1 = await app({ create: true }) const txns = await algokit.sendGroupOfTransactions( @@ -380,28 +421,21 @@ describe('Subscribing using various filters', () => { }, extractFromGroupResult(txns, 1), ) - }) - test('Works for method signatures', async () => { - const { testAccount, algod } = localnet.context - const app1 = await app({ create: true }) - const txns = await algokit.sendGroupOfTransactions( + await subscribeAndVerifyFilter( { - transactions: [ - app1.app.callAbi({ value: 'test' }, { sender: testAccount, sendParams: { skipSending: true } }), - app1.app.optIn.optIn({}, { sender: testAccount, sendParams: { skipSending: true } }), - ], - signer: testAccount, + sender: testAccount.addr, + methodSignature: ['opt_in()void', 'madeUpMethod()void'], }, - algod, + extractFromGroupResult(txns, 1), ) await subscribeAndVerifyFilter( { sender: testAccount.addr, - methodSignatures: ['opt_in()void', 'madeUpMethod()void'], + methodSignature: ['opt_in()void', 'call_abi(string)string'], }, - extractFromGroupResult(txns, 1), + [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 1)], ) }) @@ -429,7 +463,7 @@ describe('Subscribing using various filters', () => { ) }) - test('Works for custom', async () => { + test('Works for custom filter', async () => { const { testAccount, algod } = localnet.context const txns = await algokit.sendGroupOfTransactions( { From 57f70f973d4b8c36a991159c1a27a2e949c6be17 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Wed, 27 Mar 2024 02:52:59 +0800 Subject: [PATCH 2/3] fix: Resolved bug in indexer pre-filtering being too aggressive related to minAmount and maxAmount --- docs/README.md | 14 +++++++------- docs/code/modules/index.md | 2 +- src/subscriptions.ts | 17 +++++++++++++---- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/docs/README.md b/docs/README.md index 6ae0faa..f5133bb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -328,14 +328,14 @@ The indexer catchup isn't magic - if the filter you are trying to catch up with To understand how the indexer behaviour works to know if you are likely to generate a lot of transactions it's worth understanding the architecture of the indexer catchup; indexer catchup runs in two stages: 1. **Pre-filtering**: Any filters that can be translated to the [indexer search transactions endpoint](https://developer.algorand.org/docs/rest-apis/indexer/#get-v2transactions). This query is then run between the rounds that need to be synced and paginated in the max number of results (1000) at a time until all of the transactions are retrieved. This ensures we get round-based transactional consistency. This is the filter that can easily explode out though and take a long time when using indexer catchup. For avoidance of doubt, the following filters are the ones that are converted to a pre-filter: - - `sender` - - `receiver` - - `type` + - `sender` (single value) + - `receiver` (single value) + - `type` (single value) - `notePrefix` - - `appId` - - `assetId` - - `minAmount` - - `maxAmount` + - `appId` (single value) + - `assetId` (single value) + - `minAmount` (and `type = pay` or `assetId` provided) + - `maxAmount` (and `maxAmount < Number.MAX_SAFE_INTEGER` and `type = pay` or (`assetId` provided and `minAmount > 0`)) 2. **Post-filtering**: All remaining filters are then applied in-memory to the resulting list of transactions that are returned from the pre-filter before being returned as subscribed transactions. ## Entry points diff --git a/docs/code/modules/index.md b/docs/code/modules/index.md index a4074ef..bf4b9b9 100644 --- a/docs/code/modules/index.md +++ b/docs/code/modules/index.md @@ -38,7 +38,7 @@ The blocks #### Defined in -[subscriptions.ts:858](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriptions.ts#L858) +[subscriptions.ts:867](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriptions.ts#L867) ___ diff --git a/src/subscriptions.ts b/src/subscriptions.ts index ccfd9b6..16da28b 100644 --- a/src/subscriptions.ts +++ b/src/subscriptions.ts @@ -553,13 +553,22 @@ function indexerPreFilter( ) { filter = filter.assetID(Number(subscription.assetId)) } - if (subscription.minAmount) { + + // Indexer only supports minAmount and maxAmount for non-payments if an asset ID is provided so check + // we are looking for just payments, or we have provided asset ID before adding to pre-filter + // if they aren't added here they will be picked up in the in-memory pre-filter + if (subscription.minAmount && (subscription.type === TransactionType.pay || subscription.assetId)) { // Indexer only supports numbers, but even though this is less precise the in-memory indexer pre-filter will remove any false positives filter = filter.currencyGreaterThan( subscription.minAmount > Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : Number(subscription.minAmount) - 1, ) } - if (subscription.maxAmount && subscription.maxAmount < Number.MAX_SAFE_INTEGER) { + if ( + subscription.maxAmount && + subscription.maxAmount < Number.MAX_SAFE_INTEGER && + // Only let an asset currency max search work when there is also a min > 0 otherwise opt-ins aren't picked up by the pre-filter :( + (subscription.type === TransactionType.pay || (subscription.assetId && (subscription?.minAmount ?? 0) > 0)) + ) { filter = filter.currencyLessThan(Number(subscription.maxAmount) + 1) } return filter.minRound(minRound).maxRound(maxRound) @@ -772,10 +781,10 @@ function transactionFilter( } } if (subscription.minAmount) { - result &&= t.amount >= subscription.minAmount + result &&= !!t.type && [TransactionType.axfer, TransactionType.pay].includes(t.type) && (t.amount ?? 0) >= subscription.minAmount } if (subscription.maxAmount) { - result &&= t.amount <= subscription.maxAmount + result &&= !!t.type && [TransactionType.axfer, TransactionType.pay].includes(t.type) && (t.amount ?? 0) <= subscription.maxAmount } if (subscription.assetCreate) { result &&= !!createdAssetId From 999803a7bc9c1447756350abbce322a82ddfccdb Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Wed, 27 Mar 2024 02:53:26 +0800 Subject: [PATCH 3/3] test: Refactor filter tests to be an order of magnitude faster to execute --- .../interfaces/types_arc_28.Arc28Event.md | 6 +- .../types_arc_28.Arc28EventGroup.md | 10 +- .../types_arc_28.Arc28EventToProcess.md | 10 +- .../types_arc_28.EmittedArc28Event.md | 14 +- docs/code/modules/index.md | 4 +- src/subscriptions.ts | 3 +- src/types/arc-28.ts | 1 - tests/scenarios/filters.spec.ts | 728 +++++++++--------- 8 files changed, 398 insertions(+), 378 deletions(-) diff --git a/docs/code/interfaces/types_arc_28.Arc28Event.md b/docs/code/interfaces/types_arc_28.Arc28Event.md index 4e44c34..666ef37 100644 --- a/docs/code/interfaces/types_arc_28.Arc28Event.md +++ b/docs/code/interfaces/types_arc_28.Arc28Event.md @@ -24,7 +24,7 @@ The arguments of the event, in order #### Defined in -[types/arc-28.ts:15](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L15) +[types/arc-28.ts:14](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L14) ___ @@ -36,7 +36,7 @@ Optional, user-friendly description for the event #### Defined in -[types/arc-28.ts:13](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L13) +[types/arc-28.ts:12](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L12) ___ @@ -48,4 +48,4 @@ The name of the event #### Defined in -[types/arc-28.ts:11](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L11) +[types/arc-28.ts:10](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L10) diff --git a/docs/code/interfaces/types_arc_28.Arc28EventGroup.md b/docs/code/interfaces/types_arc_28.Arc28EventGroup.md index a012e9e..2784ab0 100644 --- a/docs/code/interfaces/types_arc_28.Arc28EventGroup.md +++ b/docs/code/interfaces/types_arc_28.Arc28EventGroup.md @@ -26,7 +26,7 @@ Whether or not to silently (with warning log) continue if an error is encountere #### Defined in -[types/arc-28.ts:56](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L56) +[types/arc-28.ts:55](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L55) ___ @@ -38,7 +38,7 @@ The list of ARC-28 event definitions #### Defined in -[types/arc-28.ts:58](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L58) +[types/arc-28.ts:57](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L57) ___ @@ -50,7 +50,7 @@ The name to designate for this group of events. #### Defined in -[types/arc-28.ts:50](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L50) +[types/arc-28.ts:49](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L49) ___ @@ -62,7 +62,7 @@ Optional list of app IDs that this event should apply to #### Defined in -[types/arc-28.ts:52](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L52) +[types/arc-28.ts:51](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L51) ___ @@ -88,4 +88,4 @@ Optional predicate to indicate if these ARC-28 events should be processed for th #### Defined in -[types/arc-28.ts:54](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L54) +[types/arc-28.ts:53](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L53) diff --git a/docs/code/interfaces/types_arc_28.Arc28EventToProcess.md b/docs/code/interfaces/types_arc_28.Arc28EventToProcess.md index dafb5b0..ee76e81 100644 --- a/docs/code/interfaces/types_arc_28.Arc28EventToProcess.md +++ b/docs/code/interfaces/types_arc_28.Arc28EventToProcess.md @@ -32,7 +32,7 @@ The ARC-28 definition of the event #### Defined in -[types/arc-28.ts:36](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L36) +[types/arc-28.ts:35](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L35) ___ @@ -44,7 +44,7 @@ The name of the ARC-28 event that was triggered #### Defined in -[types/arc-28.ts:30](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L30) +[types/arc-28.ts:29](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L29) ___ @@ -56,7 +56,7 @@ The 4-byte hex prefix for the event #### Defined in -[types/arc-28.ts:34](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L34) +[types/arc-28.ts:33](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L33) ___ @@ -68,7 +68,7 @@ The signature of the event e.g. `EventName(type1,type2)` #### Defined in -[types/arc-28.ts:32](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L32) +[types/arc-28.ts:31](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L31) ___ @@ -80,4 +80,4 @@ The name of the ARC-28 event group the event belongs to #### Defined in -[types/arc-28.ts:28](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L28) +[types/arc-28.ts:27](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L27) diff --git a/docs/code/interfaces/types_arc_28.EmittedArc28Event.md b/docs/code/interfaces/types_arc_28.EmittedArc28Event.md index 9984565..55b95fb 100644 --- a/docs/code/interfaces/types_arc_28.EmittedArc28Event.md +++ b/docs/code/interfaces/types_arc_28.EmittedArc28Event.md @@ -34,7 +34,7 @@ The ordered arguments extracted from the event that was emitted #### Defined in -[types/arc-28.ts:42](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L42) +[types/arc-28.ts:41](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L41) ___ @@ -46,7 +46,7 @@ The named arguments extracted from the event that was emitted (where the argumen #### Defined in -[types/arc-28.ts:44](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L44) +[types/arc-28.ts:43](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L43) ___ @@ -62,7 +62,7 @@ The ARC-28 definition of the event #### Defined in -[types/arc-28.ts:36](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L36) +[types/arc-28.ts:35](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L35) ___ @@ -78,7 +78,7 @@ The name of the ARC-28 event that was triggered #### Defined in -[types/arc-28.ts:30](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L30) +[types/arc-28.ts:29](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L29) ___ @@ -94,7 +94,7 @@ The 4-byte hex prefix for the event #### Defined in -[types/arc-28.ts:34](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L34) +[types/arc-28.ts:33](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L33) ___ @@ -110,7 +110,7 @@ The signature of the event e.g. `EventName(type1,type2)` #### Defined in -[types/arc-28.ts:32](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L32) +[types/arc-28.ts:31](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L31) ___ @@ -126,4 +126,4 @@ The name of the ARC-28 event group the event belongs to #### Defined in -[types/arc-28.ts:28](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L28) +[types/arc-28.ts:27](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/arc-28.ts#L27) diff --git a/docs/code/modules/index.md b/docs/code/modules/index.md index bf4b9b9..4841c04 100644 --- a/docs/code/modules/index.md +++ b/docs/code/modules/index.md @@ -38,7 +38,7 @@ The blocks #### Defined in -[subscriptions.ts:867](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriptions.ts#L867) +[subscriptions.ts:868](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriptions.ts#L868) ___ @@ -65,4 +65,4 @@ The result of this subscription pull/poll. #### Defined in -[subscriptions.ts:53](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriptions.ts#L53) +[subscriptions.ts:54](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriptions.ts#L54) diff --git a/src/subscriptions.ts b/src/subscriptions.ts index 16da28b..a29af20 100644 --- a/src/subscriptions.ts +++ b/src/subscriptions.ts @@ -1,7 +1,7 @@ import * as algokit from '@algorandfoundation/algokit-utils' import type { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer' import * as msgpack from 'algorand-msgpack' -import algosdk, { OnApplicationComplete } from 'algosdk' +import algosdk from 'algosdk' import type SearchForTransactions from 'algosdk/dist/types/client/v2/indexer/searchForTransactions' import sha512, { sha512_256 } from 'js-sha512' import { @@ -27,6 +27,7 @@ import ABIValue = algosdk.ABIValue import Algodv2 = algosdk.Algodv2 import Indexer = algosdk.Indexer import TransactionType = algosdk.TransactionType +import OnApplicationComplete = algosdk.OnApplicationComplete const deduplicateSubscribedTransactionsReducer = (dedupedTransactions: SubscribedTransaction[], t: SubscribedTransaction) => { const existing = dedupedTransactions.find((e) => e.id === t.id) diff --git a/src/types/arc-28.ts b/src/types/arc-28.ts index 2c84c15..a1ee438 100644 --- a/src/types/arc-28.ts +++ b/src/types/arc-28.ts @@ -1,7 +1,6 @@ import type { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer' import algosdk from 'algosdk' import ABIValue = algosdk.ABIValue -import TransactionType = algosdk.TransactionType /** * The definition of metadata for an ARC-28 event per https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0028.md#event. diff --git a/tests/scenarios/filters.spec.ts b/tests/scenarios/filters.spec.ts index f7fe2f3..9b5d26d 100644 --- a/tests/scenarios/filters.spec.ts +++ b/tests/scenarios/filters.spec.ts @@ -1,5 +1,6 @@ import * as algokit from '@algorandfoundation/algokit-utils' import { ApplicationOnComplete } from '@algorandfoundation/algokit-utils/types/indexer' +import { SendAtomicTransactionComposerResults } from '@algorandfoundation/algokit-utils/types/transaction' import algosdk, { Account, TransactionType } from 'algosdk' import { afterEach, beforeAll, beforeEach, describe, test } from 'vitest' import { TestingAppClient } from '../contract/client' @@ -7,7 +8,12 @@ import { filterFixture } from '../filterFixture' describe('Subscribing using various filters', () => { const { localnet, systemAccount, subscribeAndVerifyFilter, extractFromGroupResult, ...hooks } = filterFixture() - beforeAll(hooks.beforeAll, 10_000) + const beforeAllFixtures: (() => Promise)[] = [] + beforeAll(async () => { + await hooks.beforeAll() + await beforeAllFixtures.map(async (fixture) => await fixture()) + await localnet.context.waitForIndexer() + }, 30_000) beforeEach(hooks.beforeEach, 10_000) afterEach(hooks.afterEach) @@ -57,17 +63,25 @@ describe('Subscribing using various filters', () => { } } - test('Works for receiver(s) and sender(s)', async () => { - const { testAccount, algod, generateAccount } = localnet.context + let algoTransfersData: + | { + testAccount: Account + account2: Account + account3: Account + txns: SendAtomicTransactionComposerResults + } + | undefined = undefined + const algoTransfersFixture = async () => { + const { algod, generateAccount } = localnet.context + const testAccount = await generateAccount({ initialFunds: (10).algos() }) const account2 = await generateAccount({ initialFunds: (3).algos() }) const account3 = algokit.randomAccount() - const amount = (1).algos() const txns = await algokit.sendGroupOfTransactions( { transactions: [ - await algokit.transferAlgos({ amount, from: testAccount, to: account2, skipSending: true }, algod), - await algokit.transferAlgos({ amount, from: testAccount, to: account3, skipSending: true }, algod), - await algokit.transferAlgos({ amount, from: account2, to: testAccount, skipSending: true }, algod), + await algokit.transferAlgos({ amount: (1).algos(), from: testAccount, to: account2, note: 'a', skipSending: true }, algod), + await algokit.transferAlgos({ amount: (2).algos(), from: testAccount, to: account3, note: 'b', skipSending: true }, algod), + await algokit.transferAlgos({ amount: (1).algos(), from: account2, to: testAccount, note: 'c', skipSending: true }, algod), ].map((t) => ({ transaction: t.transaction, signer: algosdk.encodeAddress(t.transaction.from.publicKey) === testAccount.addr ? testAccount : account2, @@ -76,202 +90,121 @@ describe('Subscribing using various filters', () => { algod, ) - await subscribeAndVerifyFilter( - { - receiver: account2.addr, - }, - extractFromGroupResult(txns, 0), - ) - - await subscribeAndVerifyFilter( - { - receiver: [account2.addr, account3.addr], - }, - [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 1)], - ) - - await subscribeAndVerifyFilter( - { - sender: account2.addr, - }, - extractFromGroupResult(txns, 2), - ) - - await subscribeAndVerifyFilter( - { - sender: [testAccount.addr, account2.addr], - }, - [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 1), extractFromGroupResult(txns, 2)], - ) - }) + algoTransfersData = { + testAccount, + account2, + account3, + txns, + } + } + beforeAllFixtures.push(algoTransfersFixture) + + describe('Algo transfers', () => { + test('Single receiver', async () => { + const { account2, txns } = algoTransfersData! + + await subscribeAndVerifyFilter( + { + receiver: account2.addr, + }, + extractFromGroupResult(txns, 0), + ) + }) + test('Single sender', async () => { + const { account2, txns } = algoTransfersData! + + await subscribeAndVerifyFilter( + { + sender: account2.addr, + }, + extractFromGroupResult(txns, 2), + ) + }) + test('Multiple receivers', async () => { + const { account2, account3, txns } = algoTransfersData! + + await subscribeAndVerifyFilter( + { + receiver: [account2.addr, account3.addr], + }, + [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 1)], + ) + }) + test('Multiple senders', async () => { + const { testAccount, account2, txns } = algoTransfersData! + + await subscribeAndVerifyFilter( + { + sender: [testAccount.addr, account2.addr], + }, + [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 1), extractFromGroupResult(txns, 2)], + ) + }) - test('Works for min amount of algos', async () => { - const { testAccount, algod } = localnet.context - const txns = await algokit.sendGroupOfTransactions( - { - transactions: [ - algokit.transferAlgos({ amount: (1).algos(), from: testAccount, to: testAccount, skipSending: true }, algod), - algokit.transferAlgos({ amount: (2).algos(), from: testAccount, to: testAccount, skipSending: true }, algod), - ], - signer: testAccount, - }, - algod, - ) + test('Works for min amount of algos', async () => { + const { testAccount, txns } = algoTransfersData! - await subscribeAndVerifyFilter( - { - sender: testAccount.addr, - minAmount: (1).algos().microAlgos + 1, - }, - extractFromGroupResult(txns, 1), - ) - }) - - test('Works for max amount of algos', async () => { - const { testAccount, algod } = localnet.context - const txns = await algokit.sendGroupOfTransactions( - { - transactions: [ - algokit.transferAlgos({ amount: (1).algos(), from: testAccount, to: testAccount, skipSending: true }, algod), - algokit.transferAlgos({ amount: (2).algos(), from: testAccount, to: testAccount, skipSending: true }, algod), - ], - signer: testAccount, - }, - algod, - ) + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + minAmount: (1).algos().microAlgos + 1, + }, + extractFromGroupResult(txns, 1), + ) + }) - await subscribeAndVerifyFilter( - { - sender: testAccount.addr, - maxAmount: (1).algos().microAlgos + 1, - }, - extractFromGroupResult(txns, 0), - ) - }) + test('Works for max amount of algos', async () => { + const { testAccount, txns } = algoTransfersData! - test('Works for note prefix', async () => { - const { testAccount, algod } = localnet.context - const amount = (1).algos() - const txns = await algokit.sendGroupOfTransactions( - { - transactions: [ - algokit.transferAlgos({ amount, from: testAccount, to: testAccount, note: 'a', skipSending: true }, algod), - algokit.transferAlgos({ amount, from: testAccount, to: testAccount, note: 'b', skipSending: true }, algod), - ], - signer: testAccount, - }, - algod, - ) + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + maxAmount: (1).algos().microAlgos + 1, + }, + extractFromGroupResult(txns, 0), + ) + }) - await subscribeAndVerifyFilter( - { - sender: testAccount.addr, - notePrefix: 'a', - }, - extractFromGroupResult(txns, 0), - ) + test('Works for note prefix', async () => { + const { testAccount, txns } = algoTransfersData! + + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + notePrefix: 'a', + }, + extractFromGroupResult(txns, 0), + ) + + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + notePrefix: 'b', + }, + extractFromGroupResult(txns, 1), + ) + }) }) - test('Works for asset ID(s)', async () => { - const { testAccount, algod } = localnet.context - const asset1 = await createAsset() + let assetsData: + | { + asset1: Awaited> + asset2: Awaited> + testAccount: Account + txns: SendAtomicTransactionComposerResults + } + | undefined = undefined + const assetsFixture = async () => { + const { algod, generateAccount } = localnet.context + const testAccount = await generateAccount({ initialFunds: (10).algos() }) + const asset1 = await createAsset(testAccount) const asset2 = await createAsset() const txns = await algokit.sendGroupOfTransactions( { transactions: [ algokit.assetOptIn({ account: testAccount, assetId: asset1.assetId, skipSending: true }, algod), algokit.assetOptIn({ account: testAccount, assetId: asset2.assetId, skipSending: true }, algod), - ], - signer: testAccount, - }, - algod, - ) - - await subscribeAndVerifyFilter( - { - sender: testAccount.addr, - assetId: asset1.assetId, - }, - extractFromGroupResult(txns, 0), - ) - - await subscribeAndVerifyFilter( - { - sender: testAccount.addr, - assetId: [asset1.assetId, asset2.assetId], - }, - [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 1)], - ) - }) - - test('Works for asset create', async () => { - const { testAccount, algod } = localnet.context - const asset1 = await createAsset() - const txns = await algokit.sendGroupOfTransactions( - { - transactions: [ - algokit.assetOptIn({ account: testAccount, assetId: asset1.assetId, skipSending: true }, algod), await createAssetTxn(testAccount), - ], - signer: testAccount, - }, - algod, - ) - - await subscribeAndVerifyFilter( - { - sender: testAccount.addr, - assetCreate: true, - }, - extractFromGroupResult(txns, 1), - ) - }) - - test('Works for transaction type(s)', async () => { - const { testAccount, algod } = localnet.context - const asset1 = await createAsset() - const txns = await algokit.sendGroupOfTransactions( - { - transactions: [ - algokit.assetOptIn({ account: testAccount, assetId: asset1.assetId, skipSending: true }, algod), - await createAssetTxn(testAccount), - ], - signer: testAccount, - }, - algod, - ) - - await subscribeAndVerifyFilter( - { - sender: testAccount.addr, - type: TransactionType.axfer, - }, - extractFromGroupResult(txns, 0), - ) - - await subscribeAndVerifyFilter( - { - sender: testAccount.addr, - type: TransactionType.acfg, - }, - extractFromGroupResult(txns, 1), - ) - - await subscribeAndVerifyFilter( - { - sender: testAccount.addr, - type: [TransactionType.acfg, TransactionType.axfer], - }, - [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 1)], - ) - }) - - test('Works for min amount of asset', async () => { - const { testAccount, algod } = localnet.context - const asset1 = await createAsset(testAccount) - const txns = await algokit.sendGroupOfTransactions( - { - transactions: [ algokit.transferAsset({ assetId: asset1.assetId, amount: 1, from: testAccount, to: testAccount, skipSending: true }, algod), algokit.transferAsset({ assetId: asset1.assetId, amount: 2, from: testAccount, to: testAccount, skipSending: true }, algod), ], @@ -280,133 +213,170 @@ describe('Subscribing using various filters', () => { algod, ) - await subscribeAndVerifyFilter( - { - type: TransactionType.axfer, - sender: testAccount.addr, - minAmount: 2, - }, - extractFromGroupResult(txns, 1), - ) - }) + assetsData = { + asset1, + asset2, + testAccount, + txns, + } + } + beforeAllFixtures.push(assetsFixture) + + describe('Asset transactions', () => { + test('Works for single asset ID', async () => { + const { testAccount, asset1, txns } = assetsData! + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + assetId: asset1.assetId, + }, + [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 3), extractFromGroupResult(txns, 4)], + ) + }) - test('Works for max amount of asset', async () => { - const { testAccount, algod } = localnet.context - const asset1 = await createAsset(testAccount) - const txns = await algokit.sendGroupOfTransactions( - { - transactions: [ - algokit.transferAsset({ assetId: asset1.assetId, amount: 1, from: testAccount, to: testAccount, skipSending: true }, algod), - algokit.transferAsset({ assetId: asset1.assetId, amount: 2, from: testAccount, to: testAccount, skipSending: true }, algod), + test('Works for multiple asset IDs', async () => { + const { testAccount, asset1, asset2, txns } = assetsData! + + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + assetId: [asset1.assetId, asset2.assetId], + }, + [ + extractFromGroupResult(txns, 0), + extractFromGroupResult(txns, 1), + extractFromGroupResult(txns, 3), + extractFromGroupResult(txns, 4), ], - signer: testAccount, - }, - algod, - ) - - await subscribeAndVerifyFilter( - { - type: TransactionType.axfer, - sender: testAccount.addr, - maxAmount: 1, - }, - extractFromGroupResult(txns, 0), - ) - }) + ) + }) - test('Works for app create', async () => { - const { testAccount, algod } = localnet.context - const app1 = await app({ create: true }) - const txns = await algokit.sendGroupOfTransactions( - { - transactions: [ - app1.app.callAbi({ value: 'test' }, { sender: testAccount, sendParams: { skipSending: true } }), - (await app({ create: false }, testAccount)).creation.transaction, - ], - signer: testAccount, - }, - algod, - ) + test('Works for asset create', async () => { + const { testAccount, txns } = assetsData! - await subscribeAndVerifyFilter( - { - sender: testAccount.addr, - appCreate: true, - }, - extractFromGroupResult(txns, 1), - ) - }) + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + assetCreate: true, + }, + extractFromGroupResult(txns, 2), + ) + }) - test('Works for app ID(s)', async () => { - const { testAccount, algod } = localnet.context - const app1 = await app({ create: true }) - const app2 = await app({ create: true }) - const txns = await algokit.sendGroupOfTransactions( - { - transactions: [ - app1.app.callAbi({ value: 'test' }, { sender: testAccount, sendParams: { skipSending: true } }), - app2.app.callAbi({ value: 'test' }, { sender: testAccount, sendParams: { skipSending: true } }), + test('Works for transaction type(s)', async () => { + const { testAccount, txns } = assetsData! + + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + type: TransactionType.axfer, + }, + [ + extractFromGroupResult(txns, 0), + extractFromGroupResult(txns, 1), + extractFromGroupResult(txns, 3), + extractFromGroupResult(txns, 4), ], - signer: testAccount, - }, - algod, - ) - - await subscribeAndVerifyFilter( - { - sender: testAccount.addr, - appId: Number(app1.creation.confirmation!.applicationIndex!), - }, - extractFromGroupResult(txns, 0), - ) + ) + + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + type: TransactionType.acfg, + }, + extractFromGroupResult(txns, 2), + ) + + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + type: [TransactionType.acfg, TransactionType.axfer], + }, + [ + extractFromGroupResult(txns, 0), + extractFromGroupResult(txns, 1), + extractFromGroupResult(txns, 2), + extractFromGroupResult(txns, 3), + extractFromGroupResult(txns, 4), + ], + ) + }) - await subscribeAndVerifyFilter( - { - sender: testAccount.addr, - appId: [Number(app1.creation.confirmation!.applicationIndex!), Number(app2.creation.confirmation!.applicationIndex!)], - }, - [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 1)], - ) - }) + test('Works for min amount of asset', async () => { + const { testAccount, txns } = assetsData! + + await subscribeAndVerifyFilter( + { + type: TransactionType.axfer, + sender: testAccount.addr, + minAmount: 2, + }, + extractFromGroupResult(txns, 4), + ) + }) - test('Works for on-complete(s)', async () => { - const { testAccount, algod } = localnet.context - const app1 = await app({ create: true }) - const txns = await algokit.sendGroupOfTransactions( - { - transactions: [ - app1.app.callAbi({ value: 'test' }, { sender: testAccount, sendParams: { skipSending: true } }), - app1.app.optIn.optIn({}, { sender: testAccount, sendParams: { skipSending: true } }), - ], - signer: testAccount, - }, - algod, - ) + test('Works for max amount of asset', async () => { + const { testAccount, txns } = assetsData! + + await subscribeAndVerifyFilter( + { + type: TransactionType.axfer, + sender: testAccount.addr, + maxAmount: 1, + }, + [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 1), extractFromGroupResult(txns, 3)], + ) + }) - await subscribeAndVerifyFilter( - { - sender: testAccount.addr, - appOnComplete: ApplicationOnComplete.optin, - }, - extractFromGroupResult(txns, 1), - ) + test('Works for max amount of asset with asset ID', async () => { + const { testAccount, txns, asset1 } = assetsData! + + await subscribeAndVerifyFilter( + { + type: TransactionType.axfer, + sender: testAccount.addr, + maxAmount: 1, + assetId: asset1.assetId, + }, + [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 3)], + ) + }) - await subscribeAndVerifyFilter( - { - sender: testAccount.addr, - appOnComplete: [ApplicationOnComplete.optin, ApplicationOnComplete.noop], - }, - [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 1)], - ) + test('Works for min and max amount of asset with asset ID', async () => { + const { testAccount, txns, asset1 } = assetsData! + + await subscribeAndVerifyFilter( + { + type: TransactionType.axfer, + sender: testAccount.addr, + minAmount: 1, + maxAmount: 1, + assetId: asset1.assetId, + }, + [extractFromGroupResult(txns, 3)], + ) + }) }) - test('Works for method signature(s)', async () => { - const { testAccount, algod } = localnet.context + let appData: + | { + app1: Awaited> + app2: Awaited> + testAccount: Account + txns: SendAtomicTransactionComposerResults + } + | undefined = undefined + const appsFixture = async () => { + const { algod, generateAccount } = localnet.context + const testAccount = await generateAccount({ initialFunds: (10).algos() }) const app1 = await app({ create: true }) + const app2 = await app({ create: true }) const txns = await algokit.sendGroupOfTransactions( { transactions: [ - app1.app.callAbi({ value: 'test' }, { sender: testAccount, sendParams: { skipSending: true } }), + app1.app.callAbi({ value: 'test1' }, { sender: testAccount, sendParams: { skipSending: true } }), + app2.app.callAbi({ value: 'test2' }, { sender: testAccount, sendParams: { skipSending: true } }), + (await app({ create: false }, testAccount)).creation.transaction, app1.app.optIn.optIn({}, { sender: testAccount, sendParams: { skipSending: true } }), ], signer: testAccount, @@ -414,67 +384,117 @@ describe('Subscribing using various filters', () => { algod, ) - await subscribeAndVerifyFilter( - { - sender: testAccount.addr, - methodSignature: 'opt_in()void', - }, - extractFromGroupResult(txns, 1), - ) - - await subscribeAndVerifyFilter( - { - sender: testAccount.addr, - methodSignature: ['opt_in()void', 'madeUpMethod()void'], - }, - extractFromGroupResult(txns, 1), - ) + appData = { + app1, + app2, + testAccount, + txns, + } + } + beforeAllFixtures.push(appsFixture) + + describe('App transactions', () => { + test('Works for app create', async () => { + const { testAccount, txns } = appData! + + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + appCreate: true, + }, + extractFromGroupResult(txns, 2), + ) + }) - await subscribeAndVerifyFilter( - { - sender: testAccount.addr, - methodSignature: ['opt_in()void', 'call_abi(string)string'], - }, - [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 1)], - ) - }) + test('Works for app ID(s)', async () => { + const { testAccount, app1, app2, txns } = appData! + + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + appId: Number(app1.creation.confirmation!.applicationIndex!), + }, + [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 3)], + ) + + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + appId: [Number(app1.creation.confirmation!.applicationIndex!), Number(app2.creation.confirmation!.applicationIndex!)], + }, + [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 1), extractFromGroupResult(txns, 3)], + ) + }) - test('Works for app args', async () => { - const { testAccount, algod } = localnet.context - const app1 = await app({ create: true }) - const txns = await algokit.sendGroupOfTransactions( - { - transactions: [ - app1.app.callAbi({ value: 'test1' }, { sender: testAccount, sendParams: { skipSending: true } }), - app1.app.callAbi({ value: 'test2' }, { sender: testAccount, sendParams: { skipSending: true } }), + test('Works for on-complete(s)', async () => { + const { testAccount, txns } = appData! + + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + appOnComplete: ApplicationOnComplete.optin, + }, + extractFromGroupResult(txns, 3), + ) + + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + appOnComplete: [ApplicationOnComplete.optin, ApplicationOnComplete.noop], + }, + [ + extractFromGroupResult(txns, 0), + extractFromGroupResult(txns, 1), + extractFromGroupResult(txns, 2), + extractFromGroupResult(txns, 3), ], - signer: testAccount, - }, - algod, - ) + ) + }) - await subscribeAndVerifyFilter( - { - sender: testAccount.addr, - // ARC-4 string has first 2 bytes with length of the string so slice them off before comparing - appCallArgumentsMatch: (args) => !!args && Buffer.from(args[1].slice(2)).toString('utf-8') === 'test1', - }, - extractFromGroupResult(txns, 0), - ) + test('Works for method signature(s)', async () => { + const { testAccount, txns } = appData! + + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + methodSignature: 'opt_in()void', + }, + extractFromGroupResult(txns, 3), + ) + + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + methodSignature: ['opt_in()void', 'madeUpMethod()void'], + }, + extractFromGroupResult(txns, 3), + ) + + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + methodSignature: ['opt_in()void', 'call_abi(string)string'], + }, + [extractFromGroupResult(txns, 0), extractFromGroupResult(txns, 1), extractFromGroupResult(txns, 3)], + ) + }) + + test('Works for app args', async () => { + const { testAccount, txns } = appData! + + await subscribeAndVerifyFilter( + { + sender: testAccount.addr, + // ARC-4 string has first 2 bytes with length of the string so slice them off before comparing + appCallArgumentsMatch: (args) => !!args && args.length > 1 && Buffer.from(args[1].slice(2)).toString('utf-8') === 'test1', + }, + extractFromGroupResult(txns, 0), + ) + }) }) test('Works for custom filter', async () => { - const { testAccount, algod } = localnet.context - const txns = await algokit.sendGroupOfTransactions( - { - transactions: [ - algokit.transferAlgos({ amount: (1).algos(), from: testAccount, to: testAccount, skipSending: true }, algod), - algokit.transferAlgos({ amount: (2).algos(), from: testAccount, to: testAccount, skipSending: true }, algod), - ], - signer: testAccount, - }, - algod, - ) + const { testAccount, txns } = algoTransfersData! await subscribeAndVerifyFilter( {