From 482c5affd426bcd6dd791028981daaabc8d73c02 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 20 Dec 2022 18:26:30 -0300 Subject: [PATCH] add nip19. --- index.ts | 1 + nip19.test.js | 36 +++++++++++++++ nip19.ts | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 4 files changed, 164 insertions(+) create mode 100644 nip19.test.js create mode 100644 nip19.ts diff --git a/index.ts b/index.ts index 2c1212b..aef3274 100644 --- a/index.ts +++ b/index.ts @@ -6,3 +6,4 @@ export * from './filter' export * as nip04 from './nip04' export * as nip05 from './nip05' export * as nip06 from './nip06' +export * as nip19 from './nip19' diff --git a/nip19.test.js b/nip19.test.js new file mode 100644 index 0000000..e3dc252 --- /dev/null +++ b/nip19.test.js @@ -0,0 +1,36 @@ +/* eslint-env jest */ + +const {nip19, generatePrivateKey, getPublicKey} = require('./lib/nostr.cjs') + +test('encode and decode nsec', () => { + let sk = generatePrivateKey() + let nsec = nip19.nsecEncode(sk) + expect(nsec).toMatch(/nsec1\w+/) + let {type, data} = nip19.decode(nsec) + expect(type).toEqual('nsec') + expect(data).toEqual(sk) +}) + +test('encode and decode npub', () => { + let pk = getPublicKey(generatePrivateKey()) + let npub = nip19.npubEncode(pk) + expect(npub).toMatch(/npub1\w+/) + let {type, data} = nip19.decode(npub) + expect(type).toEqual('npub') + expect(data).toEqual(pk) +}) + +test('encode and decode nprofile', () => { + let pk = getPublicKey(generatePrivateKey()) + let relays = [ + 'wss://relay.nostr.example.mydomain.example.com', + 'wss://nostr.banana.com' + ] + let nprofile = nip19.nprofileEncode({pubkey: pk, relays}) + expect(nprofile).toMatch(/nprofile1\w+/) + let {type, data} = nip19.decode(nprofile) + expect(type).toEqual('nprofile') + expect(data.pubkey).toEqual(pk) + expect(data.relays).toContain(relays[0]) + expect(data.relays).toContain(relays[1]) +}) diff --git a/nip19.ts b/nip19.ts new file mode 100644 index 0000000..be44135 --- /dev/null +++ b/nip19.ts @@ -0,0 +1,126 @@ +import * as secp256k1 from '@noble/secp256k1' +import {bech32} from 'bech32' + +type ProfilePointer = { + pubkey: string // hex + relays?: string[] +} + +type EventPointer = { + id: string // hex + relays?: string[] +} + +let utf8Decoder = new TextDecoder('utf-8') +let utf8Encoder = new TextEncoder() + +export function decode(nip19: string): { + type: string + data: ProfilePointer | EventPointer | string +} { + let {prefix, words} = bech32.decode(nip19, 1000) + let data = new Uint8Array(bech32.fromWords(words)) + + if (prefix === 'nprofile') { + let tlv = parseTLV(data) + if (!tlv[0]?.[0]) throw new Error('missing TLV 0 for nprofile') + if (tlv[0][0].length !== 32) throw new Error('TLV 0 should be 32 bytes') + + return { + type: 'nprofile', + data: { + pubkey: secp256k1.utils.bytesToHex(tlv[0][0]), + relays: tlv[1].map(d => utf8Decoder.decode(d)) + } + } + } + + if (prefix === 'nevent') { + let tlv = parseTLV(data) + if (!tlv[0]?.[0]) throw new Error('missing TLV 0 for nevent') + if (tlv[0][0].length !== 32) throw new Error('TLV 0 should be 32 bytes') + + return { + type: 'nevent', + data: { + id: secp256k1.utils.bytesToHex(tlv[0][0]), + relays: tlv[1].map(d => utf8Decoder.decode(d)) + } + } + } + + if (prefix === 'nsec' || prefix === 'npub' || prefix === 'note') { + return {type: prefix, data: secp256k1.utils.bytesToHex(data)} + } + + throw new Error(`unknown prefix ${prefix}`) +} + +type TLV = {[t: number]: Uint8Array[]} + +function parseTLV(data: Uint8Array): TLV { + let result: TLV = {} + let rest = data + while (rest.length > 0) { + let t = rest[0] + let l = rest[1] + let v = rest.slice(2, 2 + l) + rest = rest.slice(2 + l) + if (v.length < l) continue + result[t] = result[t] || [] + result[t].push(v) + } + return result +} + +export function nsecEncode(hex: string): string { + return encodeBytes('nsec', hex) +} + +export function npubEncode(hex: string): string { + return encodeBytes('npub', hex) +} + +export function noteEncode(hex: string): string { + return encodeBytes('note', hex) +} + +function encodeBytes(prefix: string, hex: string): string { + let data = secp256k1.utils.hexToBytes(hex) + let words = bech32.toWords(data) + return bech32.encode(prefix, words, 1000) +} + +export function nprofileEncode(profile: ProfilePointer): string { + let data = encodeTLV({ + 0: [secp256k1.utils.hexToBytes(profile.pubkey)], + 1: (profile.relays || []).map(url => utf8Encoder.encode(url)) + }) + let words = bech32.toWords(data) + return bech32.encode('nprofile', words, 1000) +} + +export function neventEncode(event: EventPointer): string { + let data = encodeTLV({ + 0: [secp256k1.utils.hexToBytes(event.id)], + 1: (event.relays || []).map(url => utf8Encoder.encode(url)) + }) + let words = bech32.toWords(data) + return bech32.encode('nevent', words, 1000) +} + +function encodeTLV(tlv: TLV): Uint8Array { + let entries: Uint8Array[] = [] + + Object.entries(tlv).forEach(([t, vs]) => { + vs.forEach(v => { + let entry = new Uint8Array(v.length + 2) + entry.set([parseInt(t)], 0) + entry.set([v.length], 1) + entry.set(v, 2) + entries.push(entry) + }) + }) + + return secp256k1.utils.concatBytes(...entries) +} diff --git a/package.json b/package.json index 675766d..a53b741 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@noble/secp256k1": "^1.7.0", "@scure/bip32": "^1.1.1", "@scure/bip39": "^1.1.0", + "bech32": "^2.0.0", "browserify-cipher": ">=1", "buffer": "^6.0.3", "websocket-polyfill": "^0.0.3"