From 55df2769939a9fb1e4eaaa6151048b05786f4727 Mon Sep 17 00:00:00 2001 From: rukmini-basu-da Date: Wed, 15 Jul 2026 13:09:52 -0400 Subject: [PATCH 01/23] pagination Signed-off-by: rukmini-basu-da --- core/wallet-store-sql/src/store-sql.ts | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/core/wallet-store-sql/src/store-sql.ts b/core/wallet-store-sql/src/store-sql.ts index f3f5da765..2fefc135c 100644 --- a/core/wallet-store-sql/src/store-sql.ts +++ b/core/wallet-store-sql/src/store-sql.ts @@ -737,6 +737,46 @@ export class StoreSql implements BaseStore, AuthAware { return transactions.map((table) => toTransaction(table)) } + async listTransactionsV2(cursor?: string, limit?: number) { + const userId = this.assertConnected() + const network = await this.getCurrentNetwork() + const lim = limit ? Math.min(limit, 100) : 100 + + let query = this.db + .selectFrom('transactions') + .selectAll() + .where((eb) => + eb.and([ + eb('userId', '=', userId), + eb('networkId', '=', network.id), + ]) + ) + .orderBy('createdAt', 'desc') + .orderBy('id', 'asc') + .limit(lim) + + if (cursor) { + const split = cursor.split('::') + query = query.where((eb) => + eb.and([ + eb('id', '>', split[1]), + eb('createdAt', '<', split[0]), + ]) + ) + } + + const rows = await query.execute() + const txs = rows.map((r) => toTransaction(r)) + const nextCursor = + rows.length === limit + ? `${txs[txs.length - 1].createdAt}::${txs[txs.length - 1].id}` + : null + return { + transactions: txs, + nextCursor: nextCursor, + } + } + async removeTransaction(transactionId: string): Promise { const userId = this.assertConnected() const network = await this.getCurrentNetwork() From bd063e1308ff15687784ed754b7e638f46d2a8fe Mon Sep 17 00:00:00 2001 From: rukmini-basu-da Date: Wed, 15 Jul 2026 14:59:58 -0400 Subject: [PATCH 02/23] test Signed-off-by: rukmini-basu-da --- core/wallet-store-sql/src/store-sql.test.ts | 48 +++++++++++++++++++++ core/wallet-store-sql/src/store-sql.ts | 21 +++++---- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/core/wallet-store-sql/src/store-sql.test.ts b/core/wallet-store-sql/src/store-sql.test.ts index 115a3d5e3..e6b671f49 100644 --- a/core/wallet-store-sql/src/store-sql.test.ts +++ b/core/wallet-store-sql/src/store-sql.test.ts @@ -77,6 +77,20 @@ const network: Network = { auth, } +function addTx(id: string, createdAt: Date) { + 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 db: Kysely @@ -625,6 +639,40 @@ implementations.forEach(([name, StoreImpl]) => { ).toHaveLength(2) }) + test('paginate list transactions', async () => { + const store = new StoreImpl(db, pino(sink()), authContextMock) + await store.addIdp(idp) + await store.addNetwork(network) + await store.setSession({ + id: 'session-tx-immutable', + network: 'network1', + accessToken: 'token', + }) + + 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.listTransactionsV2() + + const collected: Transaction[] = [] + let page = await store.listTransactionsV2(undefined, 5) + collected.push(...page.transactions) + while (page.nextCursor !== null) { + page = await store.listTransactionsV2(page.nextCursor, 6) + collected.push(...page.transactions) + } + + expect(listAllTxs.transactions).toEqual(collected) + }) + test('removeWallet should cascade-delete userPartyRights', async () => { const store = new StoreImpl(db, pino(sink()), authContextMock) await store.addIdp(idp) diff --git a/core/wallet-store-sql/src/store-sql.ts b/core/wallet-store-sql/src/store-sql.ts index 2fefc135c..7a71563b9 100644 --- a/core/wallet-store-sql/src/store-sql.ts +++ b/core/wallet-store-sql/src/store-sql.ts @@ -714,7 +714,6 @@ export class StoreSql implements BaseStore, AuthAware { ]) ) .orderBy('createdAt', 'desc') - .orderBy('id', 'desc') .executeTakeFirst() return transaction ? toTransaction(transaction) : undefined @@ -733,6 +732,7 @@ export class StoreSql implements BaseStore, AuthAware { ]) ) .orderBy('createdAt', 'desc') + .orderBy('id', 'desc') .execute() return transactions.map((table) => toTransaction(table)) } @@ -752,25 +752,30 @@ export class StoreSql implements BaseStore, AuthAware { ]) ) .orderBy('createdAt', 'desc') - .orderBy('id', 'asc') + .orderBy('id', 'desc') .limit(lim) if (cursor) { const split = cursor.split('::') + const dateString = split[0] query = query.where((eb) => - eb.and([ - eb('id', '>', split[1]), - eb('createdAt', '<', split[0]), + eb.or([ + eb('createdAt', '<', dateString), + eb.and([ + eb('createdAt', '=', dateString), + eb('id', '<', split[1]), + ]), ]) ) } const rows = await query.execute() const txs = rows.map((r) => toTransaction(r)) + const d = new Date(txs[txs.length - 1].createdAt!).toISOString() const nextCursor = - rows.length === limit - ? `${txs[txs.length - 1].createdAt}::${txs[txs.length - 1].id}` - : null + rows.length === limit ? `${d}::${txs[txs.length - 1].id}` : null + console.log('next cursor') + console.log(nextCursor) return { transactions: txs, nextCursor: nextCursor, From 5fcbafdbd8f1d29116aee5217e25905754423100 Mon Sep 17 00:00:00 2001 From: rukmini-basu-da Date: Fri, 17 Jul 2026 10:49:28 -0400 Subject: [PATCH 03/23] test Signed-off-by: rukmini-basu-da --- core/wallet-store-sql/src/store-sql.test.ts | 16 ++++++++++++---- core/wallet-store-sql/src/store-sql.ts | 10 +++++++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/core/wallet-store-sql/src/store-sql.test.ts b/core/wallet-store-sql/src/store-sql.test.ts index e6b671f49..3c7dd94ed 100644 --- a/core/wallet-store-sql/src/store-sql.test.ts +++ b/core/wallet-store-sql/src/store-sql.test.ts @@ -662,12 +662,20 @@ implementations.forEach(([name, StoreImpl]) => { const listAllTxs = await store.listTransactionsV2() + // console.log(listAllTxs) const collected: Transaction[] = [] - let page = await store.listTransactionsV2(undefined, 5) - collected.push(...page.transactions) - while (page.nextCursor !== null) { - page = await store.listTransactionsV2(page.nextCursor, 6) + + try { + let page = await store.listTransactionsV2(undefined, 5) collected.push(...page.transactions) + while (page.nextCursor !== null) { + page = await store.listTransactionsV2(page.nextCursor, 6) + console.log('page') + console.log(page) + collected.push(...page.transactions) + } + } catch (e) { + console.log(e) } expect(listAllTxs.transactions).toEqual(collected) diff --git a/core/wallet-store-sql/src/store-sql.ts b/core/wallet-store-sql/src/store-sql.ts index 7a71563b9..c78eb374d 100644 --- a/core/wallet-store-sql/src/store-sql.ts +++ b/core/wallet-store-sql/src/store-sql.ts @@ -771,7 +771,15 @@ export class StoreSql implements BaseStore, AuthAware { const rows = await query.execute() const txs = rows.map((r) => toTransaction(r)) - const d = new Date(txs[txs.length - 1].createdAt!).toISOString() + const lastCreatedAt = txs[txs.length - 1].createdAt + if (lastCreatedAt === undefined) { + return { + transactions: txs, + nextCursor: null, + } + } + + const d = new Date(lastCreatedAt).toISOString() const nextCursor = rows.length === limit ? `${d}::${txs[txs.length - 1].id}` : null console.log('next cursor') From 367c32718f078920dcb7f3468cd1568d29d372ac Mon Sep 17 00:00:00 2001 From: rukmini-basu-da Date: Fri, 17 Jul 2026 11:01:00 -0400 Subject: [PATCH 04/23] store sql test Signed-off-by: rukmini-basu-da --- core/wallet-store-sql/src/store-sql.test.ts | 18 ++++++-------- core/wallet-store-sql/src/store-sql.ts | 26 ++++++++++++++------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/core/wallet-store-sql/src/store-sql.test.ts b/core/wallet-store-sql/src/store-sql.test.ts index 3c7dd94ed..7b482582d 100644 --- a/core/wallet-store-sql/src/store-sql.test.ts +++ b/core/wallet-store-sql/src/store-sql.test.ts @@ -662,23 +662,19 @@ implementations.forEach(([name, StoreImpl]) => { const listAllTxs = await store.listTransactionsV2() - // console.log(listAllTxs) const collected: Transaction[] = [] - try { - let page = await store.listTransactionsV2(undefined, 5) + let page = await store.listTransactionsV2(undefined, 5) + collected.push(...page.transactions) + let timesCalled = 1 + while (page.nextCursor !== null) { + timesCalled++ + page = await store.listTransactionsV2(page.nextCursor, 5) collected.push(...page.transactions) - while (page.nextCursor !== null) { - page = await store.listTransactionsV2(page.nextCursor, 6) - console.log('page') - console.log(page) - collected.push(...page.transactions) - } - } catch (e) { - console.log(e) } expect(listAllTxs.transactions).toEqual(collected) + expect(timesCalled).toBe(2) }) test('removeWallet should cascade-delete userPartyRights', async () => { diff --git a/core/wallet-store-sql/src/store-sql.ts b/core/wallet-store-sql/src/store-sql.ts index c78eb374d..a172905f7 100644 --- a/core/wallet-store-sql/src/store-sql.ts +++ b/core/wallet-store-sql/src/store-sql.ts @@ -753,7 +753,7 @@ export class StoreSql implements BaseStore, AuthAware { ) .orderBy('createdAt', 'desc') .orderBy('id', 'desc') - .limit(lim) + .limit(lim + 1) if (cursor) { const split = cursor.split('::') @@ -771,19 +771,29 @@ export class StoreSql implements BaseStore, AuthAware { const rows = await query.execute() const txs = rows.map((r) => toTransaction(r)) - const lastCreatedAt = txs[txs.length - 1].createdAt - if (lastCreatedAt === undefined) { + + const hasNextPage = txs.length > lim + if (hasNextPage) { + txs.pop() + } + + if (txs.length === 0) { + return { + transactions: [], + nextCursor: null, + } + } + + const lastTx = txs[txs.length - 1] + if (lastTx === undefined) { return { transactions: txs, nextCursor: null, } } - const d = new Date(lastCreatedAt).toISOString() - const nextCursor = - rows.length === limit ? `${d}::${txs[txs.length - 1].id}` : null - console.log('next cursor') - console.log(nextCursor) + const d = new Date(lastTx.createdAt!).toISOString() + const nextCursor = hasNextPage ? `${d}::${lastTx.id}` : null return { transactions: txs, nextCursor: nextCursor, From 9ad2f53fd691fd13ffea1113a3f888f7c140a877 Mon Sep 17 00:00:00 2001 From: rukmini-basu-da Date: Fri, 17 Jul 2026 12:19:27 -0400 Subject: [PATCH 05/23] test Signed-off-by: rukmini-basu-da --- core/wallet-store-sql/src/store-sql.test.ts | 45 ++++++++++++++++++++- core/wallet-store-sql/src/store-sql.ts | 37 ++++++++++++----- 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/core/wallet-store-sql/src/store-sql.test.ts b/core/wallet-store-sql/src/store-sql.test.ts index 7b482582d..8bb1ba7ac 100644 --- a/core/wallet-store-sql/src/store-sql.test.ts +++ b/core/wallet-store-sql/src/store-sql.test.ts @@ -77,7 +77,7 @@ const network: Network = { auth, } -function addTx(id: string, createdAt: Date) { +function addTx(id: string, createdAt: Date | undefined) { const initial: Transaction = { id: id, commandId: 'cmd-immutable', @@ -677,6 +677,49 @@ implementations.forEach(([name, StoreImpl]) => { expect(timesCalled).toBe(2) }) + test('paginate list transactions if createdAt is null for some txs', async () => { + const store = new StoreImpl(db, pino(sink()), authContextMock) + await store.addIdp(idp) + await store.addNetwork(network) + await store.setSession({ + id: 'session-tx-immutable', + network: 'network1', + accessToken: 'token', + }) + + 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.listTransactionsV2() + console.log('list all txs') + console.log(listAllTxs) + + const collected: Transaction[] = [] + + let page = await store.listTransactionsV2(undefined, 5) + collected.push(...page.transactions) + let timesCalled = 1 + + console.log(`page ${timesCalled}`) + console.log(page) + while (page.nextCursor !== null) { + timesCalled++ + page = await store.listTransactionsV2(page.nextCursor, 5) + collected.push(...page.transactions) + console.log(`page ${timesCalled}`) + console.log(page) + } + + // expect(listAllTxs.transactions).toEqual(collected) + // expect(timesCalled).toBe(2) + }) + test('removeWallet should cascade-delete userPartyRights', async () => { const store = new StoreImpl(db, pino(sink()), authContextMock) await store.addIdp(idp) diff --git a/core/wallet-store-sql/src/store-sql.ts b/core/wallet-store-sql/src/store-sql.ts index a172905f7..79f05fe47 100644 --- a/core/wallet-store-sql/src/store-sql.ts +++ b/core/wallet-store-sql/src/store-sql.ts @@ -757,16 +757,26 @@ export class StoreSql implements BaseStore, AuthAware { if (cursor) { const split = cursor.split('::') - const dateString = split[0] - query = query.where((eb) => - eb.or([ + const dateString = split[0] === 'null' ? null : split[0] + const cursorId = split[1] + + query = query.where((eb) => { + if (dateString === null) { + return eb.and([ + eb('createdAt', 'is', null), + eb('id', '<', cursorId), + ]) + } + + return eb.or([ eb('createdAt', '<', dateString), eb.and([ eb('createdAt', '=', dateString), eb('id', '<', split[1]), ]), + eb('createdAt', 'is', null), ]) - ) + }) } const rows = await query.execute() @@ -792,11 +802,20 @@ export class StoreSql implements BaseStore, AuthAware { } } - const d = new Date(lastTx.createdAt!).toISOString() - const nextCursor = hasNextPage ? `${d}::${lastTx.id}` : null - return { - transactions: txs, - nextCursor: nextCursor, + //handle case for if timestamp is null + if (lastTx.createdAt) { + const d = new Date(lastTx.createdAt).toISOString() + const nextCursor = hasNextPage ? `${d}::${lastTx.id}` : null + return { + transactions: txs, + nextCursor: nextCursor, + } + } else { + const nextCursor = hasNextPage ? `null::${lastTx.id}` : null + return { + transactions: txs, + nextCursor: nextCursor, + } } } From bbe1b204f9e5d1e4e5505101227ce879c8d18818 Mon Sep 17 00:00:00 2001 From: rukmini-basu-da Date: Fri, 17 Jul 2026 12:50:44 -0400 Subject: [PATCH 06/23] test Signed-off-by: rukmini-basu-da --- core/wallet-store-sql/src/store-sql.test.ts | 6 ++---- core/wallet-store-sql/src/store-sql.ts | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/core/wallet-store-sql/src/store-sql.test.ts b/core/wallet-store-sql/src/store-sql.test.ts index 8bb1ba7ac..b3a2c23d0 100644 --- a/core/wallet-store-sql/src/store-sql.test.ts +++ b/core/wallet-store-sql/src/store-sql.test.ts @@ -697,8 +697,6 @@ implementations.forEach(([name, StoreImpl]) => { } const listAllTxs = await store.listTransactionsV2() - console.log('list all txs') - console.log(listAllTxs) const collected: Transaction[] = [] @@ -716,8 +714,8 @@ implementations.forEach(([name, StoreImpl]) => { console.log(page) } - // expect(listAllTxs.transactions).toEqual(collected) - // expect(timesCalled).toBe(2) + expect(listAllTxs.transactions).toEqual(collected) + expect(timesCalled).toBe(2) }) test('removeWallet should cascade-delete userPartyRights', async () => { diff --git a/core/wallet-store-sql/src/store-sql.ts b/core/wallet-store-sql/src/store-sql.ts index 79f05fe47..82436ab9a 100644 --- a/core/wallet-store-sql/src/store-sql.ts +++ b/core/wallet-store-sql/src/store-sql.ts @@ -49,6 +49,7 @@ import { toSession, } from './schema.js' import pg from 'pg' +import { sql } from 'kysely' export class StoreSql implements BaseStore, AuthAware { authContext: AuthContext | undefined @@ -751,7 +752,7 @@ export class StoreSql implements BaseStore, AuthAware { eb('networkId', '=', network.id), ]) ) - .orderBy('createdAt', 'desc') + .orderBy(sql`${sql.ref('createdAt')} desc nulls last`) .orderBy('id', 'desc') .limit(lim + 1) From 549ab1c0a4e714828dce5c61265dc8ce73543e84 Mon Sep 17 00:00:00 2001 From: rukmini-basu-da Date: Fri, 17 Jul 2026 14:18:15 -0400 Subject: [PATCH 07/23] fix types Signed-off-by: rukmini-basu-da --- .../src/store-internal.test.ts | 6 +++-- .../src/store-internal.ts | 10 +++++++-- core/wallet-store-sql/src/store-sql.test.ts | 22 +++++++++++-------- core/wallet-store-sql/src/store-sql.ts | 20 +---------------- core/wallet-store/src/Store.ts | 5 ++++- 5 files changed, 30 insertions(+), 33 deletions(-) diff --git a/core/wallet-store-inmemory/src/store-internal.test.ts b/core/wallet-store-inmemory/src/store-internal.test.ts index bb77189f7..d3477660b 100644 --- a/core/wallet-store-inmemory/src/store-internal.test.ts +++ b/core/wallet-store-inmemory/src/store-internal.test.ts @@ -663,7 +663,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) @@ -767,7 +767,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 () => { diff --git a/core/wallet-store-inmemory/src/store-internal.ts b/core/wallet-store-inmemory/src/store-internal.ts index db144520e..5af432c2e 100644 --- a/core/wallet-store-inmemory/src/store-internal.ts +++ b/core/wallet-store-inmemory/src/store-internal.ts @@ -454,11 +454,17 @@ export class StoreInternal implements Store, AuthAware { })[0] } - async listTransactions(): Promise> { + async listTransactions() { this.assertConnected() const storage = this.getStorage() - return Array.from(storage.transactions.values()).sort(byCreatedAtDesc) + //does this need the same pagination logic? + return { + transactions: Array.from(storage.transactions.values()).sort( + byCreatedAtDesc + ), + nextCursor: null, + } } async removeTransaction(transactionId: string): Promise { diff --git a/core/wallet-store-sql/src/store-sql.test.ts b/core/wallet-store-sql/src/store-sql.test.ts index b3a2c23d0..07840a962 100644 --- a/core/wallet-store-sql/src/store-sql.test.ts +++ b/core/wallet-store-sql/src/store-sql.test.ts @@ -633,7 +633,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) @@ -660,16 +660,16 @@ implementations.forEach(([name, StoreImpl]) => { await store.setTransaction(tx) } - const listAllTxs = await store.listTransactionsV2() + const listAllTxs = await store.listTransactions() const collected: Transaction[] = [] - let page = await store.listTransactionsV2(undefined, 5) + let page = await store.listTransactions(undefined, 5) collected.push(...page.transactions) let timesCalled = 1 while (page.nextCursor !== null) { timesCalled++ - page = await store.listTransactionsV2(page.nextCursor, 5) + page = await store.listTransactions(page.nextCursor, 5) collected.push(...page.transactions) } @@ -696,11 +696,11 @@ implementations.forEach(([name, StoreImpl]) => { await store.setTransaction(tx) } - const listAllTxs = await store.listTransactionsV2() + const listAllTxs = await store.listTransactions() const collected: Transaction[] = [] - let page = await store.listTransactionsV2(undefined, 5) + let page = await store.listTransactions(undefined, 5) collected.push(...page.transactions) let timesCalled = 1 @@ -708,7 +708,7 @@ implementations.forEach(([name, StoreImpl]) => { console.log(page) while (page.nextCursor !== null) { timesCalled++ - page = await store.listTransactionsV2(page.nextCursor, 5) + page = await store.listTransactions(page.nextCursor, 5) collected.push(...page.transactions) console.log(`page ${timesCalled}`) console.log(page) @@ -790,7 +790,9 @@ implementations.forEach(([name, StoreImpl]) => { 'network1', ]) expect(await store.getWallets()).toHaveLength(1) - expect(await store.listTransactions()).toHaveLength(1) + expect((await store.listTransactions()).transactions).toHaveLength( + 1 + ) await store.removeNetwork('network1') @@ -951,7 +953,9 @@ implementations.forEach(([name, StoreImpl]) => { await store.removeTransaction('tx-old') expect(await store.getTransaction('tx-old')).toBeUndefined() - expect(await store.listTransactions()).toHaveLength(1) + expect((await store.listTransactions()).transactions).toHaveLength( + 1 + ) }) test('should throw when updating a missing transaction or message', async () => { diff --git a/core/wallet-store-sql/src/store-sql.ts b/core/wallet-store-sql/src/store-sql.ts index 82436ab9a..967039eef 100644 --- a/core/wallet-store-sql/src/store-sql.ts +++ b/core/wallet-store-sql/src/store-sql.ts @@ -720,25 +720,7 @@ export class StoreSql implements BaseStore, AuthAware { return transaction ? toTransaction(transaction) : undefined } - async listTransactions(): Promise> { - const userId = this.assertConnected() - const network = await this.getCurrentNetwork() - const transactions = await this.db - .selectFrom('transactions') - .selectAll() - .where((eb) => - eb.and([ - eb('userId', '=', userId), - eb('networkId', '=', network.id), - ]) - ) - .orderBy('createdAt', 'desc') - .orderBy('id', 'desc') - .execute() - return transactions.map((table) => toTransaction(table)) - } - - async listTransactionsV2(cursor?: string, limit?: number) { + async listTransactions(cursor?: string, limit?: number) { const userId = this.assertConnected() const network = await this.getCurrentNetwork() const lim = limit ? Math.min(limit, 100) : 100 diff --git a/core/wallet-store/src/Store.ts b/core/wallet-store/src/Store.ts index 03cb9a05d..ba699bc88 100644 --- a/core/wallet-store/src/Store.ts +++ b/core/wallet-store/src/Store.ts @@ -183,7 +183,10 @@ export interface Store { getLatestTransactionByCommandId( commandId: string ): Promise - listTransactions(): Promise> + listTransactions( + cursor?: string, + limit?: number + ): Promise<{ transactions: Array; nextCursor: string | null }> listAllPendingTransactions(): Promise> removeTransaction(transactionId: string): Promise From 81c0cbdad58adfe585347019726bddb4597d31ec Mon Sep 17 00:00:00 2001 From: rukmini-basu-da Date: Fri, 17 Jul 2026 14:50:28 -0400 Subject: [PATCH 08/23] update user api controller Signed-off-by: rukmini-basu-da --- wallet-gateway/remote/src/user-api/controller.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/wallet-gateway/remote/src/user-api/controller.ts b/wallet-gateway/remote/src/user-api/controller.ts index ccdb95b4e..590e78283 100644 --- a/wallet-gateway/remote/src/user-api/controller.ts +++ b/wallet-gateway/remote/src/user-api/controller.ts @@ -45,7 +45,7 @@ import { ListSigningProviderVaultsResult, ListSigningProviderVaultsParams, } from './rpc-gen/typings.js' -import { Store, Network } from '@canton-network/core-wallet-store' +import { Store, Network, Transaction } from '@canton-network/core-wallet-store' import { Logger } from 'pino' import { NotificationService } from '../notification/NotificationService.js' import { @@ -1003,7 +1003,13 @@ export const userController = ( } }, listTransactions: async function (): Promise { - const transactions = await store.listTransactions() + const transactions: Transaction[] = [] + let page = await store.listTransactions() + transactions.push(...page.transactions) + while (page.nextCursor !== null) { + page = await store.listTransactions(page.nextCursor, 5) + transactions.push(...page.transactions) + } const txs = transactions.map((transaction) => ({ id: transaction.id, commandId: transaction.commandId, From 0c3e36dd41b1b32c61a935b3a0f1aaf64f9830ab Mon Sep 17 00:00:00 2001 From: rukmini-basu-da Date: Fri, 17 Jul 2026 15:13:27 -0400 Subject: [PATCH 09/23] test Signed-off-by: rukmini-basu-da --- api-specs/openrpc-user-api.json | 28 +++++++++++++++- core/wallet-user-rpc-client/src/index.ts | 33 ++++++++++++++++++- core/wallet-user-rpc-client/src/openrpc.json | 28 +++++++++++++++- .../remote/src/user-api/controller.test.ts | 2 +- .../remote/src/user-api/controller.ts | 24 +++++++++----- .../remote/src/user-api/rpc-gen/typings.ts | 33 ++++++++++++++++++- 6 files changed, 134 insertions(+), 14 deletions(-) diff --git a/api-specs/openrpc-user-api.json b/api-specs/openrpc-user-api.json index 06da0222f..d8364f20b 100644 --- a/api-specs/openrpc-user-api.json +++ b/api-specs/openrpc-user-api.json @@ -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": { @@ -890,6 +911,11 @@ "items": { "$ref": "#/components/schemas/Transaction" } + }, + "nextCursor": { + "title": "nextCursor", + "type": "string", + "description": "Cursor for next page of results." } }, "required": ["transactions"] diff --git a/core/wallet-user-rpc-client/src/index.ts b/core/wallet-user-rpc-client/src/index.ts index f59eb3354..6e50a39bc 100644 --- a/core/wallet-user-rpc-client/src/index.ts +++ b/core/wallet-user-rpc-client/src/index.ts @@ -181,6 +181,24 @@ export type MessageId = string */ export type Signature = string export type SignedBy = string +/** + * + * Limit of transactions to return. + * + */ +export type Limit = number +/** + * + * Cursor for next page of results. + * + */ +export type CursorAsString = string +/** + * + * Cursor for next page of results. + * + */ +export type Cursor = CursorAsString /** * * Authentication method configured for this network @@ -433,6 +451,12 @@ export interface Transaction { externalTxId?: ExternalTxId } export type Transactions = Transaction[] +/** + * + * Cursor for next page of results. + * + */ +export type NextCursor = string /** * * The unique identifier of the current user. @@ -531,6 +555,10 @@ export interface AddSessionParams { export interface GetTransactionParams { transactionId: TransactionId } +export interface ListTransactionsParams { + limit?: Limit + cursor?: Cursor +} export interface DeleteTransactionParams { transactionId: TransactionId } @@ -635,6 +663,7 @@ export interface GetTransactionResult { } export interface ListTransactionsResult { transactions: Transactions + nextCursor?: NextCursor } export interface GetUserResult { userId: UserIdentifier @@ -699,7 +728,9 @@ export type ListSessions = () => Promise export type GetTransaction = ( params: GetTransactionParams ) => Promise -export type ListTransactions = () => Promise +export type ListTransactions = ( + params: ListTransactionsParams +) => Promise export type DeleteTransaction = ( params: DeleteTransactionParams ) => Promise diff --git a/core/wallet-user-rpc-client/src/openrpc.json b/core/wallet-user-rpc-client/src/openrpc.json index 06da0222f..d8364f20b 100644 --- a/core/wallet-user-rpc-client/src/openrpc.json +++ b/core/wallet-user-rpc-client/src/openrpc.json @@ -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": { @@ -890,6 +911,11 @@ "items": { "$ref": "#/components/schemas/Transaction" } + }, + "nextCursor": { + "title": "nextCursor", + "type": "string", + "description": "Cursor for next page of results." } }, "required": ["transactions"] diff --git a/wallet-gateway/remote/src/user-api/controller.test.ts b/wallet-gateway/remote/src/user-api/controller.test.ts index 3d02f2b06..29b151cad 100644 --- a/wallet-gateway/remote/src/user-api/controller.test.ts +++ b/wallet-gateway/remote/src/user-api/controller.test.ts @@ -752,7 +752,7 @@ describe('userController', () => { auth ) - const result = await controller.listTransactions() + const result = await controller.listTransactions({}) expect(result.transactions).toHaveLength(1) expect(result.transactions[0]?.id).toBe('tx-1') }) diff --git a/wallet-gateway/remote/src/user-api/controller.ts b/wallet-gateway/remote/src/user-api/controller.ts index 590e78283..7ed44658a 100644 --- a/wallet-gateway/remote/src/user-api/controller.ts +++ b/wallet-gateway/remote/src/user-api/controller.ts @@ -44,6 +44,7 @@ import { RemoveApiKeyParams, ListSigningProviderVaultsResult, ListSigningProviderVaultsParams, + ListTransactionsParams, } from './rpc-gen/typings.js' import { Store, Network, Transaction } from '@canton-network/core-wallet-store' import { Logger } from 'pino' @@ -1002,14 +1003,14 @@ export const userController = ( }), } }, - listTransactions: async function (): Promise { - const transactions: Transaction[] = [] - let page = await store.listTransactions() - transactions.push(...page.transactions) - while (page.nextCursor !== null) { - page = await store.listTransactions(page.nextCursor, 5) - transactions.push(...page.transactions) - } + listTransactions: async function ( + params: ListTransactionsParams + ): Promise { + const page = await store.listTransactions( + params.cursor, + params.limit + ) + const transactions = page.transactions const txs = transactions.map((transaction) => ({ id: transaction.id, commandId: transaction.commandId, @@ -1032,7 +1033,12 @@ export const userController = ( externalTxId: transaction.externalTxId, }), })) - return { transactions: txs } + + if (page.nextCursor === null) { + return { transactions: txs } + } else { + return { transactions: txs, nextCursor: page.nextCursor } + } }, deleteTransaction: async ( params: DeleteTransactionParams diff --git a/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts b/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts index c808e0b3e..144616122 100644 --- a/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts +++ b/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts @@ -180,6 +180,24 @@ export type MessageId = string */ export type Signature = string export type SignedBy = string +/** + * + * Limit of transactions to return. + * + */ +export type Limit = number +/** + * + * Cursor for next page of results. + * + */ +export type CursorAsString = string +/** + * + * Cursor for next page of results. + * + */ +export type Cursor = CursorAsString /** * * Authentication method configured for this network @@ -432,6 +450,12 @@ export interface Transaction { externalTxId?: ExternalTxId } export type Transactions = Transaction[] +/** + * + * Cursor for next page of results. + * + */ +export type NextCursor = string /** * * The unique identifier of the current user. @@ -530,6 +554,10 @@ export interface AddSessionParams { export interface GetTransactionParams { transactionId: TransactionId } +export interface ListTransactionsParams { + limit?: Limit + cursor?: Cursor +} export interface DeleteTransactionParams { transactionId: TransactionId } @@ -634,6 +662,7 @@ export interface GetTransactionResult { } export interface ListTransactionsResult { transactions: Transactions + nextCursor?: NextCursor } export interface GetUserResult { userId: UserIdentifier @@ -698,7 +727,9 @@ export type ListSessions = () => Promise export type GetTransaction = ( params: GetTransactionParams ) => Promise -export type ListTransactions = () => Promise +export type ListTransactions = ( + params: ListTransactionsParams +) => Promise export type DeleteTransaction = ( params: DeleteTransactionParams ) => Promise From 91eadee9246e41411d811dc1102dcbaf46ef73a2 Mon Sep 17 00:00:00 2001 From: rukmini-basu-da Date: Fri, 17 Jul 2026 15:27:27 -0400 Subject: [PATCH 10/23] test Signed-off-by: rukmini-basu-da --- wallet-gateway/remote/src/web/frontend/activities/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/wallet-gateway/remote/src/web/frontend/activities/index.ts b/wallet-gateway/remote/src/web/frontend/activities/index.ts index 9e2dfaa46..bf783b17d 100644 --- a/wallet-gateway/remote/src/web/frontend/activities/index.ts +++ b/wallet-gateway/remote/src/web/frontend/activities/index.ts @@ -152,6 +152,7 @@ export class UserUiActivities extends BaseElement { ) const result = await userClient.request({ method: 'listTransactions', + params: {}, }) this.transactions = result.transactions || [] this.parsedTransactions = new Map( From a601b0348d0af45d02cad940eb2be8e562bea38c Mon Sep 17 00:00:00 2001 From: rukmini-basu-da Date: Mon, 20 Jul 2026 11:10:33 -0400 Subject: [PATCH 11/23] add pagination to store internal Signed-off-by: rukmini-basu-da --- .../src/store-internal.test.ts | 70 +++++++++++++++++++ .../src/store-internal.ts | 57 +++++++++++++-- core/wallet-store-sql/src/store-sql.test.ts | 4 -- 3 files changed, 121 insertions(+), 10 deletions(-) diff --git a/core/wallet-store-inmemory/src/store-internal.test.ts b/core/wallet-store-inmemory/src/store-internal.test.ts index d3477660b..8d9e605f0 100644 --- a/core/wallet-store-inmemory/src/store-internal.test.ts +++ b/core/wallet-store-inmemory/src/store-internal.test.ts @@ -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 @@ -872,5 +885,62 @@ 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(undefined, 5) + collected.push(...page.transactions) + let timesCalled = 1 + while (page.nextCursor !== null) { + timesCalled++ + page = await store.listTransactions(page.nextCursor, 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(undefined, 5) + collected.push(...page.transactions) + let timesCalled = 1 + + while (page.nextCursor !== null) { + timesCalled++ + page = await store.listTransactions(page.nextCursor, 5) + collected.push(...page.transactions) + } + + expect(listAllTxs.transactions).toEqual(collected) + expect(timesCalled).toBe(2) + }) }) }) diff --git a/core/wallet-store-inmemory/src/store-internal.ts b/core/wallet-store-inmemory/src/store-internal.ts index 5af432c2e..fa54e2584 100644 --- a/core/wallet-store-inmemory/src/store-internal.ts +++ b/core/wallet-store-inmemory/src/store-internal.ts @@ -454,16 +454,61 @@ export class StoreInternal implements Store, AuthAware { })[0] } - async listTransactions() { + async listTransactions(cursor?: string, limit?: number) { this.assertConnected() const storage = this.getStorage() + 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 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 - //does this need the same pagination logic? return { - transactions: Array.from(storage.transactions.values()).sort( - byCreatedAtDesc - ), - nextCursor: null, + transactions: pagedTxs, + nextCursor: nextCursor, } } diff --git a/core/wallet-store-sql/src/store-sql.test.ts b/core/wallet-store-sql/src/store-sql.test.ts index 07840a962..f321643a8 100644 --- a/core/wallet-store-sql/src/store-sql.test.ts +++ b/core/wallet-store-sql/src/store-sql.test.ts @@ -704,14 +704,10 @@ implementations.forEach(([name, StoreImpl]) => { collected.push(...page.transactions) let timesCalled = 1 - console.log(`page ${timesCalled}`) - console.log(page) while (page.nextCursor !== null) { timesCalled++ page = await store.listTransactions(page.nextCursor, 5) collected.push(...page.transactions) - console.log(`page ${timesCalled}`) - console.log(page) } expect(listAllTxs.transactions).toEqual(collected) From df18f5b193cdedb5dc30b6e22eadd6c99e268aee Mon Sep 17 00:00:00 2001 From: rukmini-basu-da Date: Mon, 20 Jul 2026 11:46:56 -0400 Subject: [PATCH 12/23] test Signed-off-by: rukmini-basu-da --- .../src/store-internal.test.ts | 14 ++++++++++---- core/wallet-store-inmemory/src/store-internal.ts | 4 +++- core/wallet-store-sql/src/store-sql.test.ts | 14 ++++++++++---- core/wallet-store-sql/src/store-sql.ts | 4 +++- core/wallet-store/src/Store.ts | 8 ++++++-- wallet-gateway/remote/src/user-api/controller.ts | 9 +++------ 6 files changed, 35 insertions(+), 18 deletions(-) diff --git a/core/wallet-store-inmemory/src/store-internal.test.ts b/core/wallet-store-inmemory/src/store-internal.test.ts index 8d9e605f0..332556e2a 100644 --- a/core/wallet-store-inmemory/src/store-internal.test.ts +++ b/core/wallet-store-inmemory/src/store-internal.test.ts @@ -902,12 +902,15 @@ implementations.forEach(([name, StoreImpl]) => { const collected: Transaction[] = [] - let page = await store.listTransactions(undefined, 5) + let page = await store.listTransactions({ limit: 5 }) collected.push(...page.transactions) let timesCalled = 1 while (page.nextCursor !== null) { timesCalled++ - page = await store.listTransactions(page.nextCursor, 5) + page = await store.listTransactions({ + cursor: page.nextCursor, + limit: 5, + }) collected.push(...page.transactions) } @@ -929,13 +932,16 @@ implementations.forEach(([name, StoreImpl]) => { const collected: Transaction[] = [] - let page = await store.listTransactions(undefined, 5) + let page = await store.listTransactions({ limit: 5 }) collected.push(...page.transactions) let timesCalled = 1 while (page.nextCursor !== null) { timesCalled++ - page = await store.listTransactions(page.nextCursor, 5) + page = await store.listTransactions({ + cursor: page.nextCursor, + limit: 5, + }) collected.push(...page.transactions) } diff --git a/core/wallet-store-inmemory/src/store-internal.ts b/core/wallet-store-inmemory/src/store-internal.ts index fa54e2584..42b236d7c 100644 --- a/core/wallet-store-inmemory/src/store-internal.ts +++ b/core/wallet-store-inmemory/src/store-internal.ts @@ -23,6 +23,7 @@ import { MessageRaw, MessageRawStatusUpdate, ApiKey, + ListTransactionsOptions, } from '@canton-network/core-wallet-store' import { CurrentNetworkWalletFilter } from '@canton-network/core-wallet-store' @@ -454,9 +455,10 @@ export class StoreInternal implements Store, AuthAware { })[0] } - async listTransactions(cursor?: string, limit?: number) { + 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( diff --git a/core/wallet-store-sql/src/store-sql.test.ts b/core/wallet-store-sql/src/store-sql.test.ts index f321643a8..271b758d4 100644 --- a/core/wallet-store-sql/src/store-sql.test.ts +++ b/core/wallet-store-sql/src/store-sql.test.ts @@ -664,12 +664,15 @@ implementations.forEach(([name, StoreImpl]) => { const collected: Transaction[] = [] - let page = await store.listTransactions(undefined, 5) + let page = await store.listTransactions({ limit: 5 }) collected.push(...page.transactions) let timesCalled = 1 while (page.nextCursor !== null) { timesCalled++ - page = await store.listTransactions(page.nextCursor, 5) + page = await store.listTransactions({ + cursor: page.nextCursor, + limit: 5, + }) collected.push(...page.transactions) } @@ -700,13 +703,16 @@ implementations.forEach(([name, StoreImpl]) => { const collected: Transaction[] = [] - let page = await store.listTransactions(undefined, 5) + let page = await store.listTransactions({ limit: 5 }) collected.push(...page.transactions) let timesCalled = 1 while (page.nextCursor !== null) { timesCalled++ - page = await store.listTransactions(page.nextCursor, 5) + page = await store.listTransactions({ + cursor: page.nextCursor, + limit: 5, + }) collected.push(...page.transactions) } diff --git a/core/wallet-store-sql/src/store-sql.ts b/core/wallet-store-sql/src/store-sql.ts index 967039eef..8a6deeb62 100644 --- a/core/wallet-store-sql/src/store-sql.ts +++ b/core/wallet-store-sql/src/store-sql.ts @@ -26,6 +26,7 @@ import { TransactionStatusUpdate, UserLevelRight, ApiKey, + ListTransactionsOptions, } from '@canton-network/core-wallet-store' import { CamelCasePlugin, Kysely, PostgresDialect, SqliteDialect } from 'kysely' import Database from 'better-sqlite3' @@ -720,9 +721,10 @@ export class StoreSql implements BaseStore, AuthAware { return transaction ? toTransaction(transaction) : undefined } - async listTransactions(cursor?: string, limit?: number) { + async listTransactions(options?: ListTransactionsOptions) { const userId = this.assertConnected() const network = await this.getCurrentNetwork() + const { cursor, limit } = options ?? {} const lim = limit ? Math.min(limit, 100) : 100 let query = this.db diff --git a/core/wallet-store/src/Store.ts b/core/wallet-store/src/Store.ts index ba699bc88..1de38c687 100644 --- a/core/wallet-store/src/Store.ts +++ b/core/wallet-store/src/Store.ts @@ -100,6 +100,11 @@ export interface TransactionStatusUpdate { externalTxId?: string } +export interface ListTransactionsOptions { + cursor?: string + limit?: number +} + export interface MessageRaw { id: string status: 'pending' | 'signed' | 'failed' @@ -184,8 +189,7 @@ export interface Store { commandId: string ): Promise listTransactions( - cursor?: string, - limit?: number + options?: ListTransactionsOptions ): Promise<{ transactions: Array; nextCursor: string | null }> listAllPendingTransactions(): Promise> removeTransaction(transactionId: string): Promise diff --git a/wallet-gateway/remote/src/user-api/controller.ts b/wallet-gateway/remote/src/user-api/controller.ts index 7ed44658a..1c296eaa7 100644 --- a/wallet-gateway/remote/src/user-api/controller.ts +++ b/wallet-gateway/remote/src/user-api/controller.ts @@ -46,7 +46,7 @@ import { ListSigningProviderVaultsParams, ListTransactionsParams, } from './rpc-gen/typings.js' -import { Store, Network, Transaction } from '@canton-network/core-wallet-store' +import { Store, Network } from '@canton-network/core-wallet-store' import { Logger } from 'pino' import { NotificationService } from '../notification/NotificationService.js' import { @@ -1004,12 +1004,9 @@ export const userController = ( } }, listTransactions: async function ( - params: ListTransactionsParams + params?: ListTransactionsParams ): Promise { - const page = await store.listTransactions( - params.cursor, - params.limit - ) + const page = await store.listTransactions(params) const transactions = page.transactions const txs = transactions.map((transaction) => ({ id: transaction.id, From f132e7252723b8b412ad91d3e110058fd5c50855 Mon Sep 17 00:00:00 2001 From: rukmini-basu-da Date: Mon, 20 Jul 2026 15:10:54 -0400 Subject: [PATCH 13/23] use pagination in activities Signed-off-by: rukmini-basu-da --- .../src/web/frontend/activities/index.ts | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/wallet-gateway/remote/src/web/frontend/activities/index.ts b/wallet-gateway/remote/src/web/frontend/activities/index.ts index bf783b17d..06bb27df5 100644 --- a/wallet-gateway/remote/src/web/frontend/activities/index.ts +++ b/wallet-gateway/remote/src/web/frontend/activities/index.ts @@ -38,6 +38,11 @@ export class UserUiActivities extends BaseElement { @state() accessor currentPage = 1 + @state() + accessor hasNextPage = false + + private pageCursors: (string | undefined)[] = [undefined] + private readonly pageSize = 4 static styles = [ @@ -67,9 +72,11 @@ export class UserUiActivities extends BaseElement { `, ] - private get pagedTransactions() { - const start = (this.currentPage - 1) * this.pageSize - return this.transactions.slice(start, start + this.pageSize) + private get computedTotal() { + if (this.hasNextPage) { + return this.pageSize * this.pageSize * 1 + } + return (this.currentPage - 1) * this.pageSize + this.transactions.length } protected render() { @@ -86,7 +93,7 @@ export class UserUiActivities extends BaseElement { : this.transactions.length ? html`
- ${this.pagedTransactions.map( + ${this.transactions.map( (tx) => html` ${ - this.transactions.length > this.pageSize + this.hasNextPage || this.currentPage > 1 ? html`
{ try { From 63ce4517b492a9b3d6a2d8a8ba80eeb5cb1e7188 Mon Sep 17 00:00:00 2001 From: rukmini-basu-da Date: Mon, 20 Jul 2026 15:13:00 -0400 Subject: [PATCH 14/23] refactor Signed-off-by: rukmini-basu-da --- .../remote/src/web/frontend/activities/index.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/wallet-gateway/remote/src/web/frontend/activities/index.ts b/wallet-gateway/remote/src/web/frontend/activities/index.ts index 06bb27df5..5a0d70739 100644 --- a/wallet-gateway/remote/src/web/frontend/activities/index.ts +++ b/wallet-gateway/remote/src/web/frontend/activities/index.ts @@ -72,7 +72,7 @@ export class UserUiActivities extends BaseElement { `, ] - private get computedTotal() { + private get _computedTotal() { if (this.hasNextPage) { return this.pageSize * this.pageSize * 1 } @@ -160,14 +160,14 @@ export class UserUiActivities extends BaseElement { const currentCursor = this.pageCursors[this.currentPage - 1] - const params = - currentCursor !== undefined - ? { limit: this.pageSize, cursor: currentCursor } - : { limit: this.pageSize } - const result = await userClient.request({ method: 'listTransactions', - params, + params: { + limit: this.pageSize, + ...(currentCursor !== undefined + ? { cursor: currentCursor } + : {}), + }, }) this.transactions = result.transactions || [] this.hasNextPage = !!result.nextCursor From 52461a4d6665c32355390e32d36cd1e5c00ca6ae Mon Sep 17 00:00:00 2001 From: rukmini-basu-da Date: Mon, 20 Jul 2026 15:16:00 -0400 Subject: [PATCH 15/23] refactor Signed-off-by: rukmini-basu-da --- wallet-gateway/remote/src/web/frontend/activities/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wallet-gateway/remote/src/web/frontend/activities/index.ts b/wallet-gateway/remote/src/web/frontend/activities/index.ts index 5a0d70739..86bca46fa 100644 --- a/wallet-gateway/remote/src/web/frontend/activities/index.ts +++ b/wallet-gateway/remote/src/web/frontend/activities/index.ts @@ -72,7 +72,7 @@ export class UserUiActivities extends BaseElement { `, ] - private get _computedTotal() { + private get computedTotal() { if (this.hasNextPage) { return this.pageSize * this.pageSize * 1 } @@ -120,7 +120,7 @@ export class UserUiActivities extends BaseElement { ? html`
Date: Mon, 20 Jul 2026 16:12:07 -0400 Subject: [PATCH 16/23] fix computed total calculation Signed-off-by: rukmini-basu-da --- .../src/web/frontend/activities/index.test.ts | 47 ++++++++++++++++--- .../src/web/frontend/activities/index.ts | 11 +---- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/wallet-gateway/remote/src/web/frontend/activities/index.test.ts b/wallet-gateway/remote/src/web/frontend/activities/index.test.ts index 8883fbf2c..04d471ac0 100644 --- a/wallet-gateway/remote/src/web/frontend/activities/index.test.ts +++ b/wallet-gateway/remote/src/web/frontend/activities/index.test.ts @@ -119,12 +119,13 @@ describe('UserUiActivities', () => { it('shows pagination when there are more than one page of activities', async () => { mockRequest.mockResolvedValue({ - transactions: makeTransactions(5), + transactions: makeTransactions(4), + nextCursor: 'tx-4:randomisodatestring', }) el = await fixture(componentFixture) - await waitUntil(() => el.transactions.length === 5) + await waitUntil(() => el.transactions.length === 4) expect(el.shadowRoot?.querySelector('wg-pagination')).not.toBeNull() expect(getTransactionCards(el).map((c) => c.transactionId)).toEqual([ @@ -136,12 +137,36 @@ describe('UserUiActivities', () => { }) it('updates current page when pagination emits page-change', async () => { - mockRequest.mockResolvedValue({ - transactions: makeTransactions(5), + mockRequest.mockResolvedValueOnce({ + transactions: makeTransactions(4), + nextCursor: 'next-page-cursor', + }) + + mockRequest.mockResolvedValueOnce({ + transactions: [makeTransaction({ id: 'tx-5', commandId: 'cmd-5' })], + nextCursor: null, + }) + + mockRequest.mockImplementation(async (request) => { + const cursor = request?.params?.cursor + + if (cursor === 'next-page-cursor') { + return { + transactions: [ + makeTransaction({ id: 'tx-5', commandId: 'cmd-5' }), + ], + nextCursor: null, + } + } + + return { + transactions: makeTransactions(4), + nextCursor: 'next-page-cursor', + } }) el = await fixture(componentFixture) - await waitUntil(() => el.transactions.length === 5) + await waitUntil(() => el.transactions.length === 4) const pagination = el.shadowRoot?.querySelector( 'wg-pagination' @@ -149,13 +174,23 @@ describe('UserUiActivities', () => { pagination!.dispatchEvent(new PageChangeEvent(2)) await waitUntil( - () => getTransactionCards(el)[0]?.transactionId === 'tx-5', + () => + el.transactions.length === 1 && + getTransactionCards(el)[0]?.transactionId === 'tx-5', 'page 2 rendered' ) expect(pagination!.page).toBe(2) expect(getTransactionCards(el)).toHaveLength(1) expect(getTransactionCards(el)[0]?.transactionId).toBe('tx-5') + expect(mockRequest).toHaveBeenCalledTimes(2) + expect(mockRequest).toHaveBeenNthCalledWith(2, { + method: 'listTransactions', + params: { + limit: 4, + cursor: 'next-page-cursor', + }, + }) }) it('navigates to approve page when a card emits transaction-review', async () => { diff --git a/wallet-gateway/remote/src/web/frontend/activities/index.ts b/wallet-gateway/remote/src/web/frontend/activities/index.ts index 86bca46fa..03819f3b8 100644 --- a/wallet-gateway/remote/src/web/frontend/activities/index.ts +++ b/wallet-gateway/remote/src/web/frontend/activities/index.ts @@ -74,7 +74,7 @@ export class UserUiActivities extends BaseElement { private get computedTotal() { if (this.hasNextPage) { - return this.pageSize * this.pageSize * 1 + return this.pageSize * this.pageSize + 1 } return (this.currentPage - 1) * this.pageSize + this.transactions.length } @@ -149,6 +149,7 @@ export class UserUiActivities extends BaseElement { private _onPageChange(e: PageChangeEvent) { this.currentPage = e.page + this.updateTransactions() } private async updateTransactions() { @@ -189,14 +190,6 @@ export class UserUiActivities extends BaseElement { } }) ) - - const maxPage = Math.max( - 1, - Math.ceil(this.transactions.length / this.pageSize) - ) - if (this.currentPage > maxPage) { - this.currentPage = maxPage - } } catch (error) { handleErrorToast(error) } finally { From ba63e3449c1b0bb517b62cb3425de961bf2b631d Mon Sep 17 00:00:00 2001 From: rukmini-basu-da Date: Tue, 21 Jul 2026 11:40:48 -0400 Subject: [PATCH 17/23] test Signed-off-by: rukmini-basu-da --- core/wallet-store-sql/src/store-sql.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/wallet-store-sql/src/store-sql.ts b/core/wallet-store-sql/src/store-sql.ts index 8a6deeb62..6564423c9 100644 --- a/core/wallet-store-sql/src/store-sql.ts +++ b/core/wallet-store-sql/src/store-sql.ts @@ -715,7 +715,7 @@ export class StoreSql implements BaseStore, AuthAware { eb('networkId', '=', network.id), ]) ) - .orderBy('createdAt', 'desc') + .orderBy(sql`${sql.ref('createdAt')} desc nulls last`) .executeTakeFirst() return transaction ? toTransaction(transaction) : undefined From b01d6b5e44f7c7e1c7e41e25bbfb0656092ce2c6 Mon Sep 17 00:00:00 2001 From: rukmini-basu-da Date: Tue, 21 Jul 2026 15:08:10 -0400 Subject: [PATCH 18/23] add total count Signed-off-by: rukmini-basu-da --- api-specs/openrpc-user-api.json | 7 +++++- .../src/store-internal.ts | 6 +++++ core/wallet-store-sql/src/store-sql.ts | 18 +++++++++++++ core/wallet-store/src/Store.ts | 1 + core/wallet-user-rpc-client/src/index.ts | 7 ++++++ core/wallet-user-rpc-client/src/openrpc.json | 7 +++++- .../remote/src/user-api/controller.ts | 9 +++++-- .../remote/src/user-api/rpc-gen/typings.ts | 7 ++++++ .../src/web/frontend/activities/index.ts | 25 +++++++++++-------- 9 files changed, 72 insertions(+), 15 deletions(-) diff --git a/api-specs/openrpc-user-api.json b/api-specs/openrpc-user-api.json index d8364f20b..e419825e7 100644 --- a/api-specs/openrpc-user-api.json +++ b/api-specs/openrpc-user-api.json @@ -916,9 +916,14 @@ "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"] } } }, diff --git a/core/wallet-store-inmemory/src/store-internal.ts b/core/wallet-store-inmemory/src/store-internal.ts index 42b236d7c..55cd74228 100644 --- a/core/wallet-store-inmemory/src/store-internal.ts +++ b/core/wallet-store-inmemory/src/store-internal.ts @@ -455,6 +455,12 @@ export class StoreInternal implements Store, AuthAware { })[0] } + async transactionsCount(): Promise { + this.assertConnected() + const storage = this.getStorage() + return storage.transactions.size + } + async listTransactions(options?: ListTransactionsOptions) { this.assertConnected() const storage = this.getStorage() diff --git a/core/wallet-store-sql/src/store-sql.ts b/core/wallet-store-sql/src/store-sql.ts index 6564423c9..b6cec04d5 100644 --- a/core/wallet-store-sql/src/store-sql.ts +++ b/core/wallet-store-sql/src/store-sql.ts @@ -721,6 +721,24 @@ export class StoreSql implements BaseStore, AuthAware { return transaction ? toTransaction(transaction) : undefined } + async transactionsCount(): Promise { + const userId = this.assertConnected() + const network = await this.getCurrentNetwork() + + const result = await this.db + .selectFrom('transactions') + .where((eb) => + eb.and([ + eb('userId', '=', userId), + eb('networkId', '=', network.id), + ]) + ) + .select((eb) => eb.fn.count('id').as('count')) + .executeTakeFirstOrThrow() + + return Number(result.count) + } + async listTransactions(options?: ListTransactionsOptions) { const userId = this.assertConnected() const network = await this.getCurrentNetwork() diff --git a/core/wallet-store/src/Store.ts b/core/wallet-store/src/Store.ts index 1de38c687..96d28b074 100644 --- a/core/wallet-store/src/Store.ts +++ b/core/wallet-store/src/Store.ts @@ -193,6 +193,7 @@ export interface Store { ): Promise<{ transactions: Array; nextCursor: string | null }> listAllPendingTransactions(): Promise> removeTransaction(transactionId: string): Promise + transactionsCount(): Promise // Message signing request methods setMessageRaw(message: MessageRaw): Promise diff --git a/core/wallet-user-rpc-client/src/index.ts b/core/wallet-user-rpc-client/src/index.ts index 6e50a39bc..743007219 100644 --- a/core/wallet-user-rpc-client/src/index.ts +++ b/core/wallet-user-rpc-client/src/index.ts @@ -457,6 +457,12 @@ export type Transactions = Transaction[] * */ export type NextCursor = string +/** + * + * Number of total transactions for the user. + * + */ +export type Count = number /** * * The unique identifier of the current user. @@ -664,6 +670,7 @@ export interface GetTransactionResult { export interface ListTransactionsResult { transactions: Transactions nextCursor?: NextCursor + count: Count } export interface GetUserResult { userId: UserIdentifier diff --git a/core/wallet-user-rpc-client/src/openrpc.json b/core/wallet-user-rpc-client/src/openrpc.json index d8364f20b..e419825e7 100644 --- a/core/wallet-user-rpc-client/src/openrpc.json +++ b/core/wallet-user-rpc-client/src/openrpc.json @@ -916,9 +916,14 @@ "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"] } } }, diff --git a/wallet-gateway/remote/src/user-api/controller.ts b/wallet-gateway/remote/src/user-api/controller.ts index 1c296eaa7..da4c06101 100644 --- a/wallet-gateway/remote/src/user-api/controller.ts +++ b/wallet-gateway/remote/src/user-api/controller.ts @@ -1006,6 +1006,7 @@ export const userController = ( listTransactions: async function ( params?: ListTransactionsParams ): Promise { + const txCount = await store.transactionsCount() const page = await store.listTransactions(params) const transactions = page.transactions const txs = transactions.map((transaction) => ({ @@ -1032,9 +1033,13 @@ export const userController = ( })) if (page.nextCursor === null) { - return { transactions: txs } + return { transactions: txs, count: txCount } } else { - return { transactions: txs, nextCursor: page.nextCursor } + return { + transactions: txs, + nextCursor: page.nextCursor, + count: txCount, + } } }, deleteTransaction: async ( diff --git a/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts b/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts index 144616122..c5f5a3716 100644 --- a/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts +++ b/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts @@ -456,6 +456,12 @@ export type Transactions = Transaction[] * */ export type NextCursor = string +/** + * + * Number of total transactions for the user. + * + */ +export type Count = number /** * * The unique identifier of the current user. @@ -663,6 +669,7 @@ export interface GetTransactionResult { export interface ListTransactionsResult { transactions: Transactions nextCursor?: NextCursor + count: Count } export interface GetUserResult { userId: UserIdentifier diff --git a/wallet-gateway/remote/src/web/frontend/activities/index.ts b/wallet-gateway/remote/src/web/frontend/activities/index.ts index 03819f3b8..f6b4a6aa5 100644 --- a/wallet-gateway/remote/src/web/frontend/activities/index.ts +++ b/wallet-gateway/remote/src/web/frontend/activities/index.ts @@ -39,7 +39,7 @@ export class UserUiActivities extends BaseElement { accessor currentPage = 1 @state() - accessor hasNextPage = false + accessor totalTransactionCount = 0 private pageCursors: (string | undefined)[] = [undefined] @@ -72,12 +72,12 @@ export class UserUiActivities extends BaseElement { `, ] - private get computedTotal() { - if (this.hasNextPage) { - return this.pageSize * this.pageSize + 1 - } - return (this.currentPage - 1) * this.pageSize + this.transactions.length - } + // private get computedTotal() { + // if (this.hasNextPage) { + // return this.pageSize * this.pageSize + 1 + // } + // return (this.currentPage - 1) * this.pageSize + this.transactions.length + // } protected render() { return html` @@ -116,11 +116,11 @@ export class UserUiActivities extends BaseElement {
${ - this.hasNextPage || this.currentPage > 1 + this.totalTransactionCount > this.pageSize ? html`
Date: Tue, 21 Jul 2026 15:20:54 -0400 Subject: [PATCH 19/23] add test Signed-off-by: rukmini-basu-da --- .../src/web/frontend/activities/index.test.ts | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/wallet-gateway/remote/src/web/frontend/activities/index.test.ts b/wallet-gateway/remote/src/web/frontend/activities/index.test.ts index 04d471ac0..9bcb9cbdc 100644 --- a/wallet-gateway/remote/src/web/frontend/activities/index.test.ts +++ b/wallet-gateway/remote/src/web/frontend/activities/index.test.ts @@ -121,13 +121,19 @@ describe('UserUiActivities', () => { mockRequest.mockResolvedValue({ transactions: makeTransactions(4), nextCursor: 'tx-4:randomisodatestring', + total: 5, }) el = await fixture(componentFixture) await waitUntil(() => el.transactions.length === 4) + const pagination = el.shadowRoot?.querySelector( + 'wg-pagination' + ) as HTMLElement & { total: number } - expect(el.shadowRoot?.querySelector('wg-pagination')).not.toBeNull() + expect(pagination).not.toBeNull() + console.log(pagination) + // expect(pagination.total).toBe(5) expect(getTransactionCards(el).map((c) => c.transactionId)).toEqual([ 'tx-1', 'tx-2', @@ -140,29 +146,13 @@ describe('UserUiActivities', () => { mockRequest.mockResolvedValueOnce({ transactions: makeTransactions(4), nextCursor: 'next-page-cursor', + tota: 5, }) mockRequest.mockResolvedValueOnce({ transactions: [makeTransaction({ id: 'tx-5', commandId: 'cmd-5' })], nextCursor: null, - }) - - mockRequest.mockImplementation(async (request) => { - const cursor = request?.params?.cursor - - if (cursor === 'next-page-cursor') { - return { - transactions: [ - makeTransaction({ id: 'tx-5', commandId: 'cmd-5' }), - ], - nextCursor: null, - } - } - - return { - transactions: makeTransactions(4), - nextCursor: 'next-page-cursor', - } + total: 5, }) el = await fixture(componentFixture) @@ -170,7 +160,7 @@ describe('UserUiActivities', () => { const pagination = el.shadowRoot?.querySelector( 'wg-pagination' - ) as HTMLElement & { page: number } + ) as HTMLElement & { page: number; total: number } pagination!.dispatchEvent(new PageChangeEvent(2)) await waitUntil( From f11b69d787c98a021c6ef8b84b0321e8a56b9157 Mon Sep 17 00:00:00 2001 From: rukmini-basu-da Date: Tue, 21 Jul 2026 15:26:28 -0400 Subject: [PATCH 20/23] rename variables Signed-off-by: rukmini-basu-da --- .../remote/src/web/frontend/activities/index.test.ts | 12 ++++++------ .../remote/src/web/frontend/activities/index.ts | 7 ------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/wallet-gateway/remote/src/web/frontend/activities/index.test.ts b/wallet-gateway/remote/src/web/frontend/activities/index.test.ts index 9bcb9cbdc..deee972dd 100644 --- a/wallet-gateway/remote/src/web/frontend/activities/index.test.ts +++ b/wallet-gateway/remote/src/web/frontend/activities/index.test.ts @@ -121,7 +121,7 @@ describe('UserUiActivities', () => { mockRequest.mockResolvedValue({ transactions: makeTransactions(4), nextCursor: 'tx-4:randomisodatestring', - total: 5, + count: 5, }) el = await fixture(componentFixture) @@ -129,11 +129,11 @@ describe('UserUiActivities', () => { await waitUntil(() => el.transactions.length === 4) const pagination = el.shadowRoot?.querySelector( 'wg-pagination' - ) as HTMLElement & { total: number } + ) as HTMLElement & { count: number } expect(pagination).not.toBeNull() console.log(pagination) - // expect(pagination.total).toBe(5) + expect(pagination.count).toBe(5) expect(getTransactionCards(el).map((c) => c.transactionId)).toEqual([ 'tx-1', 'tx-2', @@ -146,13 +146,13 @@ describe('UserUiActivities', () => { mockRequest.mockResolvedValueOnce({ transactions: makeTransactions(4), nextCursor: 'next-page-cursor', - tota: 5, + count: 5, }) mockRequest.mockResolvedValueOnce({ transactions: [makeTransaction({ id: 'tx-5', commandId: 'cmd-5' })], nextCursor: null, - total: 5, + count: 5, }) el = await fixture(componentFixture) @@ -160,7 +160,7 @@ describe('UserUiActivities', () => { const pagination = el.shadowRoot?.querySelector( 'wg-pagination' - ) as HTMLElement & { page: number; total: number } + ) as HTMLElement & { page: number; count: number } pagination!.dispatchEvent(new PageChangeEvent(2)) await waitUntil( diff --git a/wallet-gateway/remote/src/web/frontend/activities/index.ts b/wallet-gateway/remote/src/web/frontend/activities/index.ts index f6b4a6aa5..e5e0f7526 100644 --- a/wallet-gateway/remote/src/web/frontend/activities/index.ts +++ b/wallet-gateway/remote/src/web/frontend/activities/index.ts @@ -72,13 +72,6 @@ export class UserUiActivities extends BaseElement { `, ] - // private get computedTotal() { - // if (this.hasNextPage) { - // return this.pageSize * this.pageSize + 1 - // } - // return (this.currentPage - 1) * this.pageSize + this.transactions.length - // } - protected render() { return html`