Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adds filtering to wallet/getNotes endpoint #3867

Merged
merged 4 commits into from
May 9, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 209 additions & 2 deletions ironfish/src/rpc/routes/wallet/getNotes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -16,16 +17,23 @@ describe('Route wallet/getNotes', () => {
const routeTest = createRouteTest(true)
let account: Account
let accountNotesByHash: BufferMap<RpcWalletNote>
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<RpcWalletNote>()
Expand Down Expand Up @@ -99,4 +107,203 @@ 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<GetNotesResponse> =
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<GetNotesResponse> =
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<GetNotesResponse> =
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<GetNotesResponse> =
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<GetNotesResponse> = await routeTest.client.wallet.getNotes(
{
account: account.name,
filter: {
assetId: 'deadbeef',
},
},
)

expect(response.status).toBe(200)
expect(response.content.notes).toHaveLength(0)
})

it('filters notes by assetName', async () => {
const ironResponse: RpcResponseEnded<GetNotesResponse> =
await routeTest.client.wallet.getNotes({
account: account.name,
filter: {
assetName: Buffer.from('$IRON', 'utf-8').toString('hex'),
},
})

expect(ironResponse.status).toBe(200)
expect(ironResponse.content.notes.length).toBe(3)

const response: RpcResponseEnded<GetNotesResponse> = await routeTest.client.wallet.getNotes(
{
account: account.name,
filter: {
assetName: '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<GetNotesResponse> =
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<GetNotesResponse> =
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<GetNotesResponse> =
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<GetNotesResponse> = 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<GetNotesResponse> = 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()
})
})
60 changes: 59 additions & 1 deletion ironfish/src/rpc/routes/wallet/getNotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,29 @@ import { getAccount, serializeRpcWalletNote } from './utils'

const DEFAULT_PAGE_SIZE = 100

type StringMinMax = {
min?: string
max?: string
}

type GetNotesRequestFilter = {
value?: StringMinMax
assetId?: string
assetName?: 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 = {
Expand All @@ -24,6 +43,23 @@ export const GetNotesRequestSchema: yup.ObjectSchema<GetNotesRequest> = 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(),
assetName: 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()

Expand Down Expand Up @@ -59,7 +95,13 @@ router.register<typeof GetNotesRequestSchema, GetNotesResponse>(

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({
Expand All @@ -68,3 +110,19 @@ router.register<typeof GetNotesRequestSchema, GetNotesResponse>(
})
},
)

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.assetName === undefined || note.assetName === filter.assetName) &&
(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)
)
}