mirror of
https://github.com/nbd-wtf/nostr-tools.git
synced 2025-12-14 19:08:50 +00:00
add nip19.
This commit is contained in:
1
index.ts
1
index.ts
@@ -6,3 +6,4 @@ export * from './filter'
|
|||||||
export * as nip04 from './nip04'
|
export * as nip04 from './nip04'
|
||||||
export * as nip05 from './nip05'
|
export * as nip05 from './nip05'
|
||||||
export * as nip06 from './nip06'
|
export * as nip06 from './nip06'
|
||||||
|
export * as nip19 from './nip19'
|
||||||
|
|||||||
36
nip19.test.js
Normal file
36
nip19.test.js
Normal file
@@ -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])
|
||||||
|
})
|
||||||
126
nip19.ts
Normal file
126
nip19.ts
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
"@noble/secp256k1": "^1.7.0",
|
"@noble/secp256k1": "^1.7.0",
|
||||||
"@scure/bip32": "^1.1.1",
|
"@scure/bip32": "^1.1.1",
|
||||||
"@scure/bip39": "^1.1.0",
|
"@scure/bip39": "^1.1.0",
|
||||||
|
"bech32": "^2.0.0",
|
||||||
"browserify-cipher": ">=1",
|
"browserify-cipher": ">=1",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"websocket-polyfill": "^0.0.3"
|
"websocket-polyfill": "^0.0.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user