feat: allow to edit the wallet mints and relays (with tests updated)

This commit is contained in:
P. Reis 2025-03-18 18:30:52 -03:00
parent 7c1297e865
commit feff31f094
2 changed files with 91 additions and 19 deletions

View file

@ -35,6 +35,9 @@ Deno.test('PUT /wallet must be successful', async () => {
'https://houston.mint.com', // duplicate on purpose 'https://houston.mint.com', // duplicate on purpose
'https://cuiaba.mint.com', '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', 'https://cuiaba.mint.com',
]); ]);
assertEquals(data.relays, [ assertEquals(data.relays, [
'ws://localhost:4036/relay', 'wss://manager.com/relay',
]); ]);
assertEquals(data.balance, 0); assertEquals(data.balance, 0);
@ -79,7 +82,7 @@ Deno.test('PUT /wallet must be successful', async () => {
assertEquals(nutzap_p2pk, p2pk); assertEquals(nutzap_p2pk, p2pk);
assertEquals([nutzap_info.tags.find(([name]) => name === 'relay')?.[1]!], [ assertEquals([nutzap_info.tags.find(([name]) => name === 'relay')?.[1]!], [
'ws://localhost:4036/relay', 'wss://manager.com/relay',
]); ]);
mock.restore(); mock.restore();
@ -111,31 +114,87 @@ Deno.test('PUT /wallet must NOT be successful: wrong request body/schema', async
mock.restore(); 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', () => { const mock = stub(globalThis, 'fetch', () => {
return Promise.resolve(new Response()); return Promise.resolve(new Response());
}); });
await using test = await createTestRoute(); 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', { const response = await route.request('/wallet', {
method: 'PUT', method: 'PUT',
headers: { headers: {
'authorization': `Bearer ${nip19.nsecEncode(sk)}`,
'content-type': 'application/json', 'content-type': 'application/json',
}, },
body: JSON.stringify({ 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); const data = walletSchema.parse(body);
assertEquals(body2, { error: 'You already have a wallet 😏' });
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(); mock.restore();
}); });

View file

@ -4,7 +4,7 @@ import { userMiddleware } from '@ditto/mastoapi/middleware';
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';
import { bytesToString } from '@scure/base'; import { bytesToString, stringToBytes } from '@scure/base';
import { logi } from '@soapbox/logi'; import { logi } from '@soapbox/logi';
import { z } from 'zod'; import { z } from 'zod';
@ -159,6 +159,9 @@ const createWalletSchema = z.object({
mints: z.array(z.string().url()).nonempty().transform((val) => { mints: z.array(z.string().url()).nonempty().transform((val) => {
return [...new Set(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 * https://github.com/nostr-protocol/nips/blob/master/61.md#nutzap-informational-event
*/ */
route.put('/wallet', userMiddleware({ enc: 'nip44' }), async (c) => { 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 pubkey = await user.signer.getPublicKey();
const body = await parseBody(c.req.raw); 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); 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 }); const [event] = await relay.query([{ authors: [pubkey], kinds: [17375] }], { signal });
if (event) { 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 walletContentTags: string[][] = [];
const sk = generateSecretKey(); const privkey = previousPrivkey ?? bytesToString('hex', generateSecretKey());
const privkey = bytesToString('hex', sk); const p2pk = getPublicKey(stringToBytes('hex', privkey));
const p2pk = getPublicKey(sk);
walletContentTags.push(['privkey', privkey]); walletContentTags.push(['privkey', privkey]);
@ -209,7 +222,7 @@ route.put('/wallet', userMiddleware({ enc: 'nip44' }), async (c) => {
kind: 10019, kind: 10019,
tags: [ tags: [
...mints.map((mint) => ['mint', mint, 'sat']), ...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], ['pubkey', p2pk],
], ],
}, c); }, c);
@ -218,7 +231,7 @@ route.put('/wallet', userMiddleware({ enc: 'nip44' }), async (c) => {
const walletEntity: Wallet = { const walletEntity: Wallet = {
pubkey_p2pk: p2pk, pubkey_p2pk: p2pk,
mints, mints,
relays: [conf.relay], relays,
balance: 0, // Newly created wallet, balance is zero. balance: 0, // Newly created wallet, balance is zero.
}; };