diff --git a/event.ts b/event.ts index 6ddaf11..d07c56e 100644 --- a/event.ts +++ b/event.ts @@ -31,6 +31,7 @@ export enum Kind { Zap = 9735, RelayList = 10002, ClientAuth = 22242, + NwcRequest = 23194, HttpAuth = 27235, ProfileBadge = 30008, BadgeDefinition = 30009, diff --git a/index.ts b/index.ts index c9dc368..8d0e0a3 100644 --- a/index.ts +++ b/index.ts @@ -20,6 +20,7 @@ export * as nip28 from './nip28.ts' export * as nip39 from './nip39.ts' export * as nip42 from './nip42.ts' export * as nip44 from './nip44.ts' +export * as nip47 from './nip47.ts' export * as nip57 from './nip57.ts' export * as nip98 from './nip98.ts' diff --git a/nip47.test.ts b/nip47.test.ts new file mode 100644 index 0000000..67b5183 --- /dev/null +++ b/nip47.test.ts @@ -0,0 +1,83 @@ +import {makeNwcRequestEvent, parseConnectionString} from './nip47' +import {Kind} from './event' +import {decrypt} from './nip04.ts' +import crypto from 'node:crypto' + +// @ts-ignore +// eslint-disable-next-line no-undef +globalThis.crypto = crypto + +describe('parseConnectionString', () => { + test('returns pubkey, relay, and secret if connection string is valid', () => { + const connectionString = + 'nostr+walletconnect:b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c' + const {pubkey, relay, secret} = parseConnectionString(connectionString) + + expect(pubkey).toBe( + 'b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4' + ) + expect(relay).toBe('wss://relay.damus.io') + expect(secret).toBe( + '71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c' + ) + }) + + test('throws an error if no pubkey in connection string', async () => { + const connectionString = + 'nostr+walletconnect:relay=wss%3A%2F%2Frelay.damus.io&secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c' + + expect(() => parseConnectionString(connectionString)).toThrow( + 'invalid connection string' + ) + }) + + test('throws an error if no relay in connection string', async () => { + const connectionString = + 'nostr+walletconnect:b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c' + + expect(() => parseConnectionString(connectionString)).toThrow( + 'invalid connection string' + ) + }) + + test('throws an error if no secret in connection string', async () => { + const connectionString = + 'nostr+walletconnect:b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io' + + expect(() => parseConnectionString(connectionString)).toThrow( + 'invalid connection string' + ) + }) +}) + +describe('makeNwcRequestEvent', () => { + test('returns a valid NWC request event', async () => { + const pubkey = + 'b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4' + const secret = + '71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c' + const invoice = + 'lnbc210n1pjdgyvupp5x43awdarnfd4mdlsklelux0nyckwfu5c708ykuet8vcjnjp3rnpqdqu2askcmr9wssx7e3q2dshgmmndp5scqzzsxqyz5vqsp52l7y9peq9pka3vd3j7aps7gjnalsmy46ndj2mlkz00dltjgqfumq9qyyssq5fasr5dxed8l4qjfnqq48a02jzss3asf8sly7sfaqtr9w3yu2q9spsxhghs3y9aqdf44zkrrg9jjjdg6amade4h0hulllkwk33eqpucp6d5jye' + const timeBefore = Date.now() / 1000 + const result = await makeNwcRequestEvent({ + pubkey, + secret, + invoice + }) + const timeAfter = Date.now() / 1000 + expect(result.kind).toBe(Kind.NwcRequest) + expect(result.created_at).toBeGreaterThan(timeBefore) + expect(result.created_at).toBeLessThan(timeAfter) + expect(await decrypt(secret, pubkey, result.content)).toEqual( + JSON.stringify({ + method: 'pay_invoice', + params: { + invoice + } + }) + ) + expect(result.tags).toEqual([['p', pubkey]]) + expect(result.id).toEqual(expect.any(String)) + expect(result.sig).toEqual(expect.any(String)) + }) +}) diff --git a/nip47.ts b/nip47.ts new file mode 100644 index 0000000..c6f608c --- /dev/null +++ b/nip47.ts @@ -0,0 +1,46 @@ +import {finishEvent} from './event.ts' +import {encrypt} from './nip04.ts' +import {Kind} from './event' + +export function parseConnectionString(connectionString: string) { + const {pathname, searchParams} = new URL(connectionString) + const pubkey = pathname + const relay = searchParams.get('relay') + const secret = searchParams.get('secret') + + if (!pubkey || !relay || !secret) { + throw new Error('invalid connection string') + } + + return {pubkey, relay, secret} +} + +export async function makeNwcRequestEvent({ + pubkey, + secret, + invoice +}: { + pubkey: string + secret: string + invoice: string +}) { + const content = { + method: 'pay_invoice', + params: { + invoice + } + } + const encryptedContent = await encrypt( + secret, + pubkey, + JSON.stringify(content) + ) + const eventTemplate = { + kind: Kind.NwcRequest, + created_at: Math.round(Date.now() / 1000), + content: encryptedContent, + tags: [['p', pubkey]] + } + + return finishEvent(eventTemplate, secret) +}