diff --git a/docs/code/interfaces/types_subscription.TransactionFilter.md b/docs/code/interfaces/types_subscription.TransactionFilter.md index e610077..9b52488 100644 --- a/docs/code/interfaces/types_subscription.TransactionFilter.md +++ b/docs/code/interfaces/types_subscription.TransactionFilter.md @@ -117,7 +117,7 @@ ___ • `Optional` **maxAmount**: `number` Filter to transactions where the amount being transferred is less than -or equal to the given maximum (microAlgos or decimal units of an ASA). +or equal to the given maximum (microAlgos or decimal units of an ASA if type: axfer). #### Defined in @@ -143,7 +143,7 @@ ___ • `Optional` **minAmount**: `number` Filter to transactions where the amount being transferred is greater -than or equal to the given minimum (microAlgos or decimal units of an ASA). +than or equal to the given minimum (microAlgos or decimal units of an ASA if type: axfer). #### Defined in diff --git a/docs/subscriptions.md b/docs/subscriptions.md index e2f25ed..b4e8411 100644 --- a/docs/subscriptions.md +++ b/docs/subscriptions.md @@ -88,10 +88,10 @@ export interface TransactionFilter { /** Filter to transactions that are creating an asset. */ assetCreate?: boolean /** Filter to transactions where the amount being transferred is greater - * than or equal to the given minimum (microAlgos or decimal units of an ASA). */ + * than or equal to the given minimum (microAlgos or decimal units of an ASA if type: axfer). */ minAmount?: number /** Filter to transactions where the amount being transferred is less than - * or equal to the given maximum (microAlgos or decimal units of an ASA). */ + * or equal to the given maximum (microAlgos or decimal units of an ASA if type: axfer). */ maxAmount?: number /** Filter to app transactions that have the given ARC-0004 method selector for * the given method signature as the first app argument. */ diff --git a/src/subscriptions.ts b/src/subscriptions.ts index 9e1c53e..dea69cf 100644 --- a/src/subscriptions.ts +++ b/src/subscriptions.ts @@ -139,7 +139,7 @@ function indexerPreFilter( filter = filter.txType(subscription.type.toString()) } if (subscription.notePrefix) { - filter = filter.notePrefix(subscription.notePrefix) + filter = filter.notePrefix(Buffer.from(subscription.notePrefix).toString('base64')) } if (subscription.appId) { filter = filter.applicationID(subscription.appId) @@ -210,7 +210,7 @@ function transactionFilter( result &&= !!t.from && encodeAddress(t.from.publicKey) === subscription.sender } if (subscription.receiver) { - result &&= !!t.to && encodeAddress(t.to.publicKey) === subscription.sender + result &&= !!t.to && encodeAddress(t.to.publicKey) === subscription.receiver } if (subscription.type) { result &&= t.type === subscription.type diff --git a/src/transform.ts b/src/transform.ts index ec6c073..c594f6c 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -139,11 +139,13 @@ export function getIndexerTransactionFromAlgodTransaction( 'default-frozen': transaction.assetDefaultFrozen, 'metadata-hash': transaction.assetMetadataHash, name: transaction.assetName, - 'name-b64': encoder.encode(Buffer.from(transaction.assetName).toString('base64')), + 'name-b64': transaction.assetName ? encoder.encode(Buffer.from(transaction.assetName).toString('base64')) : undefined, 'unit-name': transaction.assetUnitName, - 'unit-name-b64': encoder.encode(Buffer.from(transaction.assetUnitName).toString('base64')), + 'unit-name-b64': transaction.assetUnitName + ? encoder.encode(Buffer.from(transaction.assetUnitName).toString('base64')) + : undefined, url: transaction.assetURL, - 'url-b64': encoder.encode(Buffer.from(transaction.assetURL).toString('base64')), + 'url-b64': transaction.assetURL ? encoder.encode(Buffer.from(transaction.assetURL).toString('base64')) : undefined, 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, diff --git a/src/types/subscription.ts b/src/types/subscription.ts index 4d54a5f..e54fee5 100644 --- a/src/types/subscription.ts +++ b/src/types/subscription.ts @@ -64,10 +64,10 @@ export interface TransactionFilter { /** Filter to transactions that are creating an asset. */ assetCreate?: boolean /** Filter to transactions where the amount being transferred is greater - * than or equal to the given minimum (microAlgos or decimal units of an ASA). */ + * than or equal to the given minimum (microAlgos or decimal units of an ASA if type: axfer). */ minAmount?: number /** Filter to transactions where the amount being transferred is less than - * or equal to the given maximum (microAlgos or decimal units of an ASA). */ + * or equal to the given maximum (microAlgos or decimal units of an ASA if type: axfer). */ maxAmount?: number /** Filter to app transactions that have the given ARC-0004 method selector for * the given method signature as the first app argument. */ diff --git a/tests/scenarios/filters.spec.ts b/tests/scenarios/filters.spec.ts new file mode 100644 index 0000000..387e616 --- /dev/null +++ b/tests/scenarios/filters.spec.ts @@ -0,0 +1,323 @@ +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, TransactionType } from 'algosdk' +import { TransactionFilter } from '../../src/types/subscription' +import { GetSubscribedTransactions, SendXTransactions } from '../transactions' + +describe('Subscribing using various filters', () => { + 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.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.confirmation?.confirmedRound) - 1, + currentRound: Number(result.confirmation?.confirmedRound), + filter, + }, + localnet.context.algod, + ), + GetSubscribedTransactions( + { + roundsToSync: 1, + syncBehaviour: 'catchup-with-indexer', + watermark: 0, + currentRound: Number(result.confirmation?.confirmedRound) + 1, + filter, + }, + localnet.context.algod, + localnet.context.indexer, + ), + ]) + 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()) + return { algod, indexer } + } + + const extractFromGroupResult = (groupResult: Omit, index: number) => { + return { + transaction: 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), + }) + } + + test('Works for receiver', async () => { + const { testAccount, algod } = localnet.context + const account2 = algokit.randomAccount() + const amount = (1).algos() + const account3 = algokit.randomAccount() + 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, + }, + algod, + ) + + await subscribeAndVerifyFilter( + { + receiver: account2.addr, + }, + extractFromGroupResult(txns, 0), + ) + }) + + 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, + ) + + 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, + maxAmount: (1).algos().microAlgos + 1, + }, + extractFromGroupResult(txns, 0), + ) + }) + + 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, + notePrefix: 'a', + }, + extractFromGroupResult(txns, 0), + ) + }) + + test('Works for asset ID', async () => { + const { testAccount, algod } = localnet.context + const asset1 = await createAsset() + 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), + ) + }) + + 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 asset config txn', 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.acfg, + }, + extractFromGroupResult(txns, 1), + ) + }) + + test('Works for asset transfer txn', 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), + ) + }) + + 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), + ], + signer: testAccount, + }, + algod, + ) + + await subscribeAndVerifyFilter( + { + type: TransactionType.axfer, + sender: testAccount.addr, + minAmount: 2, + }, + extractFromGroupResult(txns, 1), + ) + }) + + 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), + ], + signer: testAccount, + }, + algod, + ) + + await subscribeAndVerifyFilter( + { + type: TransactionType.axfer, + sender: testAccount.addr, + maxAmount: 1, + }, + extractFromGroupResult(txns, 0), + ) + }) +}) diff --git a/tests/transactions.ts b/tests/transactions.ts index 516c199..e764c61 100644 --- a/tests/transactions.ts +++ b/tests/transactions.ts @@ -2,7 +2,7 @@ import * as algokit from '@algorandfoundation/algokit-utils' import { SendTransactionFrom, SendTransactionResult } from '@algorandfoundation/algokit-utils/types/transaction' import { Algodv2, Indexer } from 'algosdk' import { getSubscribedTransactions } from '../src' -import { TransactionSubscriptionParams } from '../src/types/subscription' +import { TransactionFilter, TransactionSubscriptionParams } from '../src/types/subscription' export const SendXTransactions = async (x: number, account: SendTransactionFrom, algod: Algodv2) => { const txns: SendTransactionResult[] = [] @@ -28,18 +28,18 @@ export const SendXTransactions = async (x: number, account: SendTransactionFrom, } } -export const GetSubscribedTransactionsFromSender = ( +export const GetSubscribedTransactions = ( subscription: { syncBehaviour: TransactionSubscriptionParams['syncBehaviour'] roundsToSync: number watermark?: number currentRound?: number + filter: TransactionFilter }, - account: SendTransactionFrom, algod: Algodv2, indexer?: Indexer, ) => { - const { roundsToSync, syncBehaviour, watermark, currentRound } = subscription + const { roundsToSync, syncBehaviour, watermark, currentRound, filter } = subscription if (currentRound !== undefined) { const existingStatus = algod.status @@ -58,9 +58,7 @@ export const GetSubscribedTransactionsFromSender = ( return getSubscribedTransactions( { - filter: { - sender: algokit.getSenderAddress(account), - }, + filter: filter, maxRoundsToSync: roundsToSync, syncBehaviour: syncBehaviour, watermark: watermark ?? 0, @@ -69,3 +67,26 @@ export const GetSubscribedTransactionsFromSender = ( indexer, ) } + +export const GetSubscribedTransactionsFromSender = ( + subscription: { + syncBehaviour: TransactionSubscriptionParams['syncBehaviour'] + roundsToSync: number + watermark?: number + currentRound?: number + }, + account: SendTransactionFrom, + algod: Algodv2, + indexer?: Indexer, +) => { + return GetSubscribedTransactions( + { + ...subscription, + filter: { + sender: algokit.getSenderAddress(account), + }, + }, + algod, + indexer, + ) +}