diff --git a/nip57.test.js b/nip57.test.js new file mode 100644 index 0000000..6acc084 --- /dev/null +++ b/nip57.test.js @@ -0,0 +1,335 @@ +const {bech32} = require('@scure/base') +const { + nip57, + generatePrivateKey, + getPublicKey, + finishEvent +} = require('./lib/nostr.cjs') + +describe('getZapEndpoint', () => { + test('returns null if neither lud06 nor lud16 is present', async () => { + const metadata = {content: '{}'} + const result = await nip57.getZapEndpoint(metadata) + + expect(result).toBeNull() + }) + + test('returns null if fetch fails', async () => { + const fetchImplementation = jest.fn(() => Promise.reject(new Error())) + nip57.useFetchImplementation(fetchImplementation) + + const metadata = {content: '{"lud16": "name@domain"}'} + const result = await nip57.getZapEndpoint(metadata) + + expect(result).toBeNull() + expect(fetchImplementation).toHaveBeenCalledWith( + 'https://domain/.well-known/lnurlp/name' + ) + }) + + test('returns null if the response does not allow Nostr payments', async () => { + const fetchImplementation = jest.fn(() => + Promise.resolve({json: () => ({allowsNostr: false})}) + ) + nip57.useFetchImplementation(fetchImplementation) + + const metadata = {content: '{"lud16": "name@domain"}'} + const result = await nip57.getZapEndpoint(metadata) + + expect(result).toBeNull() + expect(fetchImplementation).toHaveBeenCalledWith( + 'https://domain/.well-known/lnurlp/name' + ) + }) + + test('returns the callback URL if the response allows Nostr payments', async () => { + const fetchImplementation = jest.fn(() => + Promise.resolve({ + json: () => ({ + allowsNostr: true, + nostrPubkey: 'pubkey', + callback: 'callback' + }) + }) + ) + nip57.useFetchImplementation(fetchImplementation) + + const metadata = {content: '{"lud16": "name@domain"}'} + const result = await nip57.getZapEndpoint(metadata) + + expect(result).toBe('callback') + expect(fetchImplementation).toHaveBeenCalledWith( + 'https://domain/.well-known/lnurlp/name' + ) + }) +}) + +describe('makeZapRequest', () => { + test('throws an error if amount is not given', () => { + expect(() => + nip57.makeZapRequest({ + profile: 'profile', + event: null, + relays: [], + comment: '' + }) + ).toThrow() + }) + + test('throws an error if profile is not given', () => { + expect(() => + nip57.makeZapRequest({ + event: null, + amount: 100, + relays: [], + comment: '' + }) + ).toThrow() + }) + + test('returns a valid Zap request', () => { + const result = nip57.makeZapRequest({ + profile: 'profile', + event: 'event', + amount: 100, + relays: ['relay1', 'relay2'], + comment: 'comment' + }) + expect(result.kind).toBe(9734) + expect(result.created_at).toBeCloseTo(Date.now() / 1000, 0) + expect(result.content).toBe('comment') + expect(result.tags).toEqual( + expect.arrayContaining([ + ['p', 'profile'], + ['amount', '100'], + ['relays', 'relay1', 'relay2'] + ]) + ) + expect(result.tags).toContainEqual(['e', 'event']) + }) +}) + +describe('validateZapRequest', () => { + test('returns an error message for invalid JSON', () => { + expect(nip57.validateZapRequest('invalid JSON')).toBe( + 'Invalid zap request JSON.' + ) + }) + + test('returns an error message if the Zap request is not a valid Nostr event', () => { + const zapRequest = { + kind: 1234, + created_at: Date.now() / 1000, + content: 'content', + tags: [ + ['p', 'profile'], + ['amount', '100'], + ['relays', 'relay1', 'relay2'] + ] + } + + expect(nip57.validateZapRequest(JSON.stringify(zapRequest))).toBe( + 'Zap request is not a valid Nostr event.' + ) + }) + + test('returns an error message if the signature on the Zap request is invalid', () => { + const privateKey = generatePrivateKey() + const publicKey = getPublicKey(privateKey) + + const zapRequest = { + pubkey: publicKey, + kind: 9734, + created_at: Date.now() / 1000, + content: 'content', + tags: [ + ['p', publicKey], + ['amount', '100'], + ['relays', 'relay1', 'relay2'] + ] + } + + expect(nip57.validateZapRequest(JSON.stringify(zapRequest))).toBe( + 'Invalid signature on zap request.' + ) + }) + + test('returns an error message if the Zap request does not have a "p" tag', () => { + const privateKey = generatePrivateKey() + + const zapRequest = finishEvent( + { + kind: 9734, + created_at: Date.now() / 1000, + content: 'content', + tags: [ + ['amount', '100'], + ['relays', 'relay1', 'relay2'] + ] + }, + privateKey + ) + + expect(nip57.validateZapRequest(JSON.stringify(zapRequest))).toBe( + "Zap request doesn't have a 'p' tag." + ) + }) + + test('returns an error message if the "p" tag on the Zap request is not valid hex', () => { + const privateKey = generatePrivateKey() + + const zapRequest = finishEvent( + { + kind: 9734, + created_at: Date.now() / 1000, + content: 'content', + tags: [ + ['p', 'invalid hex'], + ['amount', '100'], + ['relays', 'relay1', 'relay2'] + ] + }, + privateKey + ) + + expect(nip57.validateZapRequest(JSON.stringify(zapRequest))).toBe( + "Zap request 'p' tag is not valid hex." + ) + }) + + test('returns an error message if the "e" tag on the Zap request is not valid hex', () => { + const privateKey = generatePrivateKey() + const publicKey = getPublicKey(privateKey) + + const zapRequest = finishEvent( + { + kind: 9734, + created_at: Date.now() / 1000, + content: 'content', + tags: [ + ['p', publicKey], + ['e', 'invalid hex'], + ['amount', '100'], + ['relays', 'relay1', 'relay2'] + ] + }, + privateKey + ) + + expect(nip57.validateZapRequest(JSON.stringify(zapRequest))).toBe( + "Zap request 'e' tag is not valid hex." + ) + }) + + test('returns an error message if the Zap request does not have a relays tag', () => { + const privateKey = generatePrivateKey() + const publicKey = getPublicKey(privateKey) + + const zapRequest = finishEvent( + { + kind: 9734, + created_at: Date.now() / 1000, + content: 'content', + tags: [ + ['p', publicKey], + ['amount', '100'] + ] + }, + privateKey + ) + + expect(nip57.validateZapRequest(JSON.stringify(zapRequest))).toBe( + "Zap request doesn't have a 'relays' tag." + ) + }) + + test('returns null for a valid Zap request', () => { + const privateKey = generatePrivateKey() + const publicKey = getPublicKey(privateKey) + + const zapRequest = finishEvent( + { + kind: 9734, + created_at: Date.now() / 1000, + content: 'content', + tags: [ + ['p', publicKey], + ['amount', '100'], + ['relays', 'relay1', 'relay2'] + ] + }, + privateKey + ) + + expect(nip57.validateZapRequest(JSON.stringify(zapRequest))).toBeNull() + }) +}) + +describe('makeZapReceipt', () => { + test('returns a valid Zap receipt with a preimage', () => { + const privateKey = generatePrivateKey() + const publicKey = getPublicKey(privateKey) + + const zapRequest = JSON.stringify( + finishEvent( + { + kind: 9734, + created_at: Date.now() / 1000, + content: 'content', + tags: [ + ['p', publicKey], + ['amount', '100'], + ['relays', 'relay1', 'relay2'] + ] + }, + privateKey + ) + ) + const preimage = 'preimage' + const bolt11 = 'bolt11' + const paidAt = new Date() + + const result = nip57.makeZapReceipt({zapRequest, preimage, bolt11, paidAt}) + + expect(result.kind).toBe(9735) + expect(result.created_at).toBeCloseTo(paidAt.getTime() / 1000, 0) + expect(result.content).toBe('') + expect(result.tags).toContainEqual(['bolt11', bolt11]) + expect(result.tags).toContainEqual(['description', zapRequest]) + expect(result.tags).toContainEqual(['p', publicKey]) + expect(result.tags).toContainEqual(['preimage', preimage]) + }) + + test('returns a valid Zap receipt without a preimage', () => { + const privateKey = generatePrivateKey() + const publicKey = getPublicKey(privateKey) + + const zapRequest = JSON.stringify( + finishEvent( + { + kind: 9734, + created_at: Date.now() / 1000, + content: 'content', + tags: [ + ['p', publicKey], + ['amount', '100'], + ['relays', 'relay1', 'relay2'] + ] + }, + privateKey + ) + ) + const bolt11 = 'bolt11' + const paidAt = new Date() + + const result = nip57.makeZapReceipt({zapRequest, bolt11, paidAt}) + + expect(result.kind).toBe(9735) + expect(result.created_at).toBeCloseTo(paidAt.getTime() / 1000, 0) + expect(result.content).toBe('') + expect(result.tags).toContainEqual(['bolt11', bolt11]) + expect(result.tags).toContainEqual(['description', zapRequest]) + expect(result.tags).toContainEqual(['p', publicKey]) + expect(result.tags).not.toContain('preimage') + }) +})