mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 03:19:46 +00:00
feat: support pagination in GET transactions
remove getTransactions function and replace it with renderTransaction function (all tests updated)
This commit is contained in:
parent
1360484ae9
commit
83c96c88b7
6 changed files with 149 additions and 139 deletions
|
|
@ -9,14 +9,7 @@ import { assertEquals } from '@std/assert';
|
||||||
import { DittoPolyPg, TestDB } from '@ditto/db';
|
import { DittoPolyPg, TestDB } from '@ditto/db';
|
||||||
import { DittoConf } from '@ditto/conf';
|
import { DittoConf } from '@ditto/conf';
|
||||||
|
|
||||||
import {
|
import { getLastRedeemedNutzap, getMintsToProofs, getWallet, organizeProofs, validateAndParseWallet } from './cashu.ts';
|
||||||
getLastRedeemedNutzap,
|
|
||||||
getMintsToProofs,
|
|
||||||
getTransactions,
|
|
||||||
getWallet,
|
|
||||||
organizeProofs,
|
|
||||||
validateAndParseWallet,
|
|
||||||
} from './cashu.ts';
|
|
||||||
|
|
||||||
Deno.test('validateAndParseWallet function returns valid data', async () => {
|
Deno.test('validateAndParseWallet function returns valid data', async () => {
|
||||||
const conf = new DittoConf(Deno.env);
|
const conf = new DittoConf(Deno.env);
|
||||||
|
|
@ -462,72 +455,3 @@ Deno.test('getWallet function is working', async () => {
|
||||||
pubkey_p2pk: p2pk,
|
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,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -286,55 +286,6 @@ async function getWallet(
|
||||||
return walletEntity;
|
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<NostrSigner, 'nip44'>,
|
|
||||||
pagination: { limit?: number; until?: number; since?: number },
|
|
||||||
opts?: { signal?: AbortSignal },
|
|
||||||
): Promise<Transactions> {
|
|
||||||
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. */
|
/** Serialize an error into JSON for JSON logging. */
|
||||||
export function errorJson(error: unknown): Error | null {
|
export function errorJson(error: unknown): Error | null {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
|
|
@ -348,4 +299,4 @@ function isNostrId(value: unknown): boolean {
|
||||||
return n.id().safeParse(value).success;
|
return n.id().safeParse(value).success;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { getLastRedeemedNutzap, getMintsToProofs, getTransactions, getWallet, organizeProofs, validateAndParseWallet };
|
export { getLastRedeemedNutzap, getMintsToProofs, getWallet, organizeProofs, validateAndParseWallet };
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,3 @@
|
||||||
export {
|
export { getLastRedeemedNutzap, getMintsToProofs, getWallet, organizeProofs, validateAndParseWallet } from './cashu.ts';
|
||||||
getLastRedeemedNutzap,
|
|
||||||
getMintsToProofs,
|
|
||||||
getTransactions,
|
|
||||||
getWallet,
|
|
||||||
organizeProofs,
|
|
||||||
validateAndParseWallet,
|
|
||||||
} from './cashu.ts';
|
|
||||||
export { proofSchema, tokenEventSchema, type Wallet, walletSchema } from './schemas.ts';
|
export { proofSchema, tokenEventSchema, type Wallet, walletSchema } from './schemas.ts';
|
||||||
|
export { renderTransaction, type Transaction } from './views.ts';
|
||||||
|
|
|
||||||
85
packages/cashu/views.test.ts
Normal file
85
packages/cashu/views.test.ts
Normal file
|
|
@ -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,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
44
packages/cashu/views.ts
Normal file
44
packages/cashu/views.ts
Normal file
|
|
@ -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<NostrSigner, 'nip44'>,
|
||||||
|
): Promise<Transaction | undefined> {
|
||||||
|
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 };
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { CashuMint, CashuWallet, MintQuoteState, Proof } from '@cashu/cashu-ts';
|
import { CashuMint, CashuWallet, MintQuoteState, Proof } from '@cashu/cashu-ts';
|
||||||
import {
|
import {
|
||||||
getTransactions,
|
|
||||||
getWallet,
|
getWallet,
|
||||||
organizeProofs,
|
organizeProofs,
|
||||||
|
renderTransaction,
|
||||||
tokenEventSchema,
|
tokenEventSchema,
|
||||||
validateAndParseWallet,
|
validateAndParseWallet,
|
||||||
type Wallet,
|
type Wallet,
|
||||||
} from '@ditto/cashu';
|
} from '@ditto/cashu';
|
||||||
import { userMiddleware } from '@ditto/mastoapi/middleware';
|
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 { DittoRoute } from '@ditto/mastoapi/router';
|
||||||
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
||||||
import { NostrEvent, NSchema as n } from '@nostrify/nostrify';
|
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 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. */
|
/** Get mints set by the CASHU_MINTS environment variable. */
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue