diff --git a/packages/ditto/utils/cashu.test.ts b/packages/ditto/utils/cashu.test.ts index 9a0621e8..14f14900 100644 --- a/packages/ditto/utils/cashu.test.ts +++ b/packages/ditto/utils/cashu.test.ts @@ -5,7 +5,7 @@ import { assertEquals } from '@std/assert'; import { createTestDB, genEvent } from '@/test.ts'; -import { validateAndParseWallet } from '@/utils/cashu.ts'; +import { organizeProofs, validateAndParseWallet } from '@/utils/cashu.ts'; Deno.test('validateAndParseWallet function returns valid data', async () => { await using db = await createTestDB({ pure: true }); @@ -51,3 +51,119 @@ Deno.test('validateAndParseWallet function returns valid data', async () => { mints: ['https://mint.soul.com'], }); }); + +Deno.test('organizeProofs function is working', async () => { + await using db = await createTestDB({ pure: true }); + const store = db.store; + + const sk = generateSecretKey(); + const signer = new NSecSigner(sk); + const pubkey = await signer.getPublicKey(); + + const event1 = genEvent({ + kind: 7375, + content: await signer.nip44.encrypt( + pubkey, + JSON.stringify({ + mint: 'https://mint.soul.com', + proofs: [ + { + id: '005c2502034d4f12', + amount: 25, + secret: 'z+zyxAVLRqN9lEjxuNPSyRJzEstbl69Jc1vtimvtkPg=', + C: '0241d98a8197ef238a192d47edf191a9de78b657308937b4f7dd0aa53beae72c46', + }, + { + id: '005c2502034d4f12', + amount: 25, + secret: 'z+zyxAVLRqN9lEjxuNPSyRJzEstbl69Jc1vtimvtkPg=', + C: '0241d98a8197ef238a192d47edf191a9de78b657308937b4f7dd0aa53beae72c46', + }, + { + id: '005c2502034d4f12', + amount: 25, + secret: 'z+zyxAVLRqN9lEjxuNPSyRJzEstbl69Jc1vtimvtkPg=', + C: '0241d98a8197ef238a192d47edf191a9de78b657308937b4f7dd0aa53beae72c46', + }, + { + id: '005c2502034d4f12', + amount: 25, + secret: 'z+zyxAVLRqN9lEjxuNPSyRJzEstbl69Jc1vtimvtkPg=', + C: '0241d98a8197ef238a192d47edf191a9de78b657308937b4f7dd0aa53beae72c46', + }, + ], + del: [], + }), + ), + }, sk); + await db.store.event(event1); + + const proof1 = { + 'id': '004f7adf2a04356c', + 'amount': 1, + 'secret': '6780378b186cf7ada639ce4807803ad5e4a71217688430512f35074f9bca99c0', + 'C': '03f0dd8df04427c8c53e4ae9ce8eb91c4880203d6236d1d745c788a5d7a47aaff3', + 'dleq': { + 'e': 'bd22fcdb7ede1edb52b9b8c6e1194939112928e7b4fc0176325e7671fb2bd351', + 's': 'a9ad015571a0e538d62966a16d2facf806fb956c746a3dfa41fa689486431c67', + 'r': 'b283980e30bf5a31a45e5e296e93ae9f20bf3a140c884b3b4cd952dbecc521df', + }, + }; + const token1 = JSON.stringify({ + mint: 'https://mint-fashion.com', + proofs: [proof1], + del: [], + }); + + const event2 = genEvent({ + kind: 7375, + content: await signer.nip44.encrypt( + pubkey, + token1, + ), + }, sk); + await db.store.event(event2); + + const proof2 = { + 'id': '004f7adf2a04356c', + 'amount': 123, + 'secret': '6780378b186cf7ada639ce4807803ad5e4a71217688430512f35074f9bca99c0', + 'C': '03f0dd8df04427c8c53e4ae9ce8eb91c4880203d6236d1d745c788a5d7a47aaff3', + 'dleq': { + 'e': 'bd22fcdb7ede1edb52b9b8c6e1194939112928e7b4fc0176325e7671fb2bd351', + 's': 'a9ad015571a0e538d62966a16d2facf806fb956c746a3dfa41fa689486431c67', + 'r': 'b283980e30bf5a31a45e5e296e93ae9f20bf3a140c884b3b4cd952dbecc521df', + }, + }; + + const token2 = JSON.stringify({ + mint: 'https://mint-fashion.com', + proofs: [proof2], + del: [], + }); + + const event3 = genEvent({ + kind: 7375, + content: await signer.nip44.encrypt( + pubkey, + token2, + ), + }, sk); + await db.store.event(event3); + + const unspentProofs = await store.query([{ kinds: [7375], authors: [pubkey] }]); + + const organizedProofs = await organizeProofs(unspentProofs, signer); + + assertEquals(organizedProofs, { + 'https://mint.soul.com': { + totalBalance: 100, + [event1.id]: { event: event1, balance: 100 }, + }, + 'https://mint-fashion.com': { + totalBalance: 124, + [event2.id]: { event: event2, balance: 1 }, + [event3.id]: { event: event3, balance: 123 }, + }, + }); +}); diff --git a/packages/ditto/utils/cashu.ts b/packages/ditto/utils/cashu.ts index 05a88272..625c86f3 100644 --- a/packages/ditto/utils/cashu.ts +++ b/packages/ditto/utils/cashu.ts @@ -7,6 +7,7 @@ import { z } from 'zod'; import { errorJson } from '@/utils/log.ts'; import { isNostrId } from '@/utils.ts'; +import { tokenEventSchema } from '@/schemas/cashu.ts'; type Data = { wallet: NostrEvent; @@ -99,4 +100,43 @@ async function validateAndParseWallet( return { data: { wallet, nutzapInfo, privkey, p2pk, mints }, error: null }; } -export { validateAndParseWallet }; +type OrganizedProofs = { + [mintUrl: string]: { + /** Total balance in this mint */ + totalBalance: number; + /** Event id */ + [eventId: string]: { + event: NostrEvent; + /** Total balance in this event */ + balance: number; + } | number; + }; +}; +async function organizeProofs( + events: NostrEvent[], + signer: SetRequired, +): Promise { + const organizedProofs: OrganizedProofs = {}; + const pubkey = await signer.getPublicKey(); + + for (const event of events) { + const decryptedContent = await signer.nip44.decrypt(pubkey, event.content); + const { data: token, success } = n.json().pipe(tokenEventSchema).safeParse(decryptedContent); + if (!success) { + continue; + } + const { mint, proofs } = token; + + const balance = proofs.reduce((prev, current) => prev + current.amount, 0); + + if (!organizedProofs[mint]) { + organizedProofs[mint] = { totalBalance: 0 }; + } + + organizedProofs[mint] = { ...organizedProofs[mint], [event.id]: { event, balance } }; + organizedProofs[mint].totalBalance += balance; + } + return organizedProofs; +} + +export { organizeProofs, validateAndParseWallet };