From feff31f09477a6a21c5165ff817d08aadabcda0d Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 18 Mar 2025 18:30:52 -0300 Subject: [PATCH] feat: allow to edit the wallet mints and relays (with tests updated) --- packages/ditto/controllers/api/cashu.test.ts | 79 +++++++++++++++++--- packages/ditto/controllers/api/cashu.ts | 31 +++++--- 2 files changed, 91 insertions(+), 19 deletions(-) diff --git a/packages/ditto/controllers/api/cashu.test.ts b/packages/ditto/controllers/api/cashu.test.ts index b7ebdd16..83944d81 100644 --- a/packages/ditto/controllers/api/cashu.test.ts +++ b/packages/ditto/controllers/api/cashu.test.ts @@ -35,6 +35,9 @@ Deno.test('PUT /wallet must be successful', async () => { 'https://houston.mint.com', // duplicate on purpose 'https://cuiaba.mint.com', ], + relays: [ + 'wss://manager.com/relay', + ], }), }); @@ -65,7 +68,7 @@ Deno.test('PUT /wallet must be successful', async () => { 'https://cuiaba.mint.com', ]); assertEquals(data.relays, [ - 'ws://localhost:4036/relay', + 'wss://manager.com/relay', ]); assertEquals(data.balance, 0); @@ -79,7 +82,7 @@ Deno.test('PUT /wallet must be successful', async () => { assertEquals(nutzap_p2pk, p2pk); assertEquals([nutzap_info.tags.find(([name]) => name === 'relay')?.[1]!], [ - 'ws://localhost:4036/relay', + 'wss://manager.com/relay', ]); mock.restore(); @@ -111,31 +114,87 @@ Deno.test('PUT /wallet must NOT be successful: wrong request body/schema', async mock.restore(); }); -Deno.test('PUT /wallet must NOT be successful: wallet already exists', async () => { +Deno.test('PUT /wallet must be successful: edit wallet', async () => { const mock = stub(globalThis, 'fetch', () => { return Promise.resolve(new Response()); }); await using test = await createTestRoute(); - const { route, sk, relay } = test; + const { route, sk, relay, signer } = test; - await relay.event(genEvent({ kind: 17375 }, sk)); + const pubkey = await signer.getPublicKey(); + const privkey = bytesToString('hex', generateSecretKey()); + const p2pk = getPublicKey(stringToBytes('hex', privkey)); + + // Wallet + await relay.event(genEvent({ + kind: 17375, + content: await signer.nip44.encrypt( + pubkey, + JSON.stringify([ + ['privkey', privkey], + ['mint', 'https://mint.soul.com'], + ]), + ), + }, sk)); + + // Nutzap information + await relay.event(genEvent({ + kind: 10019, + tags: [ + ['pubkey', p2pk], + ['mint', 'https://mint.soul.com'], + ['relay', 'ws://localhost:4036/relay'], + ], + }, sk)); const response = await route.request('/wallet', { method: 'PUT', headers: { - 'authorization': `Bearer ${nip19.nsecEncode(sk)}`, 'content-type': 'application/json', }, body: JSON.stringify({ - mints: ['https://mint.heart.com'], + mints: [ + 'https://new-vampire-mint.com', + 'https://new-age-mint.com', + ], + relays: [ + 'wss://law-of-the-universe/relay', + 'wss://law-of-the-universe/relay', + ], }), }); - const body2 = await response.json(); + const body = await response.json(); - assertEquals(response.status, 400); - assertEquals(body2, { error: 'You already have a wallet 😏' }); + const data = walletSchema.parse(body); + + assertEquals(response.status, 200); + + assertEquals(bytesToString('hex', sk) !== privkey, true); + + assertEquals(data.pubkey_p2pk, p2pk); + assertEquals(data.mints, [ + 'https://new-vampire-mint.com', + 'https://new-age-mint.com', + ]); + assertEquals(data.relays, [ + 'wss://law-of-the-universe/relay', + ]); + assertEquals(data.balance, 0); + + const [nutzap_info] = await relay.query([{ authors: [pubkey], kinds: [10019] }]); + + assertExists(nutzap_info); + assertEquals(nutzap_info.kind, 10019); + assertEquals(nutzap_info.tags.length, 4); + + const nutzap_p2pk = nutzap_info.tags.find(([value]) => value === 'pubkey')?.[1]!; + + assertEquals(nutzap_p2pk, p2pk); + assertEquals([nutzap_info.tags.find(([name]) => name === 'relay')?.[1]!], [ + 'wss://law-of-the-universe/relay', + ]); mock.restore(); }); diff --git a/packages/ditto/controllers/api/cashu.ts b/packages/ditto/controllers/api/cashu.ts index 7c4f33f7..cdd0f911 100644 --- a/packages/ditto/controllers/api/cashu.ts +++ b/packages/ditto/controllers/api/cashu.ts @@ -4,7 +4,7 @@ import { userMiddleware } from '@ditto/mastoapi/middleware'; import { DittoRoute } from '@ditto/mastoapi/router'; import { generateSecretKey, getPublicKey } from 'nostr-tools'; import { NostrEvent, NSchema as n } from '@nostrify/nostrify'; -import { bytesToString } from '@scure/base'; +import { bytesToString, stringToBytes } from '@scure/base'; import { logi } from '@soapbox/logi'; import { z } from 'zod'; @@ -159,6 +159,9 @@ const createWalletSchema = z.object({ mints: z.array(z.string().url()).nonempty().transform((val) => { return [...new Set(val)]; }), + relays: z.array(z.string().url()).transform((val) => { + return [...new Set(val)]; + }), }); /** @@ -167,7 +170,7 @@ const createWalletSchema = z.object({ * https://github.com/nostr-protocol/nips/blob/master/61.md#nutzap-informational-event */ route.put('/wallet', userMiddleware({ enc: 'nip44' }), async (c) => { - const { conf, user, relay, signal } = c.var; + const { user, relay, signal } = c.var; const pubkey = await user.signer.getPublicKey(); const body = await parseBody(c.req.raw); @@ -177,18 +180,28 @@ route.put('/wallet', userMiddleware({ enc: 'nip44' }), async (c) => { return c.json({ error: 'Bad schema', schema: result.error }, 400); } - const { mints } = result.data; + const { mints, relays } = result.data; + let previousPrivkey: string | undefined; const [event] = await relay.query([{ authors: [pubkey], kinds: [17375] }], { signal }); if (event) { - return c.json({ error: 'You already have a wallet 😏' }, 400); + const walletContentSchema = z.string().array().min(2).array(); + + const { data: walletContent, success, error } = n.json().pipe(walletContentSchema).safeParse( + await user.signer.nip44.decrypt(pubkey, event.content), + ); + + if (!success) { + return c.json({ error: 'Your wallet is in an invalid format', schema: error }, 400); + } + + previousPrivkey = walletContent.find(([name]) => name === 'privkey')?.[1]; } const walletContentTags: string[][] = []; - const sk = generateSecretKey(); - const privkey = bytesToString('hex', sk); - const p2pk = getPublicKey(sk); + const privkey = previousPrivkey ?? bytesToString('hex', generateSecretKey()); + const p2pk = getPublicKey(stringToBytes('hex', privkey)); walletContentTags.push(['privkey', privkey]); @@ -209,7 +222,7 @@ route.put('/wallet', userMiddleware({ enc: 'nip44' }), async (c) => { kind: 10019, tags: [ ...mints.map((mint) => ['mint', mint, 'sat']), - ['relay', conf.relay], // TODO: add more relays once things get more stable + ...relays.map((relay) => ['relay', relay]), ['pubkey', p2pk], ], }, c); @@ -218,7 +231,7 @@ route.put('/wallet', userMiddleware({ enc: 'nip44' }), async (c) => { const walletEntity: Wallet = { pubkey_p2pk: p2pk, mints, - relays: [conf.relay], + relays, balance: 0, // Newly created wallet, balance is zero. };