mirror of
https://github.com/nbd-wtf/nostr-tools.git
synced 2025-12-08 16:28:49 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87a91c2daf | ||
|
|
4f1dc9ef1c | ||
|
|
faa1a9d556 | ||
|
|
97d838f254 | ||
|
|
260400b24d | ||
|
|
6e5ab34a54 | ||
|
|
9562c408b3 | ||
|
|
4f4de458e9 | ||
|
|
88454de628 | ||
|
|
9f5984d78d | ||
|
|
80df21d47f | ||
|
|
296e99d2a4 | ||
|
|
1cd9847ad5 | ||
|
|
fa31fdca78 | ||
|
|
5876acd67a | ||
|
|
44efd49bc0 |
19
.devcontainer/Dockerfile
Executable file
19
.devcontainer/Dockerfile
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
FROM node:20
|
||||||
|
|
||||||
|
RUN npm install typescript -g
|
||||||
|
|
||||||
|
# Install bun
|
||||||
|
RUN curl -fsSL https://bun.sh/install | bash
|
||||||
|
|
||||||
|
# Install just
|
||||||
|
WORKDIR /usr/bin
|
||||||
|
RUN wget https://github.com/casey/just/releases/download/1.26.0/just-1.26.0-x86_64-unknown-linux-musl.tar.gz
|
||||||
|
RUN tar -xzf just-1.26.0-x86_64-unknown-linux-musl.tar.gz
|
||||||
|
RUN chmod +x ./just
|
||||||
|
RUN rm just-1.26.0-x86_64-unknown-linux-musl.tar.gz
|
||||||
|
|
||||||
|
WORKDIR /nostr-tools
|
||||||
|
ENV LANG C.UTF-8
|
||||||
|
|
||||||
|
# The run the start script
|
||||||
|
CMD [ "/bin/bash" ]
|
||||||
19
.devcontainer/devcontainer.json
Executable file
19
.devcontainer/devcontainer.json
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "Nostr Tools",
|
||||||
|
"dockerComposeFile": [
|
||||||
|
"docker-compose.yml"
|
||||||
|
],
|
||||||
|
"service": "nostr-tools-dev",
|
||||||
|
"workspaceFolder": "/nostr-tools",
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"ms-vscode.vscode-typescript-next",
|
||||||
|
"eamodio.gitlens",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"manishsencha.readme-preview",
|
||||||
|
"wix.vscode-import-cost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
.devcontainer/docker-compose.yml
Executable file
13
.devcontainer/docker-compose.yml
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
services:
|
||||||
|
nostr-tools-dev:
|
||||||
|
image: nostr-tools-dev
|
||||||
|
container_name: nostr-tools-dev
|
||||||
|
build:
|
||||||
|
context: ../.
|
||||||
|
dockerfile: ./.devcontainer/Dockerfile
|
||||||
|
working_dir: /nostr-tools
|
||||||
|
volumes:
|
||||||
|
- ..:/nostr-tools:cached
|
||||||
|
tty: true
|
||||||
46
README.md
46
README.md
@@ -30,7 +30,7 @@ To get the secret key in hex format, use
|
|||||||
```js
|
```js
|
||||||
import { bytesToHex, hexToBytes } from '@noble/hashes/utils' // already an installed dependency
|
import { bytesToHex, hexToBytes } from '@noble/hashes/utils' // already an installed dependency
|
||||||
|
|
||||||
let skHex = bytesToHex(sk)
|
let skHex = bytesToHex(sk)
|
||||||
let backToBytes = hexToBytes(skHex)
|
let backToBytes = hexToBytes(skHex)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -104,8 +104,11 @@ relay.close()
|
|||||||
To use this on Node.js you first must install `ws` and call something like this:
|
To use this on Node.js you first must install `ws` and call something like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { useWebSocketImplementation } from 'nostr-tools/relay'
|
import { useWebSocketImplementation } from 'nostr-tools/pool'
|
||||||
useWebSocketImplementation(require('ws'))
|
// or import { useWebSocketImplementation } from 'nostr-tools/relay' if you're using the Relay directly
|
||||||
|
|
||||||
|
import WebSocket from 'ws'
|
||||||
|
useWebSocketImplementation(WebSocket)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Interacting with multiple relays
|
### Interacting with multiple relays
|
||||||
@@ -183,6 +186,43 @@ import { useFetchImplementation } from 'nostr-tools/nip05'
|
|||||||
useFetchImplementation(require('node-fetch'))
|
useFetchImplementation(require('node-fetch'))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Including NIP-07 types
|
||||||
|
```js
|
||||||
|
import type { WindowNostr } from 'nostr-tools/nip07'
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
nostr?: WindowNostr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Generating NIP-06 keys
|
||||||
|
```js
|
||||||
|
import {
|
||||||
|
privateKeyFromSeedWords,
|
||||||
|
accountFromSeedWords,
|
||||||
|
extendedKeysFromSeedWords,
|
||||||
|
accountFromExtendedKey
|
||||||
|
} from 'nostr-tools/nip06'
|
||||||
|
|
||||||
|
const mnemonic = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong'
|
||||||
|
const passphrase = '123' // optional
|
||||||
|
const accountIndex = 0
|
||||||
|
const sk0 = privateKeyFromSeedWords(mnemonic, passphrase, accountIndex)
|
||||||
|
|
||||||
|
const { privateKey: sk1, publicKey: pk1 } = accountFromSeedWords(mnemonic, passphrase, accountIndex)
|
||||||
|
|
||||||
|
const extendedAccountIndex = 0
|
||||||
|
|
||||||
|
const { privateExtendedKey, publicExtendedKey } = extendedKeysFromSeedWords(mnemonic, passphrase, extendedAccountIndex)
|
||||||
|
|
||||||
|
const { privateKey: sk2, publicKey: pk2 } = accountFromExtendedKey(privateExtendedKey)
|
||||||
|
|
||||||
|
const { publicKey: pk3 } = accountFromExtendedKey(publicExtendedKey)
|
||||||
|
```
|
||||||
|
|
||||||
### Encoding and decoding NIP-19 codes
|
### Encoding and decoding NIP-19 codes
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { AbstractRelay as AbstractRelay, SubscriptionParams, Subscription } from './abstract-relay.ts'
|
/* global WebSocket */
|
||||||
|
|
||||||
|
import {
|
||||||
|
AbstractRelay as AbstractRelay,
|
||||||
|
SubscriptionParams,
|
||||||
|
Subscription,
|
||||||
|
type AbstractRelayConstructorOptions,
|
||||||
|
} from './abstract-relay.ts'
|
||||||
import { normalizeURL } from './utils.ts'
|
import { normalizeURL } from './utils.ts'
|
||||||
|
|
||||||
import type { Event, Nostr } from './core.ts'
|
import type { Event, Nostr } from './core.ts'
|
||||||
@@ -7,6 +14,8 @@ import { alwaysTrue } from './helpers.ts'
|
|||||||
|
|
||||||
export type SubCloser = { close: () => void }
|
export type SubCloser = { close: () => void }
|
||||||
|
|
||||||
|
export type AbstractPoolConstructorOptions = AbstractRelayConstructorOptions & {}
|
||||||
|
|
||||||
export type SubscribeManyParams = Omit<SubscriptionParams, 'onclose' | 'id'> & {
|
export type SubscribeManyParams = Omit<SubscriptionParams, 'onclose' | 'id'> & {
|
||||||
maxWait?: number
|
maxWait?: number
|
||||||
onclose?: (reasons: string[]) => void
|
onclose?: (reasons: string[]) => void
|
||||||
@@ -21,8 +30,11 @@ export class AbstractSimplePool {
|
|||||||
public verifyEvent: Nostr['verifyEvent']
|
public verifyEvent: Nostr['verifyEvent']
|
||||||
public trustedRelayURLs: Set<string> = new Set()
|
public trustedRelayURLs: Set<string> = new Set()
|
||||||
|
|
||||||
constructor(opts: { verifyEvent: Nostr['verifyEvent'] }) {
|
private _WebSocket?: typeof WebSocket
|
||||||
|
|
||||||
|
constructor(opts: AbstractPoolConstructorOptions) {
|
||||||
this.verifyEvent = opts.verifyEvent
|
this.verifyEvent = opts.verifyEvent
|
||||||
|
this._WebSocket = opts.websocketImplementation
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureRelay(url: string, params?: { connectionTimeout?: number }): Promise<AbstractRelay> {
|
async ensureRelay(url: string, params?: { connectionTimeout?: number }): Promise<AbstractRelay> {
|
||||||
@@ -32,6 +44,7 @@ export class AbstractSimplePool {
|
|||||||
if (!relay) {
|
if (!relay) {
|
||||||
relay = new AbstractRelay(url, {
|
relay = new AbstractRelay(url, {
|
||||||
verifyEvent: this.trustedRelayURLs.has(url) ? alwaysTrue : this.verifyEvent,
|
verifyEvent: this.trustedRelayURLs.has(url) ? alwaysTrue : this.verifyEvent,
|
||||||
|
websocketImplementation: this._WebSocket,
|
||||||
})
|
})
|
||||||
if (params?.connectionTimeout) relay.connectionTimeout = params.connectionTimeout
|
if (params?.connectionTimeout) relay.connectionTimeout = params.connectionTimeout
|
||||||
this.relays.set(url, relay)
|
this.relays.set(url, relay)
|
||||||
|
|||||||
@@ -7,14 +7,9 @@ import { Queue, normalizeURL } from './utils.ts'
|
|||||||
import { makeAuthEvent } from './nip42.ts'
|
import { makeAuthEvent } from './nip42.ts'
|
||||||
import { yieldThread } from './helpers.ts'
|
import { yieldThread } from './helpers.ts'
|
||||||
|
|
||||||
var _WebSocket: typeof WebSocket
|
export type AbstractRelayConstructorOptions = {
|
||||||
|
verifyEvent: Nostr['verifyEvent']
|
||||||
try {
|
websocketImplementation?: typeof WebSocket
|
||||||
_WebSocket = WebSocket
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
export function useWebSocketImplementation(websocketImplementation: any) {
|
|
||||||
_WebSocket = websocketImplementation
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AbstractRelay {
|
export class AbstractRelay {
|
||||||
@@ -42,12 +37,15 @@ export class AbstractRelay {
|
|||||||
private serial: number = 0
|
private serial: number = 0
|
||||||
private verifyEvent: Nostr['verifyEvent']
|
private verifyEvent: Nostr['verifyEvent']
|
||||||
|
|
||||||
constructor(url: string, opts: { verifyEvent: Nostr['verifyEvent'] }) {
|
private _WebSocket: typeof WebSocket
|
||||||
|
|
||||||
|
constructor(url: string, opts: AbstractRelayConstructorOptions) {
|
||||||
this.url = normalizeURL(url)
|
this.url = normalizeURL(url)
|
||||||
this.verifyEvent = opts.verifyEvent
|
this.verifyEvent = opts.verifyEvent
|
||||||
|
this._WebSocket = opts.websocketImplementation || WebSocket
|
||||||
}
|
}
|
||||||
|
|
||||||
static async connect(url: string, opts: { verifyEvent: Nostr['verifyEvent'] }): Promise<AbstractRelay> {
|
static async connect(url: string, opts: AbstractRelayConstructorOptions): Promise<AbstractRelay> {
|
||||||
const relay = new AbstractRelay(url, opts)
|
const relay = new AbstractRelay(url, opts)
|
||||||
await relay.connect()
|
await relay.connect()
|
||||||
return relay
|
return relay
|
||||||
@@ -87,7 +85,7 @@ export class AbstractRelay {
|
|||||||
}, this.connectionTimeout)
|
}, this.connectionTimeout)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.ws = new _WebSocket(this.url)
|
this.ws = new this._WebSocket(this.url)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(err)
|
reject(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ describe('Filter', () => {
|
|||||||
until: 200,
|
until: 200,
|
||||||
'#tag': ['value'],
|
'#tag': ['value'],
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = buildEvent({
|
const event = buildEvent({
|
||||||
id: '123',
|
id: '123',
|
||||||
kind: 1,
|
kind: 1,
|
||||||
@@ -21,39 +20,21 @@ describe('Filter', () => {
|
|||||||
created_at: 150,
|
created_at: 150,
|
||||||
tags: [['tag', 'value']],
|
tags: [['tag', 'value']],
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = matchFilter(filter, event)
|
const result = matchFilter(filter, event)
|
||||||
|
|
||||||
expect(result).toEqual(true)
|
expect(result).toEqual(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should return false when the event id is not in the filter', () => {
|
test('should return false when the event id is not in the filter', () => {
|
||||||
const filter = { ids: ['123', '456'] }
|
const filter = { ids: ['123', '456'] }
|
||||||
|
|
||||||
const event = buildEvent({ id: '789' })
|
const event = buildEvent({ id: '789' })
|
||||||
|
|
||||||
const result = matchFilter(filter, event)
|
const result = matchFilter(filter, event)
|
||||||
|
|
||||||
expect(result).toEqual(false)
|
expect(result).toEqual(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should return true when the event id starts with a prefix', () => {
|
|
||||||
const filter = { ids: ['22', '00'] }
|
|
||||||
|
|
||||||
const event = buildEvent({ id: '001' })
|
|
||||||
|
|
||||||
const result = matchFilter(filter, event)
|
|
||||||
|
|
||||||
expect(result).toEqual(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should return false when the event kind is not in the filter', () => {
|
test('should return false when the event kind is not in the filter', () => {
|
||||||
const filter = { kinds: [1, 2, 3] }
|
const filter = { kinds: [1, 2, 3] }
|
||||||
|
|
||||||
const event = buildEvent({ kind: 4 })
|
const event = buildEvent({ kind: 4 })
|
||||||
|
|
||||||
const result = matchFilter(filter, event)
|
const result = matchFilter(filter, event)
|
||||||
|
|
||||||
expect(result).toEqual(false)
|
expect(result).toEqual(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -154,25 +135,8 @@ describe('Filter', () => {
|
|||||||
{ ids: ['456'], kinds: [2], authors: ['def'] },
|
{ ids: ['456'], kinds: [2], authors: ['def'] },
|
||||||
{ ids: ['789'], kinds: [3], authors: ['ghi'] },
|
{ ids: ['789'], kinds: [3], authors: ['ghi'] },
|
||||||
]
|
]
|
||||||
|
|
||||||
const event = buildEvent({ id: '789', kind: 3, pubkey: 'ghi' })
|
const event = buildEvent({ id: '789', kind: 3, pubkey: 'ghi' })
|
||||||
|
|
||||||
const result = matchFilters(filters, event)
|
const result = matchFilters(filters, event)
|
||||||
|
|
||||||
expect(result).toEqual(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should return true when at least one prefix matches the event', () => {
|
|
||||||
const filters = [
|
|
||||||
{ ids: ['1'], kinds: [1], authors: ['a'] },
|
|
||||||
{ ids: ['4'], kinds: [2], authors: ['d'] },
|
|
||||||
{ ids: ['9'], kinds: [3], authors: ['g'] },
|
|
||||||
]
|
|
||||||
|
|
||||||
const event = buildEvent({ id: '987', kind: 3, pubkey: 'ghi' })
|
|
||||||
|
|
||||||
const result = matchFilters(filters, event)
|
|
||||||
|
|
||||||
expect(result).toEqual(true)
|
expect(result).toEqual(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -201,11 +165,8 @@ describe('Filter', () => {
|
|||||||
{ ids: ['456'], kinds: [2], authors: ['def'] },
|
{ ids: ['456'], kinds: [2], authors: ['def'] },
|
||||||
{ ids: ['789'], kinds: [3], authors: ['ghi'] },
|
{ ids: ['789'], kinds: [3], authors: ['ghi'] },
|
||||||
]
|
]
|
||||||
|
|
||||||
const event = buildEvent({ id: '100', kind: 4, pubkey: 'jkl' })
|
const event = buildEvent({ id: '100', kind: 4, pubkey: 'jkl' })
|
||||||
|
|
||||||
const result = matchFilters(filters, event)
|
const result = matchFilters(filters, event)
|
||||||
|
|
||||||
expect(result).toEqual(false)
|
expect(result).toEqual(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -221,9 +182,7 @@ describe('Filter', () => {
|
|||||||
pubkey: 'def',
|
pubkey: 'def',
|
||||||
created_at: 200,
|
created_at: 200,
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = matchFilters(filters, event)
|
const result = matchFilters(filters, event)
|
||||||
|
|
||||||
expect(result).toEqual(false)
|
expect(result).toEqual(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
12
filter.ts
12
filter.ts
@@ -14,15 +14,13 @@ export type Filter = {
|
|||||||
|
|
||||||
export function matchFilter(filter: Filter, event: Event): boolean {
|
export function matchFilter(filter: Filter, event: Event): boolean {
|
||||||
if (filter.ids && filter.ids.indexOf(event.id) === -1) {
|
if (filter.ids && filter.ids.indexOf(event.id) === -1) {
|
||||||
if (!filter.ids.some(prefix => event.id.startsWith(prefix))) {
|
return false
|
||||||
return false
|
}
|
||||||
}
|
if (filter.kinds && filter.kinds.indexOf(event.kind) === -1) {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
if (filter.kinds && filter.kinds.indexOf(event.kind) === -1) return false
|
|
||||||
if (filter.authors && filter.authors.indexOf(event.pubkey) === -1) {
|
if (filter.authors && filter.authors.indexOf(event.pubkey) === -1) {
|
||||||
if (!filter.authors.some(prefix => event.pubkey.startsWith(prefix))) {
|
return false
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let f in filter) {
|
for (let f in filter) {
|
||||||
|
|||||||
4
index.ts
4
index.ts
@@ -1,7 +1,7 @@
|
|||||||
export * from './pure.ts'
|
export * from './pure.ts'
|
||||||
export * from './relay.ts'
|
export { Relay } from './relay.ts'
|
||||||
export * from './filter.ts'
|
export * from './filter.ts'
|
||||||
export * from './pool.ts'
|
export { SimplePool } from './pool.ts'
|
||||||
export * from './references.ts'
|
export * from './references.ts'
|
||||||
|
|
||||||
export * as nip04 from './nip04.ts'
|
export * as nip04 from './nip04.ts'
|
||||||
|
|||||||
1
jsr.json
1
jsr.json
@@ -16,6 +16,7 @@
|
|||||||
"./nip04": "./nip04.ts",
|
"./nip04": "./nip04.ts",
|
||||||
"./nip05": "./nip05.ts",
|
"./nip05": "./nip05.ts",
|
||||||
"./nip06": "./nip06.ts",
|
"./nip06": "./nip06.ts",
|
||||||
|
"./nip07": "./nip07.ts",
|
||||||
"./nip10": "./nip10.ts",
|
"./nip10": "./nip10.ts",
|
||||||
"./nip11": "./nip11.ts",
|
"./nip11": "./nip11.ts",
|
||||||
"./nip13": "./nip13.ts",
|
"./nip13": "./nip13.ts",
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { test, expect } from 'bun:test'
|
import { test, expect } from 'bun:test'
|
||||||
import { privateKeyFromSeedWords } from './nip06.ts'
|
import {
|
||||||
|
privateKeyFromSeedWords,
|
||||||
|
accountFromSeedWords,
|
||||||
|
extendedKeysFromSeedWords,
|
||||||
|
accountFromExtendedKey,
|
||||||
|
} from './nip06.ts'
|
||||||
|
|
||||||
test('generate private key from a mnemonic', async () => {
|
test('generate private key from a mnemonic', async () => {
|
||||||
const mnemonic = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong'
|
const mnemonic = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong'
|
||||||
@@ -26,3 +31,46 @@ test('generate private key for account 1 from a mnemonic and passphrase', async
|
|||||||
const privateKey = privateKeyFromSeedWords(mnemonic, passphrase, 1)
|
const privateKey = privateKeyFromSeedWords(mnemonic, passphrase, 1)
|
||||||
expect(privateKey).toEqual('2e0f7bd9e3c3ebcdff1a90fb49c913477e7c055eba1a415d571b6a8c714c7135')
|
expect(privateKey).toEqual('2e0f7bd9e3c3ebcdff1a90fb49c913477e7c055eba1a415d571b6a8c714c7135')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('generate private and public key for account 1 from a mnemonic and passphrase', async () => {
|
||||||
|
const mnemonic = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong'
|
||||||
|
const passphrase = '123'
|
||||||
|
const { privateKey, publicKey } = accountFromSeedWords(mnemonic, passphrase, 1)
|
||||||
|
expect(privateKey).toEqual('2e0f7bd9e3c3ebcdff1a90fb49c913477e7c055eba1a415d571b6a8c714c7135')
|
||||||
|
expect(publicKey).toEqual('13f55f4f01576570ea342eb7d2b611f9dc78f8dc601aeb512011e4e73b90cf0a')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('generate extended keys from mnemonic', () => {
|
||||||
|
const mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
|
||||||
|
const passphrase = ''
|
||||||
|
const extendedAccountIndex = 0
|
||||||
|
const { privateExtendedKey, publicExtendedKey } = extendedKeysFromSeedWords(
|
||||||
|
mnemonic,
|
||||||
|
passphrase,
|
||||||
|
extendedAccountIndex,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(privateExtendedKey).toBe(
|
||||||
|
'xprv9z78fizET65qsCaRr1MSutTSGk1fcKfSt1sBqmuWShtkjRJJ4WCKcSnha6EmgNzFSsyom3MWtydHyPtJtSLZQUtictVQtM2vkPcguh6TQCH',
|
||||||
|
)
|
||||||
|
expect(publicExtendedKey).toBe(
|
||||||
|
'xpub6D6V5EX8HTe95getx2tTH2QApmrA1nPJFEnneAK813RjcDdSc3WaAF7BRNpTF7o7zXjVm3DD3VMX66jhQ7wLaZ9sS6NzyfiwfzqDZbxvpDN',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('generate account from extended private key', () => {
|
||||||
|
const xprv =
|
||||||
|
'xprv9z78fizET65qsCaRr1MSutTSGk1fcKfSt1sBqmuWShtkjRJJ4WCKcSnha6EmgNzFSsyom3MWtydHyPtJtSLZQUtictVQtM2vkPcguh6TQCH'
|
||||||
|
const { privateKey, publicKey } = accountFromExtendedKey(xprv)
|
||||||
|
|
||||||
|
expect(privateKey).toBe('5f29af3b9676180290e77a4efad265c4c2ff28a5302461f73597fda26bb25731')
|
||||||
|
expect(publicKey).toBe('e8bcf3823669444d0b49ad45d65088635d9fd8500a75b5f20b59abefa56a144f')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('generate account from extended public key', () => {
|
||||||
|
const xpub =
|
||||||
|
'xpub6D6V5EX8HTe95getx2tTH2QApmrA1nPJFEnneAK813RjcDdSc3WaAF7BRNpTF7o7zXjVm3DD3VMX66jhQ7wLaZ9sS6NzyfiwfzqDZbxvpDN'
|
||||||
|
const { publicKey } = accountFromExtendedKey(xpub)
|
||||||
|
|
||||||
|
expect(publicKey).toBe('e8bcf3823669444d0b49ad45d65088635d9fd8500a75b5f20b59abefa56a144f')
|
||||||
|
})
|
||||||
|
|||||||
58
nip06.ts
58
nip06.ts
@@ -3,13 +3,69 @@ import { wordlist } from '@scure/bip39/wordlists/english'
|
|||||||
import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from '@scure/bip39'
|
import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from '@scure/bip39'
|
||||||
import { HDKey } from '@scure/bip32'
|
import { HDKey } from '@scure/bip32'
|
||||||
|
|
||||||
|
const DERIVATION_PATH = `m/44'/1237'`
|
||||||
|
|
||||||
export function privateKeyFromSeedWords(mnemonic: string, passphrase?: string, accountIndex = 0): string {
|
export function privateKeyFromSeedWords(mnemonic: string, passphrase?: string, accountIndex = 0): string {
|
||||||
let root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase))
|
let root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase))
|
||||||
let privateKey = root.derive(`m/44'/1237'/${accountIndex}'/0/0`).privateKey
|
let privateKey = root.derive(`${DERIVATION_PATH}/${accountIndex}'/0/0`).privateKey
|
||||||
if (!privateKey) throw new Error('could not derive private key')
|
if (!privateKey) throw new Error('could not derive private key')
|
||||||
return bytesToHex(privateKey)
|
return bytesToHex(privateKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function accountFromSeedWords(
|
||||||
|
mnemonic: string,
|
||||||
|
passphrase?: string,
|
||||||
|
accountIndex = 0,
|
||||||
|
): {
|
||||||
|
privateKey: string
|
||||||
|
publicKey: string
|
||||||
|
} {
|
||||||
|
const root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase))
|
||||||
|
const seed = root.derive(`${DERIVATION_PATH}/${accountIndex}'/0/0`)
|
||||||
|
const privateKey = bytesToHex(seed.privateKey!)
|
||||||
|
const publicKey = bytesToHex(seed.publicKey!.slice(1))
|
||||||
|
if (!privateKey && !publicKey) {
|
||||||
|
throw new Error('could not derive key pair')
|
||||||
|
}
|
||||||
|
return { privateKey, publicKey }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extendedKeysFromSeedWords(
|
||||||
|
mnemonic: string,
|
||||||
|
passphrase?: string,
|
||||||
|
extendedAccountIndex = 0,
|
||||||
|
): {
|
||||||
|
privateExtendedKey: string
|
||||||
|
publicExtendedKey: string
|
||||||
|
} {
|
||||||
|
let root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase))
|
||||||
|
let seed = root.derive(`${DERIVATION_PATH}/${extendedAccountIndex}'`)
|
||||||
|
let privateExtendedKey = seed.privateExtendedKey
|
||||||
|
let publicExtendedKey = seed.publicExtendedKey
|
||||||
|
if (!privateExtendedKey && !publicExtendedKey) throw new Error('could not derive extended key pair')
|
||||||
|
return { privateExtendedKey, publicExtendedKey }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function accountFromExtendedKey(
|
||||||
|
base58key: string,
|
||||||
|
accountIndex = 0,
|
||||||
|
): {
|
||||||
|
privateKey?: string
|
||||||
|
publicKey: string
|
||||||
|
} {
|
||||||
|
let extendedKey = HDKey.fromExtendedKey(base58key)
|
||||||
|
let version = base58key.slice(0, 4)
|
||||||
|
let child = extendedKey.deriveChild(0).deriveChild(accountIndex)
|
||||||
|
let publicKey = bytesToHex(child.publicKey!.slice(1))
|
||||||
|
if (!publicKey) throw new Error('could not derive public key')
|
||||||
|
if (version === 'xprv') {
|
||||||
|
let privateKey = bytesToHex(child.privateKey!)
|
||||||
|
if (!privateKey) throw new Error('could not derive private key')
|
||||||
|
return { privateKey, publicKey }
|
||||||
|
}
|
||||||
|
return { publicKey }
|
||||||
|
}
|
||||||
|
|
||||||
export function generateSeedWords(): string {
|
export function generateSeedWords(): string {
|
||||||
return generateMnemonic(wordlist)
|
return generateMnemonic(wordlist)
|
||||||
}
|
}
|
||||||
|
|||||||
16
nip07.ts
Normal file
16
nip07.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { EventTemplate, NostrEvent } from './core.ts'
|
||||||
|
import { RelayRecord } from './relay.ts'
|
||||||
|
|
||||||
|
export interface WindowNostr {
|
||||||
|
getPublicKey(): Promise<string>
|
||||||
|
signEvent(event: EventTemplate): Promise<NostrEvent>
|
||||||
|
getRelays(): Promise<RelayRecord>
|
||||||
|
nip04?: {
|
||||||
|
encrypt(pubkey: string, plaintext: string): Promise<string>
|
||||||
|
decrypt(pubkey: string, ciphertext: string): Promise<string>
|
||||||
|
}
|
||||||
|
nip44?: {
|
||||||
|
encrypt(pubkey: string, plaintext: string): Promise<string>
|
||||||
|
decrypt(pubkey: string, ciphertext: string): Promise<string>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ const v2vec = vec.v2
|
|||||||
|
|
||||||
test('get_conversation_key', () => {
|
test('get_conversation_key', () => {
|
||||||
for (const v of v2vec.valid.get_conversation_key) {
|
for (const v of v2vec.valid.get_conversation_key) {
|
||||||
const key = v2.utils.getConversationKey(v.sec1, v.pub2)
|
const key = v2.utils.getConversationKey(hexToBytes(v.sec1), v.pub2)
|
||||||
expect(bytesToHex(key)).toEqual(v.conversation_key)
|
expect(bytesToHex(key)).toEqual(v.conversation_key)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -15,7 +15,7 @@ test('get_conversation_key', () => {
|
|||||||
test('encrypt_decrypt', () => {
|
test('encrypt_decrypt', () => {
|
||||||
for (const v of v2vec.valid.encrypt_decrypt) {
|
for (const v of v2vec.valid.encrypt_decrypt) {
|
||||||
const pub2 = bytesToHex(schnorr.getPublicKey(v.sec2))
|
const pub2 = bytesToHex(schnorr.getPublicKey(v.sec2))
|
||||||
const key = v2.utils.getConversationKey(v.sec1, pub2)
|
const key = v2.utils.getConversationKey(hexToBytes(v.sec1), pub2)
|
||||||
expect(bytesToHex(key)).toEqual(v.conversation_key)
|
expect(bytesToHex(key)).toEqual(v.conversation_key)
|
||||||
const ciphertext = v2.encrypt(v.plaintext, key, hexToBytes(v.nonce))
|
const ciphertext = v2.encrypt(v.plaintext, key, hexToBytes(v.nonce))
|
||||||
expect(ciphertext).toEqual(v.payload)
|
expect(ciphertext).toEqual(v.payload)
|
||||||
@@ -39,6 +39,8 @@ test('decrypt', async () => {
|
|||||||
|
|
||||||
test('get_conversation_key', async () => {
|
test('get_conversation_key', async () => {
|
||||||
for (const v of v2vec.invalid.get_conversation_key) {
|
for (const v of v2vec.invalid.get_conversation_key) {
|
||||||
expect(() => v2.utils.getConversationKey(v.sec1, v.pub2)).toThrow(/(Point is not on curve|Cannot find square root)/)
|
expect(() => v2.utils.getConversationKey(hexToBytes(v.sec1), v.pub2)).toThrow(
|
||||||
|
/(Point is not on curve|Cannot find square root)/,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
221
nip44.ts
221
nip44.ts
@@ -4,129 +4,124 @@ import { secp256k1 } from '@noble/curves/secp256k1'
|
|||||||
import { extract as hkdf_extract, expand as hkdf_expand } from '@noble/hashes/hkdf'
|
import { extract as hkdf_extract, expand as hkdf_expand } from '@noble/hashes/hkdf'
|
||||||
import { hmac } from '@noble/hashes/hmac'
|
import { hmac } from '@noble/hashes/hmac'
|
||||||
import { sha256 } from '@noble/hashes/sha256'
|
import { sha256 } from '@noble/hashes/sha256'
|
||||||
import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils'
|
import { concatBytes, randomBytes } from '@noble/hashes/utils'
|
||||||
import { base64 } from '@scure/base'
|
import { base64 } from '@scure/base'
|
||||||
|
|
||||||
const decoder = new TextDecoder()
|
import { utf8Decoder, utf8Encoder } from './utils.ts'
|
||||||
|
|
||||||
class u {
|
const minPlaintextSize = 0x0001 // 1b msg => padded to 32b
|
||||||
static minPlaintextSize = 0x0001 // 1b msg => padded to 32b
|
const maxPlaintextSize = 0xffff // 65535 (64kb-1) => padded to 64kb
|
||||||
static maxPlaintextSize = 0xffff // 65535 (64kb-1) => padded to 64kb
|
|
||||||
|
|
||||||
static utf8Encode = utf8ToBytes
|
export function getConversationKey(privkeyA: Uint8Array, pubkeyB: string): Uint8Array {
|
||||||
|
const sharedX = secp256k1.getSharedSecret(privkeyA, '02' + pubkeyB).subarray(1, 33)
|
||||||
|
return hkdf_extract(sha256, sharedX, 'nip44-v2')
|
||||||
|
}
|
||||||
|
|
||||||
static utf8Decode(bytes: Uint8Array): string {
|
function getMessageKeys(
|
||||||
return decoder.decode(bytes)
|
conversationKey: Uint8Array,
|
||||||
}
|
nonce: Uint8Array,
|
||||||
|
): { chacha_key: Uint8Array; chacha_nonce: Uint8Array; hmac_key: Uint8Array } {
|
||||||
static getConversationKey(privkeyA: string, pubkeyB: string): Uint8Array {
|
const keys = hkdf_expand(sha256, conversationKey, nonce, 76)
|
||||||
const sharedX = secp256k1.getSharedSecret(privkeyA, '02' + pubkeyB).subarray(1, 33)
|
return {
|
||||||
return hkdf_extract(sha256, sharedX, 'nip44-v2')
|
chacha_key: keys.subarray(0, 32),
|
||||||
}
|
chacha_nonce: keys.subarray(32, 44),
|
||||||
|
hmac_key: keys.subarray(44, 76),
|
||||||
static getMessageKeys(
|
|
||||||
conversationKey: Uint8Array,
|
|
||||||
nonce: Uint8Array,
|
|
||||||
): { chacha_key: Uint8Array; chacha_nonce: Uint8Array; hmac_key: Uint8Array } {
|
|
||||||
const keys = hkdf_expand(sha256, conversationKey, nonce, 76)
|
|
||||||
return {
|
|
||||||
chacha_key: keys.subarray(0, 32),
|
|
||||||
chacha_nonce: keys.subarray(32, 44),
|
|
||||||
hmac_key: keys.subarray(44, 76),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static calcPaddedLen(len: number): number {
|
|
||||||
if (!Number.isSafeInteger(len) || len < 1) throw new Error('expected positive integer')
|
|
||||||
if (len <= 32) return 32
|
|
||||||
const nextPower = 1 << (Math.floor(Math.log2(len - 1)) + 1)
|
|
||||||
const chunk = nextPower <= 256 ? 32 : nextPower / 8
|
|
||||||
return chunk * (Math.floor((len - 1) / chunk) + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
static writeU16BE(num: number): Uint8Array {
|
|
||||||
if (!Number.isSafeInteger(num) || num < u.minPlaintextSize || num > u.maxPlaintextSize)
|
|
||||||
throw new Error('invalid plaintext size: must be between 1 and 65535 bytes')
|
|
||||||
const arr = new Uint8Array(2)
|
|
||||||
new DataView(arr.buffer).setUint16(0, num, false)
|
|
||||||
return arr
|
|
||||||
}
|
|
||||||
|
|
||||||
static pad(plaintext: string): Uint8Array {
|
|
||||||
const unpadded = u.utf8Encode(plaintext)
|
|
||||||
const unpaddedLen = unpadded.length
|
|
||||||
const prefix = u.writeU16BE(unpaddedLen)
|
|
||||||
const suffix = new Uint8Array(u.calcPaddedLen(unpaddedLen) - unpaddedLen)
|
|
||||||
return concatBytes(prefix, unpadded, suffix)
|
|
||||||
}
|
|
||||||
|
|
||||||
static unpad(padded: Uint8Array): string {
|
|
||||||
const unpaddedLen = new DataView(padded.buffer).getUint16(0)
|
|
||||||
const unpadded = padded.subarray(2, 2 + unpaddedLen)
|
|
||||||
if (
|
|
||||||
unpaddedLen < u.minPlaintextSize ||
|
|
||||||
unpaddedLen > u.maxPlaintextSize ||
|
|
||||||
unpadded.length !== unpaddedLen ||
|
|
||||||
padded.length !== 2 + u.calcPaddedLen(unpaddedLen)
|
|
||||||
)
|
|
||||||
throw new Error('invalid padding')
|
|
||||||
return u.utf8Decode(unpadded)
|
|
||||||
}
|
|
||||||
|
|
||||||
static hmacAad(key: Uint8Array, message: Uint8Array, aad: Uint8Array): Uint8Array {
|
|
||||||
if (aad.length !== 32) throw new Error('AAD associated data must be 32 bytes')
|
|
||||||
const combined = concatBytes(aad, message)
|
|
||||||
return hmac(sha256, key, combined)
|
|
||||||
}
|
|
||||||
|
|
||||||
// metadata: always 65b (version: 1b, nonce: 32b, max: 32b)
|
|
||||||
// plaintext: 1b to 0xffff
|
|
||||||
// padded plaintext: 32b to 0xffff
|
|
||||||
// ciphertext: 32b+2 to 0xffff+2
|
|
||||||
// raw payload: 99 (65+32+2) to 65603 (65+0xffff+2)
|
|
||||||
// compressed payload (base64): 132b to 87472b
|
|
||||||
static decodePayload(payload: string): { nonce: Uint8Array; ciphertext: Uint8Array; mac: Uint8Array } {
|
|
||||||
if (typeof payload !== 'string') throw new Error('payload must be a valid string')
|
|
||||||
const plen = payload.length
|
|
||||||
if (plen < 132 || plen > 87472) throw new Error('invalid payload length: ' + plen)
|
|
||||||
if (payload[0] === '#') throw new Error('unknown encryption version')
|
|
||||||
let data: Uint8Array
|
|
||||||
try {
|
|
||||||
data = base64.decode(payload)
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error('invalid base64: ' + (error as any).message)
|
|
||||||
}
|
|
||||||
const dlen = data.length
|
|
||||||
if (dlen < 99 || dlen > 65603) throw new Error('invalid data length: ' + dlen)
|
|
||||||
const vers = data[0]
|
|
||||||
if (vers !== 2) throw new Error('unknown encryption version ' + vers)
|
|
||||||
return {
|
|
||||||
nonce: data.subarray(1, 33),
|
|
||||||
ciphertext: data.subarray(33, -32),
|
|
||||||
mac: data.subarray(-32),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class v2 {
|
function calcPaddedLen(len: number): number {
|
||||||
static utils = u
|
if (!Number.isSafeInteger(len) || len < 1) throw new Error('expected positive integer')
|
||||||
|
if (len <= 32) return 32
|
||||||
|
const nextPower = 1 << (Math.floor(Math.log2(len - 1)) + 1)
|
||||||
|
const chunk = nextPower <= 256 ? 32 : nextPower / 8
|
||||||
|
return chunk * (Math.floor((len - 1) / chunk) + 1)
|
||||||
|
}
|
||||||
|
|
||||||
static encrypt(plaintext: string, conversationKey: Uint8Array, nonce: Uint8Array = randomBytes(32)): string {
|
function writeU16BE(num: number): Uint8Array {
|
||||||
const { chacha_key, chacha_nonce, hmac_key } = u.getMessageKeys(conversationKey, nonce)
|
if (!Number.isSafeInteger(num) || num < minPlaintextSize || num > maxPlaintextSize)
|
||||||
const padded = u.pad(plaintext)
|
throw new Error('invalid plaintext size: must be between 1 and 65535 bytes')
|
||||||
const ciphertext = chacha20(chacha_key, chacha_nonce, padded)
|
const arr = new Uint8Array(2)
|
||||||
const mac = u.hmacAad(hmac_key, ciphertext, nonce)
|
new DataView(arr.buffer).setUint16(0, num, false)
|
||||||
return base64.encode(concatBytes(new Uint8Array([2]), nonce, ciphertext, mac))
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
function pad(plaintext: string): Uint8Array {
|
||||||
|
const unpadded = utf8Encoder.encode(plaintext)
|
||||||
|
const unpaddedLen = unpadded.length
|
||||||
|
const prefix = writeU16BE(unpaddedLen)
|
||||||
|
const suffix = new Uint8Array(calcPaddedLen(unpaddedLen) - unpaddedLen)
|
||||||
|
return concatBytes(prefix, unpadded, suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
function unpad(padded: Uint8Array): string {
|
||||||
|
const unpaddedLen = new DataView(padded.buffer).getUint16(0)
|
||||||
|
const unpadded = padded.subarray(2, 2 + unpaddedLen)
|
||||||
|
if (
|
||||||
|
unpaddedLen < minPlaintextSize ||
|
||||||
|
unpaddedLen > maxPlaintextSize ||
|
||||||
|
unpadded.length !== unpaddedLen ||
|
||||||
|
padded.length !== 2 + calcPaddedLen(unpaddedLen)
|
||||||
|
)
|
||||||
|
throw new Error('invalid padding')
|
||||||
|
return utf8Decoder.decode(unpadded)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hmacAad(key: Uint8Array, message: Uint8Array, aad: Uint8Array): Uint8Array {
|
||||||
|
if (aad.length !== 32) throw new Error('AAD associated data must be 32 bytes')
|
||||||
|
const combined = concatBytes(aad, message)
|
||||||
|
return hmac(sha256, key, combined)
|
||||||
|
}
|
||||||
|
|
||||||
|
// metadata: always 65b (version: 1b, nonce: 32b, max: 32b)
|
||||||
|
// plaintext: 1b to 0xffff
|
||||||
|
// padded plaintext: 32b to 0xffff
|
||||||
|
// ciphertext: 32b+2 to 0xffff+2
|
||||||
|
// raw payload: 99 (65+32+2) to 65603 (65+0xffff+2)
|
||||||
|
// compressed payload (base64): 132b to 87472b
|
||||||
|
function decodePayload(payload: string): { nonce: Uint8Array; ciphertext: Uint8Array; mac: Uint8Array } {
|
||||||
|
if (typeof payload !== 'string') throw new Error('payload must be a valid string')
|
||||||
|
const plen = payload.length
|
||||||
|
if (plen < 132 || plen > 87472) throw new Error('invalid payload length: ' + plen)
|
||||||
|
if (payload[0] === '#') throw new Error('unknown encryption version')
|
||||||
|
let data: Uint8Array
|
||||||
|
try {
|
||||||
|
data = base64.decode(payload)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('invalid base64: ' + (error as any).message)
|
||||||
}
|
}
|
||||||
|
const dlen = data.length
|
||||||
static decrypt(payload: string, conversationKey: Uint8Array): string {
|
if (dlen < 99 || dlen > 65603) throw new Error('invalid data length: ' + dlen)
|
||||||
const { nonce, ciphertext, mac } = u.decodePayload(payload)
|
const vers = data[0]
|
||||||
const { chacha_key, chacha_nonce, hmac_key } = u.getMessageKeys(conversationKey, nonce)
|
if (vers !== 2) throw new Error('unknown encryption version ' + vers)
|
||||||
const calculatedMac = u.hmacAad(hmac_key, ciphertext, nonce)
|
return {
|
||||||
if (!equalBytes(calculatedMac, mac)) throw new Error('invalid MAC')
|
nonce: data.subarray(1, 33),
|
||||||
const padded = chacha20(chacha_key, chacha_nonce, ciphertext)
|
ciphertext: data.subarray(33, -32),
|
||||||
return u.unpad(padded)
|
mac: data.subarray(-32),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default { v2 }
|
export function encrypt(plaintext: string, conversationKey: Uint8Array, nonce: Uint8Array = randomBytes(32)): string {
|
||||||
|
const { chacha_key, chacha_nonce, hmac_key } = getMessageKeys(conversationKey, nonce)
|
||||||
|
const padded = pad(plaintext)
|
||||||
|
const ciphertext = chacha20(chacha_key, chacha_nonce, padded)
|
||||||
|
const mac = hmacAad(hmac_key, ciphertext, nonce)
|
||||||
|
return base64.encode(concatBytes(new Uint8Array([2]), nonce, ciphertext, mac))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decrypt(payload: string, conversationKey: Uint8Array): string {
|
||||||
|
const { nonce, ciphertext, mac } = decodePayload(payload)
|
||||||
|
const { chacha_key, chacha_nonce, hmac_key } = getMessageKeys(conversationKey, nonce)
|
||||||
|
const calculatedMac = hmacAad(hmac_key, ciphertext, nonce)
|
||||||
|
if (!equalBytes(calculatedMac, mac)) throw new Error('invalid MAC')
|
||||||
|
const padded = chacha20(chacha_key, chacha_nonce, ciphertext)
|
||||||
|
return unpad(padded)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const v2 = {
|
||||||
|
utils: {
|
||||||
|
getConversationKey,
|
||||||
|
calcPaddedLen,
|
||||||
|
},
|
||||||
|
encrypt,
|
||||||
|
decrypt,
|
||||||
|
}
|
||||||
|
|||||||
18
nip46.ts
18
nip46.ts
@@ -1,11 +1,13 @@
|
|||||||
|
import { hexToBytes } from '@noble/hashes/utils'
|
||||||
import { NostrEvent, UnsignedEvent, VerifiedEvent } from './core.ts'
|
import { NostrEvent, UnsignedEvent, VerifiedEvent } from './core.ts'
|
||||||
import { generateSecretKey, finalizeEvent, getPublicKey, verifyEvent } from './pure.ts'
|
import { generateSecretKey, finalizeEvent, getPublicKey, verifyEvent } from './pure.ts'
|
||||||
import { AbstractSimplePool, SubCloser } from './abstract-pool.ts'
|
import { AbstractSimplePool, SubCloser } from './abstract-pool.ts'
|
||||||
import { decrypt, encrypt } from './nip04.ts'
|
import { decrypt, encrypt } from './nip04.ts'
|
||||||
|
import { getConversationKey, decrypt as nip44decrypt } from './nip44.ts'
|
||||||
import { NIP05_REGEX } from './nip05.ts'
|
import { NIP05_REGEX } from './nip05.ts'
|
||||||
import { SimplePool } from './pool.ts'
|
import { SimplePool } from './pool.ts'
|
||||||
import { Handlerinformation, NostrConnect } from './kinds.ts'
|
import { Handlerinformation, NostrConnect } from './kinds.ts'
|
||||||
import { hexToBytes } from '@noble/hashes/utils'
|
import type { RelayRecord } from './relay.ts'
|
||||||
|
|
||||||
var _fetch: any
|
var _fetch: any
|
||||||
|
|
||||||
@@ -109,13 +111,21 @@ export class BunkerSigner {
|
|||||||
|
|
||||||
const listeners = this.listeners
|
const listeners = this.listeners
|
||||||
const waitingForAuth = this.waitingForAuth
|
const waitingForAuth = this.waitingForAuth
|
||||||
|
const skBytes = this.secretKey
|
||||||
|
|
||||||
this.subCloser = this.pool.subscribeMany(
|
this.subCloser = this.pool.subscribeMany(
|
||||||
this.bp.relays,
|
this.bp.relays,
|
||||||
[{ kinds: [NostrConnect], '#p': [getPublicKey(this.secretKey)] }],
|
[{ kinds: [NostrConnect], '#p': [getPublicKey(this.secretKey)] }],
|
||||||
{
|
{
|
||||||
async onevent(event: NostrEvent) {
|
async onevent(event: NostrEvent) {
|
||||||
const { id, result, error } = JSON.parse(await decrypt(clientSecretKey, event.pubkey, event.content))
|
let o
|
||||||
|
try {
|
||||||
|
o = JSON.parse(await decrypt(clientSecretKey, event.pubkey, event.content))
|
||||||
|
} catch (err) {
|
||||||
|
o = JSON.parse(nip44decrypt(event.content, getConversationKey(skBytes, event.pubkey)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, result, error } = o
|
||||||
|
|
||||||
if (result === 'auth_url' && waitingForAuth[id]) {
|
if (result === 'auth_url' && waitingForAuth[id]) {
|
||||||
delete waitingForAuth[id]
|
delete waitingForAuth[id]
|
||||||
@@ -207,7 +217,7 @@ export class BunkerSigner {
|
|||||||
/**
|
/**
|
||||||
* Calls the "get_relays" method on the bunker.
|
* Calls the "get_relays" method on the bunker.
|
||||||
*/
|
*/
|
||||||
async getRelays(): Promise<{ [relay: string]: { read: boolean; write: boolean } }> {
|
async getRelays(): Promise<RelayRecord> {
|
||||||
return JSON.parse(await this.sendRequest('get_relays', []))
|
return JSON.parse(await this.sendRequest('get_relays', []))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,7 +275,7 @@ export async function createAccount(
|
|||||||
username: string,
|
username: string,
|
||||||
domain: string,
|
domain: string,
|
||||||
email?: string,
|
email?: string,
|
||||||
localSecretKey: Uint8Array = generateSecretKey()
|
localSecretKey: Uint8Array = generateSecretKey(),
|
||||||
): Promise<BunkerSigner> {
|
): Promise<BunkerSigner> {
|
||||||
if (email && !EMAIL_REGEX.test(email)) throw new Error('Invalid email')
|
if (email && !EMAIL_REGEX.test(email)) throw new Error('Invalid email')
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"name": "nostr-tools",
|
"name": "nostr-tools",
|
||||||
"version": "2.5.2",
|
"version": "2.7.0",
|
||||||
"description": "Tools for making a Nostr client.",
|
"description": "Tools for making a Nostr client.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -85,6 +85,9 @@
|
|||||||
"require": "./lib/cjs/nip06.js",
|
"require": "./lib/cjs/nip06.js",
|
||||||
"types": "./lib/types/nip06.d.ts"
|
"types": "./lib/types/nip06.d.ts"
|
||||||
},
|
},
|
||||||
|
"./nip07": {
|
||||||
|
"types": "./lib/types/nip07.d.ts"
|
||||||
|
},
|
||||||
"./nip10": {
|
"./nip10": {
|
||||||
"import": "./lib/esm/nip10.js",
|
"import": "./lib/esm/nip10.js",
|
||||||
"require": "./lib/cjs/nip10.js",
|
"require": "./lib/cjs/nip10.js",
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
||||||
|
|
||||||
import { SimplePool } from './pool.ts'
|
import { SimplePool, useWebSocketImplementation } from './pool.ts'
|
||||||
import { finalizeEvent, generateSecretKey, getPublicKey, type Event } from './pure.ts'
|
import { finalizeEvent, generateSecretKey, getPublicKey, type Event } from './pure.ts'
|
||||||
import { useWebSocketImplementation } from './relay.ts'
|
|
||||||
import { MockRelay, MockWebSocketClient } from './test-helpers.ts'
|
import { MockRelay, MockWebSocketClient } from './test-helpers.ts'
|
||||||
import { hexToBytes } from '@noble/hashes/utils'
|
import { hexToBytes } from '@noble/hashes/utils'
|
||||||
|
|
||||||
|
|||||||
14
pool.ts
14
pool.ts
@@ -1,9 +1,21 @@
|
|||||||
|
/* global WebSocket */
|
||||||
|
|
||||||
import { verifyEvent } from './pure.ts'
|
import { verifyEvent } from './pure.ts'
|
||||||
import { AbstractSimplePool } from './abstract-pool.ts'
|
import { AbstractSimplePool } from './abstract-pool.ts'
|
||||||
|
|
||||||
|
var _WebSocket: typeof WebSocket
|
||||||
|
|
||||||
|
try {
|
||||||
|
_WebSocket = WebSocket
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
export function useWebSocketImplementation(websocketImplementation: any) {
|
||||||
|
_WebSocket = websocketImplementation
|
||||||
|
}
|
||||||
|
|
||||||
export class SimplePool extends AbstractSimplePool {
|
export class SimplePool extends AbstractSimplePool {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({ verifyEvent })
|
super({ verifyEvent, websocketImplementation: _WebSocket })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
relay.ts
16
relay.ts
@@ -1,3 +1,5 @@
|
|||||||
|
/* global WebSocket */
|
||||||
|
|
||||||
import { verifyEvent } from './pure.ts'
|
import { verifyEvent } from './pure.ts'
|
||||||
import { AbstractRelay } from './abstract-relay.ts'
|
import { AbstractRelay } from './abstract-relay.ts'
|
||||||
|
|
||||||
@@ -8,9 +10,19 @@ export function relayConnect(url: string): Promise<Relay> {
|
|||||||
return Relay.connect(url)
|
return Relay.connect(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _WebSocket: typeof WebSocket
|
||||||
|
|
||||||
|
try {
|
||||||
|
_WebSocket = WebSocket
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
export function useWebSocketImplementation(websocketImplementation: any) {
|
||||||
|
_WebSocket = websocketImplementation
|
||||||
|
}
|
||||||
|
|
||||||
export class Relay extends AbstractRelay {
|
export class Relay extends AbstractRelay {
|
||||||
constructor(url: string) {
|
constructor(url: string) {
|
||||||
super(url, { verifyEvent })
|
super(url, { verifyEvent, websocketImplementation: _WebSocket })
|
||||||
}
|
}
|
||||||
|
|
||||||
static async connect(url: string): Promise<Relay> {
|
static async connect(url: string): Promise<Relay> {
|
||||||
@@ -20,4 +32,6 @@ export class Relay extends AbstractRelay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RelayRecord = Record<string, { read: boolean; write: boolean }>
|
||||||
|
|
||||||
export * from './abstract-relay.ts'
|
export * from './abstract-relay.ts'
|
||||||
|
|||||||
Reference in New Issue
Block a user