mirror of
https://github.com/nbd-wtf/nostr-tools.git
synced 2025-12-08 16:28:49 +00:00
Compare commits
7 Commits
v1.0.0-alp
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
364c37cac5 | ||
|
|
385cdb4ac6 | ||
|
|
3f1025f551 | ||
|
|
482c5affd4 | ||
|
|
679ac0c133 | ||
|
|
b96159ad36 | ||
|
|
6dede4a688 |
17
.github/workflows/npm-publish.yml
vendored
17
.github/workflows/npm-publish.yml
vendored
@@ -1,8 +1,8 @@
|
||||
name: publish npm package
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
push:
|
||||
tags: [v*]
|
||||
|
||||
jobs:
|
||||
publish-npm:
|
||||
@@ -11,8 +11,11 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- run: npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
|
||||
node-version: 18
|
||||
- run: yarn --ignore-engines
|
||||
- run: node build.js
|
||||
- run: yarn test
|
||||
- uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
greater-version-only: true
|
||||
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -1,6 +1,8 @@
|
||||
name: test every commit
|
||||
on:
|
||||
- push
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -11,5 +13,5 @@ jobs:
|
||||
with:
|
||||
node-version: 18
|
||||
- run: yarn --ignore-engines
|
||||
- run: node build.cjs
|
||||
- run: node build.js
|
||||
- run: yarn test
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,7 +3,5 @@ dist
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
.envrc
|
||||
standalone
|
||||
cjs
|
||||
esm
|
||||
lib
|
||||
test.html
|
||||
|
||||
46
README.md
46
README.md
@@ -112,6 +112,50 @@ pub.on('failed', reason => {
|
||||
await relay.close()
|
||||
```
|
||||
|
||||
### Querying profile data from a NIP-05 address
|
||||
|
||||
```js
|
||||
import {nip05} from 'nostr-tools'
|
||||
|
||||
let profile = await nip05.queryProfile('jb55.com')
|
||||
console.log(profile.pubkey)
|
||||
// prints: 32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245
|
||||
console.log(profile.relays)
|
||||
// prints: [wss://relay.damus.io]
|
||||
|
||||
// on nodejs, install node-fetch@2 and call this first:
|
||||
nip05.useFetchImplementation(require('node-fetch'))
|
||||
```
|
||||
|
||||
### Encoding and decoding NIP-19 codes
|
||||
|
||||
```js
|
||||
import {nip19, generatePrivateKey, getPublicKey} from 'nostr-tools'
|
||||
|
||||
let sk = generatePrivateKey()
|
||||
let nsec = nip19.nsecEncode(sk)
|
||||
let {type, data} = nip19.decode(nsec)
|
||||
assert(type === 'nsec')
|
||||
assert(data === sk)
|
||||
|
||||
let pk = getPublicKey(generatePrivateKey())
|
||||
let npub = nip19.npubEncode(pk)
|
||||
let {type, data} = nip19.decode(npub)
|
||||
assert(type === 'npub')
|
||||
assert(data === pk)
|
||||
|
||||
let pk = getPublicKey(generatePrivateKey())
|
||||
let relays = [
|
||||
'wss://relay.nostr.example.mydomain.example.com',
|
||||
'wss://nostr.banana.com'
|
||||
]
|
||||
let nprofile = nip19.nprofileEncode({pubkey: pk, relays})
|
||||
let {type, data} = nip19.decode(nprofile)
|
||||
assert(type === 'nprofile')
|
||||
assert(data.pubkey === pk)
|
||||
assert(data.relays.length === 2)
|
||||
```
|
||||
|
||||
### Encrypting and decrypting direct messages
|
||||
|
||||
```js
|
||||
@@ -153,7 +197,7 @@ Please consult the tests or [the source code](https://github.com/fiatjaf/nostr-t
|
||||
### Using from the browser (if you don't want to use a bundler)
|
||||
|
||||
```html
|
||||
<script src="https://unpkg.com/nostr-tools/standalone/index.js"></script>
|
||||
<script src="https://unpkg.com/nostr-tools/lib/nostr.bundle.js"></script>
|
||||
<script>
|
||||
window.NostrTools.generatePrivateKey('...') // and so on
|
||||
</script>
|
||||
|
||||
@@ -15,17 +15,27 @@ let common = {
|
||||
}
|
||||
|
||||
esbuild
|
||||
.build({...common, outdir: 'esm/', format: 'esm', packages: 'external'})
|
||||
.build({
|
||||
...common,
|
||||
outfile: 'lib/nostr.esm.js',
|
||||
format: 'esm',
|
||||
packages: 'external'
|
||||
})
|
||||
.then(() => console.log('esm build success.'))
|
||||
|
||||
esbuild
|
||||
.build({...common, outdir: 'cjs/', format: 'cjs', packages: 'external'})
|
||||
.build({
|
||||
...common,
|
||||
outfile: 'lib/nostr.cjs.js',
|
||||
format: 'cjs',
|
||||
packages: 'external'
|
||||
})
|
||||
.then(() => console.log('cjs build success.'))
|
||||
|
||||
esbuild
|
||||
.build({
|
||||
...common,
|
||||
outdir: 'standalone/',
|
||||
outfile: 'lib/nostr.bundle.js',
|
||||
format: 'iife',
|
||||
globalName: 'NostrTools',
|
||||
define: {
|
||||
@@ -6,7 +6,7 @@ const {
|
||||
signEvent,
|
||||
getEventHash,
|
||||
getPublicKey
|
||||
} = require('./cjs')
|
||||
} = require('./lib/nostr.cjs')
|
||||
|
||||
const event = {
|
||||
id: 'd7dd5eb3ab747e16f8d0212d53032ea2a7cadef53837e5a6c66d42849fcb9027',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
const {matchFilters} = require('./cjs')
|
||||
const {matchFilters} = require('./lib/nostr.cjs')
|
||||
|
||||
test('test if filters match', () => {
|
||||
;[
|
||||
|
||||
1
index.ts
1
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'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
const {generatePrivateKey, getPublicKey} = require('./cjs')
|
||||
const {generatePrivateKey, getPublicKey} = require('./lib/nostr.cjs')
|
||||
|
||||
test('test private key generation', () => {
|
||||
expect(generatePrivateKey()).toMatch(/[a-f0-9]{64}/)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
const {nip04, getPublicKey, generatePrivateKey} = require('./cjs')
|
||||
const {nip04, getPublicKey, generatePrivateKey} = require('./lib/nostr.cjs')
|
||||
|
||||
test('encrypt and decrypt message', () => {
|
||||
let sk1 = generatePrivateKey()
|
||||
|
||||
20
nip05.test.js
Normal file
20
nip05.test.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
const fetch = require('node-fetch')
|
||||
const {nip05} = require('./lib/nostr.cjs')
|
||||
|
||||
test('fetch nip05 profiles', async () => {
|
||||
nip05.useFetchImplementation(fetch)
|
||||
|
||||
let p1 = await nip05.queryProfile('jb55.com')
|
||||
expect(p1.pubkey).toEqual(
|
||||
'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'
|
||||
)
|
||||
expect(p1.relays).toEqual(['wss://relay.damus.io'])
|
||||
|
||||
let p2 = await nip05.queryProfile('jb55@jb55.com')
|
||||
expect(p2.pubkey).toEqual(
|
||||
'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'
|
||||
)
|
||||
expect(p2.relays).toEqual(['wss://relay.damus.io'])
|
||||
})
|
||||
23
nip05.ts
23
nip05.ts
@@ -1,3 +1,5 @@
|
||||
import {ProfilePointer} from './nip19'
|
||||
|
||||
var _fetch = fetch
|
||||
|
||||
export function useFetchImplementation(fetchImplementation: any) {
|
||||
@@ -19,13 +21,28 @@ export async function searchDomain(
|
||||
}
|
||||
}
|
||||
|
||||
export async function queryName(fullname: string): Promise<string> {
|
||||
export async function queryProfile(
|
||||
fullname: string
|
||||
): Promise<ProfilePointer | null> {
|
||||
let [name, domain] = fullname.split('@')
|
||||
if (!domain) throw new Error('invalid identifier, must contain an @')
|
||||
|
||||
if (!domain) {
|
||||
// if there is no @, it is because it is just a domain, so assume the name is "_"
|
||||
domain = name
|
||||
name = '_'
|
||||
}
|
||||
|
||||
let res = await (
|
||||
await _fetch(`https://${domain}/.well-known/nostr.json?name=${name}`)
|
||||
).json()
|
||||
|
||||
return res.names && res.names[name]
|
||||
if (!res?.names?.[name]) return null
|
||||
|
||||
let pubkey = res.names[name] as string
|
||||
let relays = (res.relays?.[pubkey] || []) as string[] // nip35
|
||||
|
||||
return {
|
||||
pubkey,
|
||||
relays
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
||||
|
||||
export type ProfilePointer = {
|
||||
pubkey: string // hex
|
||||
relays?: string[]
|
||||
}
|
||||
|
||||
export 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)
|
||||
}
|
||||
12
package.json
12
package.json
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"name": "nostr-tools",
|
||||
"version": "1.0.0-alpha",
|
||||
"version": "1.0.0-beta",
|
||||
"description": "Tools for making a Nostr client.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/fiatjaf/nostr-tools.git"
|
||||
},
|
||||
"main": "cjs/index.js",
|
||||
"module": "esm/index.js",
|
||||
"main": "lib/nostr.cjs.js",
|
||||
"module": "lib/nostr.esm.js",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^0.5.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"
|
||||
@@ -35,13 +36,14 @@
|
||||
"esm-loader-typescript": "^1.0.1",
|
||||
"events": "^3.3.0",
|
||||
"jest": "^29.3.1",
|
||||
"node-fetch": "2",
|
||||
"ts-jest": "^29.0.3",
|
||||
"tsd": "^0.22.0",
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node build.cjs",
|
||||
"pretest": "node build.cjs",
|
||||
"build": "node build.js",
|
||||
"pretest": "node build.js",
|
||||
"test": "jest"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ const {
|
||||
getPublicKey,
|
||||
getEventHash,
|
||||
signEvent
|
||||
} = require('./cjs')
|
||||
} = require('./lib/nostr.cjs')
|
||||
|
||||
describe('relay interaction', () => {
|
||||
let relay = relayInit('wss://nostr-pub.wellorder.net/')
|
||||
let relay = relayInit('wss://nostr-pub.semisol.dev/')
|
||||
|
||||
beforeAll(() => {
|
||||
relay.connect()
|
||||
|
||||
Reference in New Issue
Block a user