Compare commits

...

5 Commits

Author SHA1 Message Date
fiatjaf
87a91c2daf fix useWebSocketImplementation so it works with pool on nodejs esm. 2024-05-29 13:39:00 -03:00
Anderson Juhasc
4f1dc9ef1c fixing formatting with Prettier 2024-05-27 10:44:44 -03:00
Anderson Juhasc
faa1a9d556 adding nip06 examples to the README 2024-05-27 10:44:44 -03:00
Anderson Juhasc
97d838f254 white spaces removed 2024-05-27 10:44:44 -03:00
Don
260400b24d fix typo in nip07.ts 2024-05-27 10:42:35 -03:00
11 changed files with 123 additions and 39 deletions

View File

@@ -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
@@ -194,6 +197,32 @@ declare global {
} }
``` ```
### 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

View File

@@ -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)

View File

@@ -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

View File

@@ -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'

View File

@@ -3,7 +3,7 @@ import {
privateKeyFromSeedWords, privateKeyFromSeedWords,
accountFromSeedWords, accountFromSeedWords,
extendedKeysFromSeedWords, extendedKeysFromSeedWords,
accountFromExtendedKey accountFromExtendedKey,
} from './nip06.ts' } from './nip06.ts'
test('generate private key from a mnemonic', async () => { test('generate private key from a mnemonic', async () => {
@@ -44,14 +44,23 @@ test('generate extended keys from mnemonic', () => {
const mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about' const mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
const passphrase = '' const passphrase = ''
const extendedAccountIndex = 0 const extendedAccountIndex = 0
const { privateExtendedKey, publicExtendedKey } = extendedKeysFromSeedWords(mnemonic, passphrase, extendedAccountIndex) const { privateExtendedKey, publicExtendedKey } = extendedKeysFromSeedWords(
mnemonic,
passphrase,
extendedAccountIndex,
)
expect(privateExtendedKey).toBe('xprv9z78fizET65qsCaRr1MSutTSGk1fcKfSt1sBqmuWShtkjRJJ4WCKcSnha6EmgNzFSsyom3MWtydHyPtJtSLZQUtictVQtM2vkPcguh6TQCH') expect(privateExtendedKey).toBe(
expect(publicExtendedKey).toBe('xpub6D6V5EX8HTe95getx2tTH2QApmrA1nPJFEnneAK813RjcDdSc3WaAF7BRNpTF7o7zXjVm3DD3VMX66jhQ7wLaZ9sS6NzyfiwfzqDZbxvpDN') 'xprv9z78fizET65qsCaRr1MSutTSGk1fcKfSt1sBqmuWShtkjRJJ4WCKcSnha6EmgNzFSsyom3MWtydHyPtJtSLZQUtictVQtM2vkPcguh6TQCH',
)
expect(publicExtendedKey).toBe(
'xpub6D6V5EX8HTe95getx2tTH2QApmrA1nPJFEnneAK813RjcDdSc3WaAF7BRNpTF7o7zXjVm3DD3VMX66jhQ7wLaZ9sS6NzyfiwfzqDZbxvpDN',
)
}) })
test('generate account from extended private key', () => { test('generate account from extended private key', () => {
const xprv = 'xprv9z78fizET65qsCaRr1MSutTSGk1fcKfSt1sBqmuWShtkjRJJ4WCKcSnha6EmgNzFSsyom3MWtydHyPtJtSLZQUtictVQtM2vkPcguh6TQCH' const xprv =
'xprv9z78fizET65qsCaRr1MSutTSGk1fcKfSt1sBqmuWShtkjRJJ4WCKcSnha6EmgNzFSsyom3MWtydHyPtJtSLZQUtictVQtM2vkPcguh6TQCH'
const { privateKey, publicKey } = accountFromExtendedKey(xprv) const { privateKey, publicKey } = accountFromExtendedKey(xprv)
expect(privateKey).toBe('5f29af3b9676180290e77a4efad265c4c2ff28a5302461f73597fda26bb25731') expect(privateKey).toBe('5f29af3b9676180290e77a4efad265c4c2ff28a5302461f73597fda26bb25731')
@@ -59,7 +68,8 @@ test('generate account from extended private key', () => {
}) })
test('generate account from extended public key', () => { test('generate account from extended public key', () => {
const xpub = 'xpub6D6V5EX8HTe95getx2tTH2QApmrA1nPJFEnneAK813RjcDdSc3WaAF7BRNpTF7o7zXjVm3DD3VMX66jhQ7wLaZ9sS6NzyfiwfzqDZbxvpDN' const xpub =
'xpub6D6V5EX8HTe95getx2tTH2QApmrA1nPJFEnneAK813RjcDdSc3WaAF7BRNpTF7o7zXjVm3DD3VMX66jhQ7wLaZ9sS6NzyfiwfzqDZbxvpDN'
const { publicKey } = accountFromExtendedKey(xpub) const { publicKey } = accountFromExtendedKey(xpub)
expect(publicKey).toBe('e8bcf3823669444d0b49ad45d65088635d9fd8500a75b5f20b59abefa56a144f') expect(publicKey).toBe('e8bcf3823669444d0b49ad45d65088635d9fd8500a75b5f20b59abefa56a144f')

View File

@@ -12,9 +12,13 @@ export function privateKeyFromSeedWords(mnemonic: string, passphrase?: string, a
return bytesToHex(privateKey) return bytesToHex(privateKey)
} }
export function accountFromSeedWords(mnemonic: string, passphrase?: string, accountIndex = 0): { export function accountFromSeedWords(
privateKey: string, mnemonic: string,
publicKey: string, passphrase?: string,
accountIndex = 0,
): {
privateKey: string
publicKey: string
} { } {
const root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase)) const root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase))
const seed = root.derive(`${DERIVATION_PATH}/${accountIndex}'/0/0`) const seed = root.derive(`${DERIVATION_PATH}/${accountIndex}'/0/0`)
@@ -26,8 +30,12 @@ export function accountFromSeedWords(mnemonic: string, passphrase?: string, acco
return { privateKey, publicKey } return { privateKey, publicKey }
} }
export function extendedKeysFromSeedWords(mnemonic: string, passphrase?: string, extendedAccountIndex = 0): { export function extendedKeysFromSeedWords(
privateExtendedKey: string, mnemonic: string,
passphrase?: string,
extendedAccountIndex = 0,
): {
privateExtendedKey: string
publicExtendedKey: string publicExtendedKey: string
} { } {
let root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase)) let root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase))
@@ -38,8 +46,11 @@ export function extendedKeysFromSeedWords(mnemonic: string, passphrase?: string,
return { privateExtendedKey, publicExtendedKey } return { privateExtendedKey, publicExtendedKey }
} }
export function accountFromExtendedKey(base58key: string, accountIndex = 0): { export function accountFromExtendedKey(
privateKey?: string, base58key: string,
accountIndex = 0,
): {
privateKey?: string
publicKey: string publicKey: string
} { } {
let extendedKey = HDKey.fromExtendedKey(base58key) let extendedKey = HDKey.fromExtendedKey(base58key)
@@ -50,9 +61,9 @@ export function accountFromExtendedKey(base58key: string, accountIndex = 0): {
if (version === 'xprv') { if (version === 'xprv') {
let privateKey = bytesToHex(child.privateKey!) let privateKey = bytesToHex(child.privateKey!)
if (!privateKey) throw new Error('could not derive private key') if (!privateKey) throw new Error('could not derive private key')
return { privateKey, publicKey } return { privateKey, publicKey }
} }
return { publicKey } return { publicKey }
} }
export function generateSeedWords(): string { export function generateSeedWords(): string {

View File

@@ -7,7 +7,7 @@ export interface WindowNostr {
getRelays(): Promise<RelayRecord> getRelays(): Promise<RelayRecord>
nip04?: { nip04?: {
encrypt(pubkey: string, plaintext: string): Promise<string> encrypt(pubkey: string, plaintext: string): Promise<string>
ecrypt(pubkey: string, ciphertext: string): Promise<string> decrypt(pubkey: string, ciphertext: string): Promise<string>
} }
nip44?: { nip44?: {
encrypt(pubkey: string, plaintext: string): Promise<string> encrypt(pubkey: string, plaintext: string): Promise<string>

View File

@@ -1,7 +1,7 @@
{ {
"type": "module", "type": "module",
"name": "nostr-tools", "name": "nostr-tools",
"version": "2.6.0", "version": "2.7.0",
"description": "Tools for making a Nostr client.", "description": "Tools for making a Nostr client.",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@@ -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
View File

@@ -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 })
} }
} }

View File

@@ -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,6 +32,6 @@ export class Relay extends AbstractRelay {
} }
} }
export type RelayRecord = Record<string, { read: boolean; write: boolean }>; export type RelayRecord = Record<string, { read: boolean; write: boolean }>
export * from './abstract-relay.ts' export * from './abstract-relay.ts'