Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
35 changes: 33 additions & 2 deletions api-specs/openrpc-user-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -876,7 +876,28 @@
},
{
"name": "listTransactions",
"params": [],
"params": [
{
"name": "params",
"schema": {
"title": "ListTransactionsParams",
"type": "object",
"additionalProperties": false,
"properties": {
"limit": {
"title": "limit",
"type": "integer",
"description": "Limit of transactions to return."
},
"cursor": {
"title": "cursor",
"type": ["string"],
"description": "Cursor for next page of results."
}
}
}
}
],
"result": {
"name": "result",
"schema": {
Expand All @@ -890,9 +911,19 @@
"items": {
"$ref": "#/components/schemas/Transaction"
}
},
"nextCursor": {
"title": "nextCursor",
"type": "string",
"description": "Cursor for next page of results."
},
"count": {
"title": "count",
"type": "number",
"description": "Number of total transactions for the user."
}
},
"required": ["transactions"]
"required": ["transactions", "count"]
}
}
},
Expand Down
98 changes: 96 additions & 2 deletions core/wallet-store-inmemory/src/store-internal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ const implementations: Array<[string, StoreCtor]> = [
['StoreInternal', StoreInternal],
]

function addTx(id: string, createdAt: Date | undefined) {
const initial: Transaction = {
id: id,
commandId: 'cmd-immutable',
status: 'pending',
preparedTransaction: 'prepared-1',
preparedTransactionHash: 'hash-1',
payload: { amount: 100 },
origin: 'https://safe.example',
createdAt: createdAt,
}
return initial
}
implementations.forEach(([name, StoreImpl]) => {
describe(name, () => {
let store: Store
Expand Down Expand Up @@ -663,7 +676,7 @@ implementations.forEach(([name, StoreImpl]) => {
new Date('2026-01-01T00:01:00.000Z')
)

const duplicates = await store.listTransactions()
const duplicates = (await store.listTransactions()).transactions
expect(
duplicates.filter((tx) => tx.commandId === initial.commandId)
).toHaveLength(2)
Expand Down Expand Up @@ -767,7 +780,9 @@ implementations.forEach(([name, StoreImpl]) => {
).rejects.toThrow('Transaction not found')

await store.removeTransaction('tx-0')
expect(await store.listTransactions()).toHaveLength(1)
expect((await store.listTransactions()).transactions).toHaveLength(
1
)
})

test('should manage message signing requests', async () => {
Expand Down Expand Up @@ -870,5 +885,84 @@ implementations.forEach(([name, StoreImpl]) => {
'ApiKey userId mismatch: expected other-user-id, got test-user-id'
)
})

test('paginate list transactions', async () => {
for (let i = 0; i < 10; i++) {
const date =
i % 2 > 0
? new Date(`2026-01-02T00:00:00.000Z`)
: new Date(`2026-01-01T00:00:00.000Z`)

const tx = addTx(`tx${i}`, date)

await store.setTransaction(tx)
}

const listAllTxs = await store.listTransactions()

const collected: Transaction[] = []

let page = await store.listTransactions({ limit: 5 })
collected.push(...page.transactions)
let timesCalled = 1
while (page.nextCursor !== null) {
timesCalled++
page = await store.listTransactions({
cursor: page.nextCursor,
limit: 5,
})
collected.push(...page.transactions)
}

expect(listAllTxs.transactions).toEqual(collected)
expect(timesCalled).toBe(2)
})

test('paginate list transactions if createdAt is null for some txs', async () => {
for (let i = 0; i < 10; i++) {
const date =
i % 2 > 0 ? new Date(`2026-01-02T00:00:00.000Z`) : undefined

const tx = addTx(`tx${i}`, date)

await store.setTransaction(tx)
}

const listAllTxs = await store.listTransactions()

const collected: Transaction[] = []

let page = await store.listTransactions({ limit: 5 })
collected.push(...page.transactions)
let timesCalled = 1

while (page.nextCursor !== null) {
timesCalled++
page = await store.listTransactions({
cursor: page.nextCursor,
limit: 5,
})
collected.push(...page.transactions)
}

expect(listAllTxs.transactions).toEqual(collected)
expect(timesCalled).toBe(2)
})

test('return total number of transactions', async () => {
for (let i = 0; i < 10; i++) {
const date =
i % 2 > 0
? new Date(`2026-01-02T00:00:00.000Z`)
: new Date(`2026-01-01T00:00:00.000Z`)

const tx = addTx(`tx${i}`, date)

await store.setTransaction(tx)
}

const count = await store.transactionsCount()
expect(count).toBe(10)
})
})
})
63 changes: 61 additions & 2 deletions core/wallet-store-inmemory/src/store-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
MessageRaw,
MessageRawStatusUpdate,
ApiKey,
ListTransactionsOptions,
} from '@canton-network/core-wallet-store'
import { CurrentNetworkWalletFilter } from '@canton-network/core-wallet-store'

Expand Down Expand Up @@ -454,11 +455,69 @@ export class StoreInternal implements Store, AuthAware<StoreInternal> {
})[0]
}

async listTransactions(): Promise<Array<Transaction>> {
async transactionsCount(): Promise<number> {
this.assertConnected()
const storage = this.getStorage()
return storage.transactions.size
}

async listTransactions(options?: ListTransactionsOptions) {
this.assertConnected()
const storage = this.getStorage()
const { cursor, limit } = options ?? {}
const lim = limit ? Math.min(limit, 100) : 100

const sortedTxs = Array.from(storage.transactions.values()).sort(
(a, b) => {
if (a.createdAt && b.createdAt) {
const diff =
new Date(b.createdAt).getTime() -
new Date(a.createdAt).getTime()
if (diff !== 0) return diff
} else if (a.createdAt && !b.createdAt) {
return -1
} else if (!a.createdAt && b.createdAt) {
return 1
}

return Array.from(storage.transactions.values()).sort(byCreatedAtDesc)
return b.id.localeCompare(a.id)
}
)

let startIndex = 0
if (cursor) {
const [cursorDate, cursorId] = cursor.split('::')
const index = sortedTxs.findIndex((tx) => {
const txDateStr = tx.createdAt
? new Date(tx.createdAt).toISOString()
: 'null'
return txDateStr === cursorDate && tx.id === cursorId
})
if (index !== -1) {
startIndex = index + 1
}
}

const pagedTxs = sortedTxs.slice(startIndex, startIndex + lim + 1)
const hasNextPage = pagedTxs.length > lim
if (hasNextPage) {
pagedTxs.pop()
}

if (pagedTxs.length === 0) {
return { transactions: [], nextCursor: null }
}

const lastTx = pagedTxs[pagedTxs.length - 1]
const d = lastTx.createdAt
? new Date(lastTx.createdAt).toISOString()
: 'null'
const nextCursor = hasNextPage ? `${d}::${lastTx.id}` : null

return {
transactions: pagedTxs,
nextCursor: nextCursor,
}
}

async removeTransaction(transactionId: string): Promise<void> {
Expand Down
Loading