diff --git a/ironfish/src/rpc/routes/wallet/getNotes.test.ts b/ironfish/src/rpc/routes/wallet/getNotes.test.ts index b984cd1012..d44b2f278a 100644 --- a/ironfish/src/rpc/routes/wallet/getNotes.test.ts +++ b/ironfish/src/rpc/routes/wallet/getNotes.test.ts @@ -2,8 +2,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { Asset } from '@ironfish/rust-nodejs' -import { BufferMap } from 'buffer-map' +import { BufferMap, BufferSet } from 'buffer-map' import { Assert } from '../../../assert' +import { Transaction } from '../../../primitives' import { useAccountFixture, useBlockWithTx } from '../../../testUtilities' import { createRouteTest } from '../../../testUtilities/routeTest' import { Account } from '../../../wallet' @@ -16,16 +17,23 @@ describe('Route wallet/getNotes', () => { const routeTest = createRouteTest(true) let account: Account let accountNotesByHash: BufferMap + let transaction: Transaction beforeAll(async () => { const node = routeTest.node account = await useAccountFixture(node.wallet, 'account') - const { previous, block } = await useBlockWithTx(node, account, account, true) + const { + previous, + block, + transaction: blockTransaction, + } = await useBlockWithTx(node, account, account, true) await node.chain.addBlock(block) await node.wallet.updateHead() + transaction = blockTransaction + const asset = await account.getAsset(Asset.nativeId()) accountNotesByHash = new BufferMap() @@ -99,4 +107,178 @@ describe('Route wallet/getNotes', () => { // last nextPageCursor expect(nextPageCursor).toBeNull() }) + + it('filters notes by value', async () => { + // notes have values 1, 199999998, and 200000000 + const minValue = '2' + const maxValue = '1999999999' + + const minResponse: RpcResponseEnded = + await routeTest.client.wallet.getNotes({ + account: account.name, + filter: { + value: { min: minValue }, + }, + }) + const { notes: minResponseNotes } = minResponse.content + + expect(minResponse.status).toBe(200) + expect(minResponseNotes.length).toBe(2) + + const maxResponse: RpcResponseEnded = + await routeTest.client.wallet.getNotes({ + account: account.name, + filter: { + value: { max: maxValue }, + }, + }) + const { notes: maxResponseNotes } = maxResponse.content + + expect(maxResponse.status).toBe(200) + expect(maxResponseNotes.length).toBe(2) + + const minMaxResponse: RpcResponseEnded = + await routeTest.client.wallet.getNotes({ + account: account.name, + filter: { + value: { min: minValue, max: maxValue }, + }, + }) + const { notes: minMaxResponseNotes } = minMaxResponse.content + + expect(minMaxResponse.status).toBe(200) + expect(minMaxResponseNotes.length).toBe(1) + }) + + it('filters notes by assetId', async () => { + const nativeResponse: RpcResponseEnded = + await routeTest.client.wallet.getNotes({ + account: account.name, + filter: { + assetId: Asset.nativeId().toString('hex'), + }, + }) + + expect(nativeResponse.status).toBe(200) + expect(nativeResponse.content.notes.length).toBe(3) + + const response: RpcResponseEnded = await routeTest.client.wallet.getNotes( + { + account: account.name, + filter: { + assetId: 'deadbeef', + }, + }, + ) + + expect(response.status).toBe(200) + expect(response.content.notes).toHaveLength(0) + }) + + it('finds notes by index', async () => { + for (const [, note] of accountNotesByHash) { + if (!note.index) { + continue + } + + const response: RpcResponseEnded = + await routeTest.client.wallet.getNotes({ + account: account.name, + filter: { + index: note.index, + }, + }) + + expect(response.status).toBe(200) + expect(response.content.notes.length).toBe(1) + expect(response.content.notes[0]).toEqual(note) + } + }) + + it('finds notes by nullifier', async () => { + for (const [, note] of accountNotesByHash) { + if (!note.nullifier) { + continue + } + + const response: RpcResponseEnded = + await routeTest.client.wallet.getNotes({ + account: account.name, + filter: { + nullifier: note.nullifier, + }, + }) + + expect(response.status).toBe(200) + expect(response.content.notes.length).toBe(1) + expect(response.content.notes[0]).toEqual(note) + } + }) + + it('finds notes by noteHash', async () => { + for (const [, note] of accountNotesByHash) { + const response: RpcResponseEnded = + await routeTest.client.wallet.getNotes({ + account: account.name, + filter: { + noteHash: note.noteHash, + }, + }) + + expect(response.status).toBe(200) + expect(response.content.notes.length).toBe(1) + expect(response.content.notes[0]).toEqual(note) + } + }) + + it('filters notes by transactionHash', async () => { + const response: RpcResponseEnded = await routeTest.client.wallet.getNotes( + { + account: account.name, + filter: { + transactionHash: transaction.hash().toString('hex'), + }, + }, + ) + + expect(response.status).toBe(200) + expect(response.content.notes.length).toBe(2) + for (const note of response.content.notes) { + const accountNote: RpcWalletNote | undefined = accountNotesByHash.get( + Buffer.from(note.noteHash, 'hex'), + ) + Assert.isNotUndefined(accountNote) + + expect(note.transactionHash).toEqual(accountNote.transactionHash) + } + }) + + it('filters notes by spent', async () => { + const filteredNoteHashes = new BufferSet() + + for (const [noteHash, note] of accountNotesByHash) { + if (!note.spent) { + filteredNoteHashes.add(noteHash) + } + } + + const response: RpcResponseEnded = await routeTest.client.wallet.getNotes( + { + account: account.name, + filter: { + spent: false, + }, + }, + ) + const { notes: responseNotes, nextPageCursor } = response.content + + expect(response.status).toBe(200) + expect(responseNotes.length).toBe(filteredNoteHashes.size) + + for (const note of responseNotes) { + expect(note.spent).toBe(false) + } + + expect(nextPageCursor).toBeNull() + }) }) diff --git a/ironfish/src/rpc/routes/wallet/getNotes.ts b/ironfish/src/rpc/routes/wallet/getNotes.ts index c895843b6a..2d7a6dacd6 100644 --- a/ironfish/src/rpc/routes/wallet/getNotes.ts +++ b/ironfish/src/rpc/routes/wallet/getNotes.ts @@ -8,10 +8,28 @@ import { getAccount, serializeRpcWalletNote } from './utils' const DEFAULT_PAGE_SIZE = 100 +type StringMinMax = { + min?: string + max?: string +} + +type GetNotesRequestFilter = { + value?: StringMinMax + assetId?: string + memo?: string + sender?: string + noteHash?: string + transactionHash?: string + index?: number + nullifier?: string + spent?: boolean +} + export type GetNotesRequest = { account?: string pageSize?: number pageCursor?: string + filter?: GetNotesRequestFilter } export type GetNotesResponse = { @@ -24,6 +42,22 @@ export const GetNotesRequestSchema: yup.ObjectSchema = yup account: yup.string().trim(), pageSize: yup.number().min(1), pageCursor: yup.string(), + filter: yup + .object({ + value: yup.object({ + min: yup.string(), + max: yup.string(), + }), + assetId: yup.string(), + memo: yup.string(), + sender: yup.string(), + noteHash: yup.string(), + transactionHash: yup.string(), + index: yup.number(), + nullifier: yup.string(), + spent: yup.boolean(), + }) + .defined(), }) .defined() @@ -59,7 +93,13 @@ router.register( const asset = await account.getAsset(decryptedNote.note.assetId()) - notes.push(serializeRpcWalletNote(decryptedNote, account.publicAddress, asset)) + const note = serializeRpcWalletNote(decryptedNote, account.publicAddress, asset) + + if (!includeNote(note, request.data.filter ?? {})) { + continue + } + + notes.push(note) } request.end({ @@ -68,3 +108,18 @@ router.register( }) }, ) + +function includeNote(note: RpcWalletNote, filter: GetNotesRequestFilter): boolean { + return ( + (filter.value?.min === undefined || BigInt(note.value) >= BigInt(filter.value.min)) && + (filter.value?.max === undefined || BigInt(note.value) <= BigInt(filter.value.max)) && + (filter.assetId === undefined || note.assetId === filter.assetId) && + (filter.memo === undefined || note.memo === filter.memo) && + (filter.sender === undefined || note.sender === filter.sender) && + (filter.noteHash === undefined || note.noteHash === filter.noteHash) && + (filter.transactionHash === undefined || note.transactionHash === filter.transactionHash) && + (filter.index === undefined || note.index === filter.index) && + (filter.nullifier === undefined || note.nullifier === filter.nullifier) && + (filter.spent === undefined || note.spent === filter.spent) + ) +}