From 83c96c88b7125bf9b367d4022077264bcc4ebed6 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 24 Mar 2025 23:02:04 -0300 Subject: [PATCH] feat: support pagination in GET transactions remove getTransactions function and replace it with renderTransaction function (all tests updated) --- packages/cashu/cashu.test.ts | 78 +---------------------- packages/cashu/cashu.ts | 51 +-------------- packages/cashu/mod.ts | 10 +-- packages/cashu/views.test.ts | 85 +++++++++++++++++++++++++ packages/cashu/views.ts | 44 +++++++++++++ packages/ditto/controllers/api/cashu.ts | 20 ++++-- 6 files changed, 149 insertions(+), 139 deletions(-) create mode 100644 packages/cashu/views.test.ts create mode 100644 packages/cashu/views.ts diff --git a/packages/cashu/cashu.test.ts b/packages/cashu/cashu.test.ts index 5bf88951..2e5aca5b 100644 --- a/packages/cashu/cashu.test.ts +++ b/packages/cashu/cashu.test.ts @@ -9,14 +9,7 @@ import { assertEquals } from '@std/assert'; import { DittoPolyPg, TestDB } from '@ditto/db'; import { DittoConf } from '@ditto/conf'; -import { - getLastRedeemedNutzap, - getMintsToProofs, - getTransactions, - getWallet, - organizeProofs, - validateAndParseWallet, -} from './cashu.ts'; +import { getLastRedeemedNutzap, getMintsToProofs, getWallet, organizeProofs, validateAndParseWallet } from './cashu.ts'; Deno.test('validateAndParseWallet function returns valid data', async () => { const conf = new DittoConf(Deno.env); @@ -462,72 +455,3 @@ Deno.test('getWallet function is working', async () => { pubkey_p2pk: p2pk, }); }); - -Deno.test('getTransactions function is working', async () => { - const conf = new DittoConf(Deno.env); - const orig = new DittoPolyPg(conf.databaseUrl); - - await using db = new TestDB(orig); - await db.migrate(); - await db.clear(); - - const sk = generateSecretKey(); - const signer = new NSecSigner(sk); - const pubkey = await signer.getPublicKey(); - - const relay = new NPostgres(orig.kysely); - - const history1 = genEvent({ - kind: 7376, - content: await signer.nip44.encrypt( - pubkey, - JSON.stringify([ - ['direction', 'in'], - ['amount', '33'], - ]), - ), - created_at: Math.floor(Date.now() / 1000), // now - }, sk); - await relay.event(history1); - - const history2 = genEvent({ - kind: 7376, - content: await signer.nip44.encrypt( - pubkey, - JSON.stringify([ - ['direction', 'out'], - ['amount', '29'], - ]), - ), - created_at: Math.floor(Date.now() / 1000) - 1, // now - 1 second - }, sk); - await relay.event(history2); - - const history3 = genEvent({ - kind: 7376, - content: await signer.nip44.encrypt( - pubkey, - JSON.stringify([ - ['direction', 'ouch'], - ['amount', 'yolo'], - ]), - ), - created_at: Math.floor(Date.now() / 1000) - 2, // now - 2 second - }, sk); - await relay.event(history3); - - const transactions = await getTransactions(relay, pubkey, signer, {}); - - assertEquals(transactions, [ - { - direction: 'in', - amount: 33, - created_at: history1.created_at, - }, - { - direction: 'out', - amount: 29, - created_at: history2.created_at, - }, - ]); -}); diff --git a/packages/cashu/cashu.ts b/packages/cashu/cashu.ts index 8bce4e48..aa1b3583 100644 --- a/packages/cashu/cashu.ts +++ b/packages/cashu/cashu.ts @@ -286,55 +286,6 @@ async function getWallet( return walletEntity; } -type Transactions = { - amount: number; - created_at: number; - direction: 'in' | 'out'; -}[]; - -/** Returns a history of transactions. */ -async function getTransactions( - store: NStore, - pubkey: string, - signer: SetRequired, - pagination: { limit?: number; until?: number; since?: number }, - opts?: { signal?: AbortSignal }, -): Promise { - const { since, until, limit } = pagination; - const transactions: Transactions = []; - - const events = await store.query([{ kinds: [7376], authors: [pubkey], since, until, limit }], { - signal: opts?.signal, - }); - - for (const event of events) { - const { data: contentTags, success } = n.json().pipe(z.coerce.string().array().min(2).array()).safeParse( - await signer.nip44.decrypt(pubkey, event.content), - ); - - if (!success) { - continue; - } - - const direction = contentTags.find(([name]) => name === 'direction')?.[1]; - if (direction !== 'out' && direction !== 'in') { - continue; - } - const amount = parseInt(contentTags.find(([name]) => name === 'amount')?.[1] ?? '', 10); - if (isNaN(amount)) { - continue; - } - - transactions.push({ - created_at: event.created_at, - direction, - amount, - }); - } - - return transactions; -} - /** Serialize an error into JSON for JSON logging. */ export function errorJson(error: unknown): Error | null { if (error instanceof Error) { @@ -348,4 +299,4 @@ function isNostrId(value: unknown): boolean { return n.id().safeParse(value).success; } -export { getLastRedeemedNutzap, getMintsToProofs, getTransactions, getWallet, organizeProofs, validateAndParseWallet }; +export { getLastRedeemedNutzap, getMintsToProofs, getWallet, organizeProofs, validateAndParseWallet }; diff --git a/packages/cashu/mod.ts b/packages/cashu/mod.ts index 392eeccb..9d939097 100644 --- a/packages/cashu/mod.ts +++ b/packages/cashu/mod.ts @@ -1,9 +1,3 @@ -export { - getLastRedeemedNutzap, - getMintsToProofs, - getTransactions, - getWallet, - organizeProofs, - validateAndParseWallet, -} from './cashu.ts'; +export { getLastRedeemedNutzap, getMintsToProofs, getWallet, organizeProofs, validateAndParseWallet } from './cashu.ts'; export { proofSchema, tokenEventSchema, type Wallet, walletSchema } from './schemas.ts'; +export { renderTransaction, type Transaction } from './views.ts'; diff --git a/packages/cashu/views.test.ts b/packages/cashu/views.test.ts new file mode 100644 index 00000000..6cff62ee --- /dev/null +++ b/packages/cashu/views.test.ts @@ -0,0 +1,85 @@ +import { NSecSigner } from '@nostrify/nostrify'; +import { NPostgres } from '@nostrify/db'; +import { genEvent } from '@nostrify/nostrify/test'; + +import { generateSecretKey } from 'nostr-tools'; +import { assertEquals } from '@std/assert'; + +import { DittoPolyPg, TestDB } from '@ditto/db'; +import { DittoConf } from '@ditto/conf'; +import { renderTransaction, type Transaction } from './views.ts'; + +Deno.test('renderTransaction function is working', async () => { + const conf = new DittoConf(Deno.env); + const orig = new DittoPolyPg(conf.databaseUrl); + + await using db = new TestDB(orig); + await db.migrate(); + await db.clear(); + + const sk = generateSecretKey(); + const signer = new NSecSigner(sk); + const pubkey = await signer.getPublicKey(); + + const relay = new NPostgres(orig.kysely); + + const history1 = genEvent({ + kind: 7376, + content: await signer.nip44.encrypt( + pubkey, + JSON.stringify([ + ['direction', 'in'], + ['amount', '33'], + ]), + ), + created_at: Math.floor(Date.now() / 1000), // now + }, sk); + await relay.event(history1); + + const history2 = genEvent({ + kind: 7376, + content: await signer.nip44.encrypt( + pubkey, + JSON.stringify([ + ['direction', 'out'], + ['amount', '29'], + ]), + ), + created_at: Math.floor(Date.now() / 1000) - 1, // now - 1 second + }, sk); + await relay.event(history2); + + const history3 = genEvent({ + kind: 7376, + content: await signer.nip44.encrypt( + pubkey, + JSON.stringify([ + ['direction', 'ouch'], + ['amount', 'yolo'], + ]), + ), + created_at: Math.floor(Date.now() / 1000) - 2, // now - 2 second + }, sk); + await relay.event(history3); + + const events = await relay.query([{ kinds: [7376], authors: [pubkey], since: history2.created_at }]); + + const transactions = await Promise.all( + events.map((event) => { + return renderTransaction(event, pubkey, signer); + }), + ); + + assertEquals(transactions, [ + { + direction: 'in', + amount: 33, + created_at: history1.created_at, + }, + { + direction: 'out', + amount: 29, + created_at: history2.created_at, + }, + ]); +}); diff --git a/packages/cashu/views.ts b/packages/cashu/views.ts new file mode 100644 index 00000000..0b2467a8 --- /dev/null +++ b/packages/cashu/views.ts @@ -0,0 +1,44 @@ +import { type NostrEvent, type NostrSigner, NSchema as n } from '@nostrify/nostrify'; +import type { SetRequired } from 'type-fest'; +import { z } from 'zod'; + +type Transaction = { + amount: number; + created_at: number; + direction: 'in' | 'out'; +}; + +/** Renders one history of transaction. */ +async function renderTransaction( + event: NostrEvent, + viewerPubkey: string, + signer: SetRequired, +): Promise { + if (event.kind !== 7376) return; + + const { data: contentTags, success } = n.json().pipe(z.coerce.string().array().min(2).array()).safeParse( + await signer.nip44.decrypt(viewerPubkey, event.content), + ); + + if (!success) { + return; + } + + const direction = contentTags.find(([name]) => name === 'direction')?.[1]; + if (direction !== 'out' && direction !== 'in') { + return; + } + + const amount = parseInt(contentTags.find(([name]) => name === 'amount')?.[1] ?? '', 10); + if (isNaN(amount)) { + return; + } + + return { + created_at: event.created_at, + direction, + amount, + }; +} + +export { renderTransaction, type Transaction }; diff --git a/packages/ditto/controllers/api/cashu.ts b/packages/ditto/controllers/api/cashu.ts index f5c3bf78..f355d392 100644 --- a/packages/ditto/controllers/api/cashu.ts +++ b/packages/ditto/controllers/api/cashu.ts @@ -1,14 +1,14 @@ import { CashuMint, CashuWallet, MintQuoteState, Proof } from '@cashu/cashu-ts'; import { - getTransactions, getWallet, organizeProofs, + renderTransaction, tokenEventSchema, validateAndParseWallet, type Wallet, } from '@ditto/cashu'; import { userMiddleware } from '@ditto/mastoapi/middleware'; -import { paginationSchema } from '@ditto/mastoapi/pagination'; +import { paginated, paginationSchema } from '@ditto/mastoapi/pagination'; import { DittoRoute } from '@ditto/mastoapi/router'; import { generateSecretKey, getPublicKey } from 'nostr-tools'; import { NostrEvent, NSchema as n } from '@nostrify/nostrify'; @@ -277,9 +277,21 @@ route.get('/transactions', userMiddleware({ enc: 'nip44' }), async (c) => { const pubkey = await user.signer.getPublicKey(); - const transactions = await getTransactions(relay, pubkey, user.signer, { since, until, limit }, { signal }); + const events = await relay.query([{ kinds: [7376], authors: [pubkey], since, until, limit }], { + signal, + }); - return c.json(transactions, 200); + const transactions = await Promise.all( + events.map((event) => { + return renderTransaction(event, pubkey, user.signer); + }), + ); + + if (!transactions.length) { + return c.json([], 200); + } + + return paginated(c, events, transactions); }); /** Get mints set by the CASHU_MINTS environment variable. */