nostr
package: fetch relay info
#446
@ -3,10 +3,13 @@ module.exports = {
|
|||||||
parser: "@typescript-eslint/parser",
|
parser: "@typescript-eslint/parser",
|
||||||
plugins: ["@typescript-eslint"],
|
plugins: ["@typescript-eslint"],
|
||||||
root: true,
|
root: true,
|
||||||
ignorePatterns: ["dist/"],
|
ignorePatterns: ["dist/", "src/legacy"],
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
node: true,
|
node: true,
|
||||||
mocha: true,
|
mocha: true,
|
||||||
},
|
},
|
||||||
|
rules: {
|
||||||
|
"require-await": "warn",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ A strongly-typed nostr client for Node and the browser.
|
|||||||
The goal of the project is to have all of the following implemented
|
The goal of the project is to have all of the following implemented
|
||||||
and tested against a real-world relay implementation.
|
and tested against a real-world relay implementation.
|
||||||
|
|
||||||
_Progress: 4/34 (12%)._
|
_Progress: 5/34 (15%)._
|
||||||
|
|
||||||
- [X] NIP-01: Basic protocol flow description
|
- [X] NIP-01: Basic protocol flow description
|
||||||
- [ ] NIP-02: Contact List and Petnames
|
- [ ] NIP-02: Contact List and Petnames
|
||||||
@ -22,7 +22,7 @@ _Progress: 4/34 (12%)._
|
|||||||
- [ ] NIP-09: Event Deletion
|
- [ ] NIP-09: Event Deletion
|
||||||
- [ ] NIP-10: Conventions for clients' use of `e` and `p` tags in text events
|
- [ ] NIP-10: Conventions for clients' use of `e` and `p` tags in text events
|
||||||
- TODO Check if this applies
|
- TODO Check if this applies
|
||||||
- [ ] NIP-11: Relay Information Document
|
- [X] NIP-11: Relay Information Document
|
||||||
- [ ] NIP-12: Generic Tag Queries
|
- [ ] NIP-12: Generic Tag Queries
|
||||||
- [ ] NIP-13: Proof of Work
|
- [ ] NIP-13: Proof of Work
|
||||||
- [ ] NIP-14: Subject tag in text events
|
- [ ] NIP-14: Subject tag in text events
|
||||||
@ -30,7 +30,7 @@ _Progress: 4/34 (12%)._
|
|||||||
- [ ] NIP-19: bech32-encoded entities
|
- [ ] NIP-19: bech32-encoded entities
|
||||||
- [X] `npub`
|
- [X] `npub`
|
||||||
- [X] `nsec`
|
- [X] `nsec`
|
||||||
- [ ] `nprofile`
|
- [ ] `note`, `nprofile`, `nevent`, `nrelay`, `naddr`
|
||||||
- [X] NIP-20: Command Results
|
- [X] NIP-20: Command Results
|
||||||
- [ ] NIP-21: `nostr:` URL scheme
|
- [ ] NIP-21: `nostr:` URL scheme
|
||||||
- [ ] NIP-23: Long-form Content
|
- [ ] NIP-23: Long-form Content
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
|
[info]
|
||||||
|
relay_url = "wss://nostr.example.com/"
|
||||||
|
name = "nostr-rs-relay"
|
||||||
|
description = "nostr-rs-relay description"
|
||||||
|
contact = "mailto:contact@example.com"
|
||||||
|
favicon = "favicon.ico"
|
||||||
|
|
||||||
[authorization]
|
[authorization]
|
||||||
nip42_auth = true
|
nip42_auth = true
|
||||||
# This seems to have no effect.
|
# This seems to have no effect.
|
||||||
|
@ -14,13 +14,12 @@ import { unixTimestamp } from "../util"
|
|||||||
*/
|
*/
|
||||||
export class Conn {
|
export class Conn {
|
||||||
readonly #socket: WebSocket
|
readonly #socket: WebSocket
|
||||||
|
// TODO This should probably be moved to Nostr (ConnState) because deciding whether or not to send a message
|
||||||
|
// requires looking at relay info which the Conn should know nothing about.
|
||||||
/**
|
/**
|
||||||
* Messages which were requested to be sent before the websocket was ready.
|
* Messages which were requested to be sent before the websocket was ready.
|
||||||
* Once the websocket becomes ready, these messages will be sent and cleared.
|
* Once the websocket becomes ready, these messages will be sent and cleared.
|
||||||
*/
|
*/
|
||||||
// TODO Another reason why pending messages might be required is when the user tries to send a message
|
|
||||||
// before NIP-44 auth. The legacy code reuses the same array for these two but I think they should be
|
|
||||||
// different, and the NIP-44 stuff should be handled by Nostr.
|
|
||||||
#pending: OutgoingMessage[] = []
|
#pending: OutgoingMessage[] = []
|
||||||
/**
|
/**
|
||||||
* Callback for errors.
|
* Callback for errors.
|
||||||
@ -35,27 +34,27 @@ export class Conn {
|
|||||||
url,
|
url,
|
||||||
onMessage,
|
onMessage,
|
||||||
onOpen,
|
onOpen,
|
||||||
|
onClose,
|
||||||
onError,
|
onError,
|
||||||
}: {
|
}: {
|
||||||
url: URL
|
url: URL
|
||||||
onMessage: (msg: IncomingMessage) => void
|
onMessage: (msg: IncomingMessage) => void
|
||||||
onOpen: () => void
|
onOpen: () => void | Promise<void>
|
||||||
|
onClose: () => void | Promise<void>
|
||||||
onError: (err: unknown) => void
|
onError: (err: unknown) => void
|
||||||
}) {
|
}) {
|
||||||
this.#onError = onError
|
this.#onError = onError
|
||||||
this.#socket = new WebSocket(url)
|
this.#socket = new WebSocket(url)
|
||||||
|
|
||||||
// Handle incoming messages.
|
// Handle incoming messages.
|
||||||
this.#socket.addEventListener("message", async (msgData) => {
|
this.#socket.addEventListener("message", (msgData) => {
|
||||||
|
try {
|
||||||
const value = msgData.data.valueOf()
|
const value = msgData.data.valueOf()
|
||||||
// Validate and parse the message.
|
// Validate and parse the message.
|
||||||
if (typeof value !== "string") {
|
if (typeof value !== "string") {
|
||||||
const err = new ProtocolError(`invalid message data: ${value}`)
|
throw new ProtocolError(`invalid message data: ${value}`)
|
||||||
onError(err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
try {
|
const msg = parseIncomingMessage(value)
|
||||||
const msg = await parseIncomingMessage(value)
|
|
||||||
onMessage(msg)
|
onMessage(msg)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
onError(err)
|
onError(err)
|
||||||
@ -63,17 +62,32 @@ export class Conn {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// When the connection is ready, send any outstanding messages.
|
// When the connection is ready, send any outstanding messages.
|
||||||
this.#socket.addEventListener("open", () => {
|
this.#socket.addEventListener("open", async () => {
|
||||||
|
try {
|
||||||
for (const msg of this.#pending) {
|
for (const msg of this.#pending) {
|
||||||
this.send(msg)
|
this.send(msg)
|
||||||
}
|
}
|
||||||
this.#pending = []
|
this.#pending = []
|
||||||
onOpen()
|
const result = onOpen()
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
await result
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
onError(e)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.#socket.addEventListener("error", (err) => {
|
this.#socket.addEventListener("close", async () => {
|
||||||
onError(err)
|
try {
|
||||||
|
const result = onClose()
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
await result
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
onError(e)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
this.#socket.addEventListener("error", onError)
|
||||||
}
|
}
|
||||||
|
|
||||||
send(msg: OutgoingMessage): void {
|
send(msg: OutgoingMessage): void {
|
||||||
@ -235,7 +249,7 @@ function serializeFilters(filters: Filters[]): RawFilters[] {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function parseIncomingMessage(data: string): Promise<IncomingMessage> {
|
function parseIncomingMessage(data: string): IncomingMessage {
|
||||||
// Parse the incoming data as a nonempty JSON array.
|
// Parse the incoming data as a nonempty JSON array.
|
||||||
const json = parseJson(data)
|
const json = parseJson(data)
|
||||||
if (!(json instanceof Array)) {
|
if (!(json instanceof Array)) {
|
||||||
|
@ -6,12 +6,17 @@ import { Event, EventId } from "../event"
|
|||||||
* Overrides providing better types for EventEmitter methods.
|
* Overrides providing better types for EventEmitter methods.
|
||||||
*/
|
*/
|
||||||
export class EventEmitter extends Base {
|
export class EventEmitter extends Base {
|
||||||
|
constructor() {
|
||||||
|
super({ captureRejections: true })
|
||||||
|
}
|
||||||
|
|
||||||
override addListener(eventName: "newListener", listener: NewListener): this
|
override addListener(eventName: "newListener", listener: NewListener): this
|
||||||
override addListener(
|
override addListener(
|
||||||
eventName: "removeListener",
|
eventName: "removeListener",
|
||||||
listener: RemoveListener
|
listener: RemoveListener
|
||||||
): this
|
): this
|
||||||
override addListener(eventName: "open", listener: OpenListener): this
|
override addListener(eventName: "open", listener: OpenListener): this
|
||||||
|
override addListener(eventName: "close", listener: CloseListener): this
|
||||||
override addListener(eventName: "event", listener: EventListener): this
|
override addListener(eventName: "event", listener: EventListener): this
|
||||||
override addListener(eventName: "notice", listener: NoticeListener): this
|
override addListener(eventName: "notice", listener: NoticeListener): this
|
||||||
override addListener(eventName: "ok", listener: OkListener): this
|
override addListener(eventName: "ok", listener: OkListener): this
|
||||||
@ -24,6 +29,7 @@ export class EventEmitter extends Base {
|
|||||||
override emit(eventName: "newListener", listener: NewListener): boolean
|
override emit(eventName: "newListener", listener: NewListener): boolean
|
||||||
override emit(eventName: "removeListener", listener: RemoveListener): boolean
|
override emit(eventName: "removeListener", listener: RemoveListener): boolean
|
||||||
override emit(eventName: "open", relay: URL, nostr: Nostr): boolean
|
override emit(eventName: "open", relay: URL, nostr: Nostr): boolean
|
||||||
|
override emit(eventName: "close", relay: URL, nostr: Nostr): boolean
|
||||||
override emit(eventName: "event", params: EventParams, nostr: Nostr): boolean
|
override emit(eventName: "event", params: EventParams, nostr: Nostr): boolean
|
||||||
override emit(eventName: "notice", notice: string, nostr: Nostr): boolean
|
override emit(eventName: "notice", notice: string, nostr: Nostr): boolean
|
||||||
override emit(eventName: "ok", params: OkParams, nostr: Nostr): boolean
|
override emit(eventName: "ok", params: OkParams, nostr: Nostr): boolean
|
||||||
@ -44,6 +50,7 @@ export class EventEmitter extends Base {
|
|||||||
override listeners(eventName: "newListener"): EventListener[]
|
override listeners(eventName: "newListener"): EventListener[]
|
||||||
override listeners(eventName: "removeListener"): EventListener[]
|
override listeners(eventName: "removeListener"): EventListener[]
|
||||||
override listeners(eventName: "open"): OpenListener[]
|
override listeners(eventName: "open"): OpenListener[]
|
||||||
|
override listeners(eventName: "close"): CloseListener[]
|
||||||
override listeners(eventName: "event"): EventListener[]
|
override listeners(eventName: "event"): EventListener[]
|
||||||
override listeners(eventName: "notice"): NoticeListener[]
|
override listeners(eventName: "notice"): NoticeListener[]
|
||||||
override listeners(eventName: "ok"): OkListener[]
|
override listeners(eventName: "ok"): OkListener[]
|
||||||
@ -56,6 +63,7 @@ export class EventEmitter extends Base {
|
|||||||
override off(eventName: "newListener", listener: NewListener): this
|
override off(eventName: "newListener", listener: NewListener): this
|
||||||
override off(eventName: "removeListener", listener: RemoveListener): this
|
override off(eventName: "removeListener", listener: RemoveListener): this
|
||||||
override off(eventName: "open", listener: OpenListener): this
|
override off(eventName: "open", listener: OpenListener): this
|
||||||
|
override off(eventName: "close", listener: CloseListener): this
|
||||||
override off(eventName: "event", listener: EventListener): this
|
override off(eventName: "event", listener: EventListener): this
|
||||||
override off(eventName: "notice", listener: NoticeListener): this
|
override off(eventName: "notice", listener: NoticeListener): this
|
||||||
override off(eventName: "ok", listener: OkListener): this
|
override off(eventName: "ok", listener: OkListener): this
|
||||||
@ -68,6 +76,7 @@ export class EventEmitter extends Base {
|
|||||||
override on(eventName: "newListener", listener: NewListener): this
|
override on(eventName: "newListener", listener: NewListener): this
|
||||||
override on(eventName: "removeListener", listener: RemoveListener): this
|
override on(eventName: "removeListener", listener: RemoveListener): this
|
||||||
override on(eventName: "open", listener: OpenListener): this
|
override on(eventName: "open", listener: OpenListener): this
|
||||||
|
override on(eventName: "close", listener: CloseListener): this
|
||||||
override on(eventName: "event", listener: EventListener): this
|
override on(eventName: "event", listener: EventListener): this
|
||||||
override on(eventName: "notice", listener: NoticeListener): this
|
override on(eventName: "notice", listener: NoticeListener): this
|
||||||
override on(eventName: "ok", listener: OkListener): this
|
override on(eventName: "ok", listener: OkListener): this
|
||||||
@ -80,6 +89,7 @@ export class EventEmitter extends Base {
|
|||||||
override once(eventName: "newListener", listener: NewListener): this
|
override once(eventName: "newListener", listener: NewListener): this
|
||||||
override once(eventName: "removeListener", listener: RemoveListener): this
|
override once(eventName: "removeListener", listener: RemoveListener): this
|
||||||
override once(eventName: "open", listener: OpenListener): this
|
override once(eventName: "open", listener: OpenListener): this
|
||||||
|
override once(eventName: "close", listener: CloseListener): this
|
||||||
override once(eventName: "event", listener: EventListener): this
|
override once(eventName: "event", listener: EventListener): this
|
||||||
override once(eventName: "notice", listener: NoticeListener): this
|
override once(eventName: "notice", listener: NoticeListener): this
|
||||||
override once(eventName: "ok", listener: OkListener): this
|
override once(eventName: "ok", listener: OkListener): this
|
||||||
@ -98,6 +108,7 @@ export class EventEmitter extends Base {
|
|||||||
listener: RemoveListener
|
listener: RemoveListener
|
||||||
): this
|
): this
|
||||||
override prependListener(eventName: "open", listener: OpenListener): this
|
override prependListener(eventName: "open", listener: OpenListener): this
|
||||||
|
override prependListener(eventName: "close", listener: CloseListener): this
|
||||||
override prependListener(eventName: "event", listener: EventListener): this
|
override prependListener(eventName: "event", listener: EventListener): this
|
||||||
override prependListener(eventName: "notice", listener: NoticeListener): this
|
override prependListener(eventName: "notice", listener: NoticeListener): this
|
||||||
override prependListener(eventName: "ok", listener: OkListener): this
|
override prependListener(eventName: "ok", listener: OkListener): this
|
||||||
@ -116,6 +127,10 @@ export class EventEmitter extends Base {
|
|||||||
listener: RemoveListener
|
listener: RemoveListener
|
||||||
): this
|
): this
|
||||||
override prependOnceListener(eventName: "open", listener: OpenListener): this
|
override prependOnceListener(eventName: "open", listener: OpenListener): this
|
||||||
|
override prependOnceListener(
|
||||||
|
eventName: "close",
|
||||||
|
listener: CloseListener
|
||||||
|
): this
|
||||||
override prependOnceListener(
|
override prependOnceListener(
|
||||||
eventName: "event",
|
eventName: "event",
|
||||||
listener: EventListener
|
listener: EventListener
|
||||||
@ -144,6 +159,7 @@ export class EventEmitter extends Base {
|
|||||||
listener: RemoveListener
|
listener: RemoveListener
|
||||||
): this
|
): this
|
||||||
override removeListener(eventName: "open", listener: OpenListener): this
|
override removeListener(eventName: "open", listener: OpenListener): this
|
||||||
|
override removeListener(eventName: "close", listener: CloseListener): this
|
||||||
override removeListener(eventName: "event", listener: EventListener): this
|
override removeListener(eventName: "event", listener: EventListener): this
|
||||||
override removeListener(eventName: "notice", listener: NoticeListener): this
|
override removeListener(eventName: "notice", listener: NoticeListener): this
|
||||||
override removeListener(eventName: "ok", listener: OkListener): this
|
override removeListener(eventName: "ok", listener: OkListener): this
|
||||||
@ -156,16 +172,17 @@ export class EventEmitter extends Base {
|
|||||||
override rawListeners(eventName: EventName): Listener[] {
|
override rawListeners(eventName: EventName): Listener[] {
|
||||||
return super.rawListeners(eventName) as Listener[]
|
return super.rawListeners(eventName) as Listener[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
|
||||||
// emitter[Symbol.for('nodejs.rejection')](err, eventName[, ...args]) shenanigans?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Refactor the params to be a single interface
|
// TODO Refactor the params to always be a single interface
|
||||||
|
// TODO Params should always include relay as well
|
||||||
|
// TODO Params should not include Nostr, `this` should be Nostr
|
||||||
|
// TODO Ideas for events: "auth" for NIP-42 AUTH, "message" for the raw incoming messages
|
||||||
type EventName =
|
type EventName =
|
||||||
| "newListener"
|
| "newListener"
|
||||||
| "removeListener"
|
| "removeListener"
|
||||||
| "open"
|
| "open"
|
||||||
|
| "close"
|
||||||
| "event"
|
| "event"
|
||||||
| "notice"
|
| "notice"
|
||||||
| "ok"
|
| "ok"
|
||||||
@ -175,6 +192,7 @@ type EventName =
|
|||||||
type NewListener = (eventName: EventName, listener: Listener) => void
|
type NewListener = (eventName: EventName, listener: Listener) => void
|
||||||
type RemoveListener = (eventName: EventName, listener: Listener) => void
|
type RemoveListener = (eventName: EventName, listener: Listener) => void
|
||||||
type OpenListener = (relay: URL, nostr: Nostr) => void
|
type OpenListener = (relay: URL, nostr: Nostr) => void
|
||||||
|
type CloseListener = (relay: URL, nostr: Nostr) => void
|
||||||
type EventListener = (params: EventParams, nostr: Nostr) => void
|
type EventListener = (params: EventParams, nostr: Nostr) => void
|
||||||
type NoticeListener = (notice: string, nostr: Nostr) => void
|
type NoticeListener = (notice: string, nostr: Nostr) => void
|
||||||
type OkListener = (params: OkParams, nostr: Nostr) => void
|
type OkListener = (params: OkParams, nostr: Nostr) => void
|
||||||
@ -185,6 +203,7 @@ type Listener =
|
|||||||
| NewListener
|
| NewListener
|
||||||
| RemoveListener
|
| RemoveListener
|
||||||
| OpenListener
|
| OpenListener
|
||||||
|
| CloseListener
|
||||||
| EventListener
|
| EventListener
|
||||||
| NoticeListener
|
| NoticeListener
|
||||||
| OkListener
|
| OkListener
|
||||||
|
@ -5,13 +5,29 @@ import { Conn } from "./conn"
|
|||||||
import * as secp from "@noble/secp256k1"
|
import * as secp from "@noble/secp256k1"
|
||||||
import { EventEmitter } from "./emitter"
|
import { EventEmitter } from "./emitter"
|
||||||
|
|
||||||
|
// TODO The EventEmitter will call "error" by default if errors are thrown,
|
||||||
|
// but if there is no error listener it actually rethrows the error. Revisit
|
||||||
|
// the try/catch stuff to be consistent with this.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A nostr client.
|
* A nostr client.
|
||||||
*
|
*
|
||||||
* TODO Document the events here
|
* TODO Document the events here
|
||||||
|
* TODO When document this type, remember to explicitly say that promise rejections will also be routed to "error"!
|
||||||
*/
|
*/
|
||||||
export class Nostr extends EventEmitter {
|
export class Nostr extends EventEmitter {
|
||||||
// TODO NIP-44 AUTH, leave this for later
|
static get CONNECTING(): ReadyState.CONNECTING {
|
||||||
|
return ReadyState.CONNECTING
|
||||||
|
}
|
||||||
|
|
||||||
|
static get OPEN(): ReadyState.OPEN {
|
||||||
|
return ReadyState.OPEN
|
||||||
|
}
|
||||||
|
|
||||||
|
static get CLOSED(): ReadyState.CLOSED {
|
||||||
|
return ReadyState.CLOSED
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open connections to relays.
|
* Open connections to relays.
|
||||||
*/
|
*/
|
||||||
@ -28,9 +44,14 @@ export class Nostr extends EventEmitter {
|
|||||||
* this method will only update it with the new options, and an exception will be thrown
|
* this method will only update it with the new options, and an exception will be thrown
|
||||||
* if no options are specified.
|
* if no options are specified.
|
||||||
*/
|
*/
|
||||||
open(url: URL | string, opts?: { read?: boolean; write?: boolean }): void {
|
open(
|
||||||
|
url: URL | string,
|
||||||
|
opts?: { read?: boolean; write?: boolean; fetchInfo?: boolean }
|
||||||
|
): void {
|
||||||
|
const connUrl = new URL(url)
|
||||||
|
|
||||||
// If the connection already exists, update the options.
|
// If the connection already exists, update the options.
|
||||||
const existingConn = this.#conns.get(url.toString())
|
const existingConn = this.#conns.get(connUrl.toString())
|
||||||
if (existingConn !== undefined) {
|
if (existingConn !== undefined) {
|
||||||
if (opts === undefined) {
|
if (opts === undefined) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -46,11 +67,16 @@ export class Nostr extends EventEmitter {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const connUrl = new URL(url)
|
// Fetch the relay info in parallel to opening the WebSocket connection.
|
||||||
|
const fetchInfo =
|
||||||
|
opts?.fetchInfo === false
|
||||||
|
? Promise.resolve({})
|
||||||
|
: fetchRelayInfo(connUrl).catch((e) => this.emit("error", e, this))
|
||||||
|
|
||||||
// If there is no existing connection, open a new one.
|
// If there is no existing connection, open a new one.
|
||||||
const conn = new Conn({
|
const conn = new Conn({
|
||||||
url: connUrl,
|
url: connUrl,
|
||||||
|
|
||||||
// Handle messages on this connection.
|
// Handle messages on this connection.
|
||||||
onMessage: async (msg) => {
|
onMessage: async (msg) => {
|
||||||
try {
|
try {
|
||||||
@ -87,8 +113,68 @@ export class Nostr extends EventEmitter {
|
|||||||
this.emit("error", err, this)
|
this.emit("error", err, this)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Forward "open" events.
|
|
||||||
onOpen: () => this.emit("open", connUrl, this),
|
// Handle "open" events.
|
||||||
|
onOpen: async () => {
|
||||||
|
// Update the connection readyState.
|
||||||
|
const conn = this.#conns.get(connUrl.toString())
|
||||||
|
if (conn === undefined) {
|
||||||
|
this.emit(
|
||||||
|
"error",
|
||||||
|
new Error(
|
||||||
|
`bug: expected connection to ${connUrl.toString()} to be in the map`
|
||||||
|
),
|
||||||
|
this
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if (conn.readyState !== ReadyState.CONNECTING) {
|
||||||
|
this.emit(
|
||||||
|
"error",
|
||||||
|
new Error(
|
||||||
|
`bug: expected connection to ${connUrl.toString()} to have readyState CONNECTING, got ${
|
||||||
|
conn.readyState
|
||||||
|
}`
|
||||||
|
),
|
||||||
|
this
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.#conns.set(connUrl.toString(), {
|
||||||
|
...conn,
|
||||||
|
readyState: ReadyState.OPEN,
|
||||||
|
info: await fetchInfo,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Forward the event to the user.
|
||||||
|
this.emit("open", connUrl, this)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Handle "close" events.
|
||||||
|
onClose: () => {
|
||||||
|
// Update the connection readyState.
|
||||||
|
const conn = this.#conns.get(connUrl.toString())
|
||||||
|
if (conn === undefined) {
|
||||||
|
this.emit(
|
||||||
|
"error",
|
||||||
|
new Error(
|
||||||
|
`bug: expected connection to ${connUrl.toString()} to be in the map`
|
||||||
|
),
|
||||||
|
this
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.#conns.set(connUrl.toString(), {
|
||||||
|
...conn,
|
||||||
|
readyState: ReadyState.CLOSED,
|
||||||
|
info:
|
||||||
|
conn.readyState === ReadyState.CONNECTING ? undefined : conn.info,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Forward the event to the user.
|
||||||
|
this.emit("close", connUrl, this)
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO If there is no error handler, this will silently swallow the error. Maybe have an
|
||||||
|
// #onError method which re-throws if emit() returns false? This should at least make
|
||||||
|
// some noise.
|
||||||
// Forward errors on this connection.
|
// Forward errors on this connection.
|
||||||
onError: (err) => this.emit("error", err, this),
|
onError: (err) => this.emit("error", err, this),
|
||||||
})
|
})
|
||||||
@ -102,11 +188,12 @@ export class Nostr extends EventEmitter {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#conns.set(url.toString(), {
|
this.#conns.set(connUrl.toString(), {
|
||||||
conn,
|
conn,
|
||||||
auth: false,
|
auth: false,
|
||||||
read: opts?.read ?? true,
|
read: opts?.read ?? true,
|
||||||
write: opts?.write ?? true,
|
write: opts?.write ?? true,
|
||||||
|
readyState: ReadyState.CONNECTING,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,23 +203,19 @@ export class Nostr extends EventEmitter {
|
|||||||
* @param url If specified, only close the connection to this relay. If the connection does
|
* @param url If specified, only close the connection to this relay. If the connection does
|
||||||
* not exist, an exception will be thrown. If this parameter is not specified, all connections
|
* not exist, an exception will be thrown. If this parameter is not specified, all connections
|
||||||
* will be closed.
|
* will be closed.
|
||||||
*
|
|
||||||
* TODO There needs to be a way to check connection state. isOpen(), isReady(), isClosing() maybe?
|
|
||||||
* Because of how WebSocket states work this isn't as simple as it seems.
|
|
||||||
*/
|
*/
|
||||||
close(url?: URL | string): void {
|
close(url?: URL | string): void {
|
||||||
if (url === undefined) {
|
if (url === undefined) {
|
||||||
for (const { conn } of this.#conns.values()) {
|
for (const { conn } of this.#conns.values()) {
|
||||||
conn.close()
|
conn.close()
|
||||||
}
|
}
|
||||||
this.#conns.clear()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const c = this.#conns.get(url.toString())
|
const connUrl = new URL(url)
|
||||||
|
const c = this.#conns.get(connUrl.toString())
|
||||||
if (c === undefined) {
|
if (c === undefined) {
|
||||||
throw new Error(`connection to ${url} doesn't exist`)
|
throw new Error(`connection to ${url} doesn't exist`)
|
||||||
}
|
}
|
||||||
this.#conns.delete(url.toString())
|
|
||||||
c.conn.close()
|
c.conn.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,7 +256,7 @@ export class Nostr extends EventEmitter {
|
|||||||
*
|
*
|
||||||
* TODO Reference subscribed()
|
* TODO Reference subscribed()
|
||||||
*/
|
*/
|
||||||
async unsubscribe(subscriptionId: SubscriptionId): Promise<void> {
|
unsubscribe(subscriptionId: SubscriptionId): void {
|
||||||
if (!this.#subscriptions.delete(subscriptionId)) {
|
if (!this.#subscriptions.delete(subscriptionId)) {
|
||||||
throw new Error(`subscription ${subscriptionId} does not exist`)
|
throw new Error(`subscription ${subscriptionId} does not exist`)
|
||||||
}
|
}
|
||||||
@ -191,7 +274,7 @@ export class Nostr extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Publish an event.
|
* Publish an event.
|
||||||
*/
|
*/
|
||||||
async publish(event: RawEvent): Promise<void> {
|
publish(event: RawEvent): void {
|
||||||
for (const { conn, write } of this.#conns.values()) {
|
for (const { conn, write } of this.#conns.values()) {
|
||||||
if (!write) {
|
if (!write) {
|
||||||
continue
|
continue
|
||||||
@ -202,9 +285,177 @@ export class Nostr extends EventEmitter {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the relays which this client has tried to open connections to.
|
||||||
|
*/
|
||||||
|
get relays(): Relay[] {
|
||||||
|
return [...this.#conns.entries()].map(([url, c]) => {
|
||||||
|
if (c.readyState === ReadyState.CONNECTING) {
|
||||||
|
return {
|
||||||
|
url: new URL(url),
|
||||||
|
readyState: ReadyState.CONNECTING,
|
||||||
|
}
|
||||||
|
} else if (c.readyState === ReadyState.OPEN) {
|
||||||
|
return {
|
||||||
|
url: new URL(url),
|
||||||
|
readyState: ReadyState.OPEN,
|
||||||
|
info: c.info,
|
||||||
|
}
|
||||||
|
} else if (c.readyState === ReadyState.CLOSED) {
|
||||||
|
return {
|
||||||
|
url: new URL(url),
|
||||||
|
readyState: ReadyState.CLOSED,
|
||||||
|
info: c.info,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error("bug: unknown readyState")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConnState {
|
// TODO Keep in mind this should be part of the public API of the lib
|
||||||
|
/**
|
||||||
|
* Fetch the NIP-11 relay info with some reasonable timeout. Throw an error if
|
||||||
|
* the info is invalid.
|
||||||
|
*/
|
||||||
|
export async function fetchRelayInfo(url: URL | string): Promise<RelayInfo> {
|
||||||
|
url = new URL(url.toString().trim().replace(/^ws/, "http"))
|
||||||
|
const abort = new AbortController()
|
||||||
|
const timeout = setTimeout(() => abort.abort(), 15_000)
|
||||||
|
const res = await fetch(url, {
|
||||||
|
signal: abort.signal,
|
||||||
|
headers: {
|
||||||
|
Accept: "application/nostr+json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
clearTimeout(timeout)
|
||||||
|
const info = await res.json()
|
||||||
|
// Validate the known fields in the JSON.
|
||||||
|
if (info.name !== undefined && typeof info.name !== "string") {
|
||||||
|
info.name = undefined
|
||||||
|
throw new ProtocolError(
|
||||||
|
`invalid relay info, expected "name" to be a string: ${JSON.stringify(
|
||||||
|
info
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (info.description !== undefined && typeof info.description !== "string") {
|
||||||
|
info.description = undefined
|
||||||
|
throw new ProtocolError(
|
||||||
|
`invalid relay info, expected "description" to be a string: ${JSON.stringify(
|
||||||
|
info
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (info.pubkey !== undefined && typeof info.pubkey !== "string") {
|
||||||
|
info.pubkey = undefined
|
||||||
|
throw new ProtocolError(
|
||||||
|
`invalid relay info, expected "pubkey" to be a string: ${JSON.stringify(
|
||||||
|
info
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (info.contact !== undefined && typeof info.contact !== "string") {
|
||||||
|
info.contact = undefined
|
||||||
|
throw new ProtocolError(
|
||||||
|
`invalid relay info, expected "contact" to be a string: ${JSON.stringify(
|
||||||
|
info
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (info.supported_nips !== undefined) {
|
||||||
|
if (info.supported_nips instanceof Array) {
|
||||||
|
if (info.supported_nips.some((e: unknown) => typeof e !== "number")) {
|
||||||
|
info.supported_nips = undefined
|
||||||
|
throw new ProtocolError(
|
||||||
|
`invalid relay info, expected "supported_nips" elements to be numbers: ${JSON.stringify(
|
||||||
|
info
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info.supported_nips = undefined
|
||||||
|
throw new ProtocolError(
|
||||||
|
`invalid relay info, expected "supported_nips" to be an array: ${JSON.stringify(
|
||||||
|
info
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (info.software !== undefined && typeof info.software !== "string") {
|
||||||
|
info.software = undefined
|
||||||
|
throw new ProtocolError(
|
||||||
|
`invalid relay info, expected "software" to be a string: ${JSON.stringify(
|
||||||
|
info
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (info.version !== undefined && typeof info.version !== "string") {
|
||||||
|
info.version = undefined
|
||||||
|
throw new ProtocolError(
|
||||||
|
`invalid relay info, expected "version" to be a string: ${JSON.stringify(
|
||||||
|
info
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state of a relay connection.
|
||||||
|
*/
|
||||||
|
export enum ReadyState {
|
||||||
|
/**
|
||||||
|
* The connection has not been established yet.
|
||||||
|
*/
|
||||||
|
CONNECTING = 0,
|
||||||
|
/**
|
||||||
|
* The connection has been established.
|
||||||
|
*/
|
||||||
|
OPEN = 1,
|
||||||
|
/**
|
||||||
|
* The connection has been closed, forcefully or gracefully, by either party.
|
||||||
|
*/
|
||||||
|
CLOSED = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Relay =
|
||||||
|
| {
|
||||||
|
url: URL
|
||||||
|
readyState: ReadyState.CONNECTING
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
url: URL
|
||||||
|
readyState: ReadyState.OPEN
|
||||||
|
info: RelayInfo
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
url: URL
|
||||||
|
readyState: ReadyState.CLOSED
|
||||||
|
/**
|
||||||
|
* If the relay is closed before the opening process is fully finished,
|
||||||
|
* the relay info may be undefined.
|
||||||
|
*/
|
||||||
|
info?: RelayInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The information that a relay broadcasts about itself as defined in NIP-11.
|
||||||
|
*/
|
||||||
|
export interface RelayInfo {
|
||||||
|
name?: string
|
||||||
|
description?: string
|
||||||
|
pubkey?: PublicKey
|
||||||
|
contact?: string
|
||||||
|
supported_nips?: number[]
|
||||||
|
software?: string
|
||||||
|
version?: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConnStateCommon {
|
||||||
conn: Conn
|
conn: Conn
|
||||||
/**
|
/**
|
||||||
* Has this connection been authenticated via NIP-44 AUTH?
|
* Has this connection been authenticated via NIP-44 AUTH?
|
||||||
@ -220,6 +471,21 @@ interface ConnState {
|
|||||||
write: boolean
|
write: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConnState = ConnStateCommon &
|
||||||
|
(
|
||||||
|
| {
|
||||||
|
readyState: ReadyState.CONNECTING
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
readyState: ReadyState.OPEN
|
||||||
|
info: RelayInfo
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
readyState: ReadyState.CLOSED
|
||||||
|
info?: RelayInfo
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A string uniquely identifying a client subscription.
|
* A string uniquely identifying a client subscription.
|
||||||
*/
|
*/
|
||||||
|
@ -90,7 +90,7 @@ export async function schnorrSign(data: Hex, priv: PrivateKey): Promise<Hex> {
|
|||||||
/**
|
/**
|
||||||
* Verify that the elliptic curve signature is correct.
|
* Verify that the elliptic curve signature is correct.
|
||||||
*/
|
*/
|
||||||
export async function schnorrVerify(
|
export function schnorrVerify(
|
||||||
sig: Hex,
|
sig: Hex,
|
||||||
data: Hex,
|
data: Hex,
|
||||||
key: PublicKey
|
key: PublicKey
|
||||||
|
@ -3,12 +3,13 @@ import { parsePublicKey } from "../src/crypto"
|
|||||||
import assert from "assert"
|
import assert from "assert"
|
||||||
import { setup } from "./setup"
|
import { setup } from "./setup"
|
||||||
|
|
||||||
describe("dm", async function () {
|
describe("dm", () => {
|
||||||
const message = "for your eyes only"
|
const message = "for your eyes only"
|
||||||
|
|
||||||
// Test that the intended recipient can receive and decrypt the direct message.
|
// Test that the intended recipient can receive and decrypt the direct message.
|
||||||
it("to intended recipient", (done) => {
|
it("to intended recipient", (done) => {
|
||||||
setup(done).then(
|
setup(
|
||||||
|
done,
|
||||||
({
|
({
|
||||||
publisher,
|
publisher,
|
||||||
publisherPubkey,
|
publisherPubkey,
|
||||||
@ -17,12 +18,12 @@ describe("dm", async function () {
|
|||||||
subscriberPubkey,
|
subscriberPubkey,
|
||||||
subscriberSecret,
|
subscriberSecret,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
done,
|
||||||
}) => {
|
}) => {
|
||||||
// Expect the direct message.
|
// Expect the direct message.
|
||||||
subscriber.on(
|
subscriber.on(
|
||||||
"event",
|
"event",
|
||||||
async ({ event, subscriptionId: actualSubscriptionId }, nostr) => {
|
async ({ event, subscriptionId: actualSubscriptionId }, nostr) => {
|
||||||
try {
|
|
||||||
assert.equal(nostr, subscriber)
|
assert.equal(nostr, subscriber)
|
||||||
assert.equal(event.kind, EventKind.DirectMessage)
|
assert.equal(event.kind, EventKind.DirectMessage)
|
||||||
assert.equal(event.pubkey, parsePublicKey(publisherPubkey))
|
assert.equal(event.pubkey, parsePublicKey(publisherPubkey))
|
||||||
@ -37,13 +38,7 @@ describe("dm", async function () {
|
|||||||
assert.equal(await event.getMessage(subscriberSecret), message)
|
assert.equal(await event.getMessage(subscriberSecret), message)
|
||||||
}
|
}
|
||||||
|
|
||||||
publisher.close()
|
|
||||||
subscriber.close()
|
|
||||||
|
|
||||||
done()
|
done()
|
||||||
} catch (e) {
|
|
||||||
done(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -67,7 +62,8 @@ describe("dm", async function () {
|
|||||||
|
|
||||||
// Test that an unintended recipient still receives the direct message event, but cannot decrypt it.
|
// Test that an unintended recipient still receives the direct message event, but cannot decrypt it.
|
||||||
it("to unintended recipient", (done) => {
|
it("to unintended recipient", (done) => {
|
||||||
setup(done).then(
|
setup(
|
||||||
|
done,
|
||||||
({
|
({
|
||||||
publisher,
|
publisher,
|
||||||
publisherPubkey,
|
publisherPubkey,
|
||||||
@ -75,6 +71,7 @@ describe("dm", async function () {
|
|||||||
subscriber,
|
subscriber,
|
||||||
subscriberSecret,
|
subscriberSecret,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
done,
|
||||||
}) => {
|
}) => {
|
||||||
const recipientPubkey =
|
const recipientPubkey =
|
||||||
"npub1u2dl3scpzuwyd45flgtm3wcjgv20j4azuzgevdpgtsvvmqzvc63sz327gc"
|
"npub1u2dl3scpzuwyd45flgtm3wcjgv20j4azuzgevdpgtsvvmqzvc63sz327gc"
|
||||||
@ -101,9 +98,6 @@ describe("dm", async function () {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
publisher.close()
|
|
||||||
subscriber.close()
|
|
||||||
|
|
||||||
done()
|
done()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
done(e)
|
done(e)
|
||||||
|
24
packages/nostr/test/ready-state.ts
Normal file
24
packages/nostr/test/ready-state.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import assert from "assert"
|
||||||
|
import { Nostr } from "../src/client"
|
||||||
|
import { relayUrl } from "./setup"
|
||||||
|
|
||||||
|
describe("ready state", () => {
|
||||||
|
it("ready state transitions", (done) => {
|
||||||
|
const nostr = new Nostr()
|
||||||
|
|
||||||
|
nostr.on("error", done)
|
||||||
|
|
||||||
|
nostr.on("open", () => {
|
||||||
|
assert.strictEqual(nostr.relays[0].readyState, Nostr.OPEN)
|
||||||
|
nostr.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
nostr.on("close", () => {
|
||||||
|
assert.strictEqual(nostr.relays[0].readyState, Nostr.CLOSED)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
|
||||||
|
nostr.open(relayUrl)
|
||||||
|
assert.strictEqual(nostr.relays[0].readyState, Nostr.CONNECTING)
|
||||||
|
})
|
||||||
|
})
|
26
packages/nostr/test/relay-info.ts
Normal file
26
packages/nostr/test/relay-info.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import assert from "assert"
|
||||||
|
import { Nostr } from "../src/client"
|
||||||
|
import { setup } from "./setup"
|
||||||
|
|
||||||
|
describe("relay info", () => {
|
||||||
|
it("fetching relay info", (done) => {
|
||||||
|
setup(done, ({ publisher, done }) => {
|
||||||
|
assert.strictEqual(publisher.relays.length, 1)
|
||||||
|
const relay = publisher.relays[0]
|
||||||
|
assert.strictEqual(relay.readyState, Nostr.OPEN)
|
||||||
|
if (relay.readyState === Nostr.OPEN) {
|
||||||
|
assert.strictEqual(relay.info.name, "nostr-rs-relay")
|
||||||
|
assert.strictEqual(relay.info.description, "nostr-rs-relay description")
|
||||||
|
assert.strictEqual(relay.info.pubkey, undefined)
|
||||||
|
assert.strictEqual(relay.info.contact, "mailto:contact@example.com")
|
||||||
|
assert.ok((relay.info.supported_nips?.length ?? 0) > 0)
|
||||||
|
assert.strictEqual(
|
||||||
|
relay.info.software,
|
||||||
|
"https://git.sr.ht/~gheartsfield/nostr-rs-relay"
|
||||||
|
)
|
||||||
|
assert.strictEqual(relay.info.version, "0.8.8")
|
||||||
|
}
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -1,6 +1,8 @@
|
|||||||
import { Nostr } from "../src/client"
|
import { Nostr } from "../src/client"
|
||||||
import { unixTimestamp } from "../src/util"
|
import { unixTimestamp } from "../src/util"
|
||||||
|
|
||||||
|
export const relayUrl = new URL("ws://localhost:12648")
|
||||||
|
|
||||||
export interface Setup {
|
export interface Setup {
|
||||||
publisher: Nostr
|
publisher: Nostr
|
||||||
publisherSecret: string
|
publisherSecret: string
|
||||||
@ -10,21 +12,36 @@ export interface Setup {
|
|||||||
subscriberPubkey: string
|
subscriberPubkey: string
|
||||||
timestamp: number
|
timestamp: number
|
||||||
url: URL
|
url: URL
|
||||||
|
/**
|
||||||
|
* Signal that the test is done. Call this instead of the callback provided by
|
||||||
|
* mocha. This will also take care of test cleanup.
|
||||||
|
*/
|
||||||
|
done: (e?: unknown) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setup(done: jest.DoneCallback): Promise<Setup> {
|
export async function setup(
|
||||||
|
done: jest.DoneCallback,
|
||||||
|
test: (setup: Setup) => void | Promise<void>
|
||||||
|
) {
|
||||||
|
try {
|
||||||
await restartRelay()
|
await restartRelay()
|
||||||
const publisher = new Nostr()
|
const publisher = new Nostr()
|
||||||
const subscriber = new Nostr()
|
const subscriber = new Nostr()
|
||||||
const url = new URL("ws://localhost:12648")
|
|
||||||
|
|
||||||
publisher.on("error", done)
|
publisher.on("error", done)
|
||||||
subscriber.on("error", done)
|
subscriber.on("error", done)
|
||||||
|
|
||||||
publisher.open(url)
|
const openPromise = Promise.all([
|
||||||
subscriber.open(url)
|
new Promise((resolve) => publisher.on("open", resolve)),
|
||||||
|
new Promise((resolve) => subscriber.on("open", resolve)),
|
||||||
|
])
|
||||||
|
|
||||||
return {
|
publisher.open(relayUrl)
|
||||||
|
subscriber.open(relayUrl)
|
||||||
|
|
||||||
|
await openPromise
|
||||||
|
|
||||||
|
const result = test({
|
||||||
publisher,
|
publisher,
|
||||||
publisherSecret:
|
publisherSecret:
|
||||||
"nsec15fnff4uxlgyu79ua3l7327w0wstrd6x565cx6zze78zgkktmr8vs90j363",
|
"nsec15fnff4uxlgyu79ua3l7327w0wstrd6x565cx6zze78zgkktmr8vs90j363",
|
||||||
@ -36,7 +53,18 @@ export async function setup(done: jest.DoneCallback): Promise<Setup> {
|
|||||||
subscriberPubkey:
|
subscriberPubkey:
|
||||||
"npub1mtwskm558jugtj724nsgf3jf80c5adl39ttydngrn48250l6xmjqa00yxd",
|
"npub1mtwskm558jugtj724nsgf3jf80c5adl39ttydngrn48250l6xmjqa00yxd",
|
||||||
timestamp: unixTimestamp(),
|
timestamp: unixTimestamp(),
|
||||||
url,
|
url: relayUrl,
|
||||||
|
done: (e?: unknown) => {
|
||||||
|
publisher.close()
|
||||||
|
subscriber.close()
|
||||||
|
done(e)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
await result
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
done(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +88,7 @@ async function restartRelay() {
|
|||||||
nostr.close()
|
nostr.close()
|
||||||
resolve(true)
|
resolve(true)
|
||||||
})
|
})
|
||||||
nostr.open("ws://localhost:12648")
|
nostr.open("ws://localhost:12648", { fetchInfo: false })
|
||||||
})
|
})
|
||||||
if (ok) {
|
if (ok) {
|
||||||
break
|
break
|
||||||
|
@ -3,18 +3,20 @@ import { parsePublicKey } from "../src/crypto"
|
|||||||
import assert from "assert"
|
import assert from "assert"
|
||||||
import { setup } from "./setup"
|
import { setup } from "./setup"
|
||||||
|
|
||||||
describe("text note", async function () {
|
describe("text note", () => {
|
||||||
const note = "hello world"
|
const note = "hello world"
|
||||||
|
|
||||||
// Test that a text note can be published by one client and received by the other.
|
// Test that a text note can be published by one client and received by the other.
|
||||||
it("publish and receive", (done) => {
|
it("publish and receive", (done) => {
|
||||||
setup(done).then(
|
setup(
|
||||||
|
done,
|
||||||
({
|
({
|
||||||
publisher,
|
publisher,
|
||||||
publisherSecret,
|
publisherSecret,
|
||||||
publisherPubkey,
|
publisherPubkey,
|
||||||
subscriber,
|
subscriber,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
done,
|
||||||
}) => {
|
}) => {
|
||||||
// Expect the test event.
|
// Expect the test event.
|
||||||
subscriber.on(
|
subscriber.on(
|
||||||
@ -26,10 +28,6 @@ describe("text note", async function () {
|
|||||||
assert.strictEqual(event.created_at, timestamp)
|
assert.strictEqual(event.created_at, timestamp)
|
||||||
assert.strictEqual(event.content, note)
|
assert.strictEqual(event.content, note)
|
||||||
assert.strictEqual(actualSubscriptionId, subscriptionId)
|
assert.strictEqual(actualSubscriptionId, subscriptionId)
|
||||||
|
|
||||||
subscriber.close()
|
|
||||||
publisher.close()
|
|
||||||
|
|
||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -53,7 +51,7 @@ describe("text note", async function () {
|
|||||||
|
|
||||||
// Test that a client interprets an "OK" message after publishing a text note.
|
// Test that a client interprets an "OK" message after publishing a text note.
|
||||||
it("publish and ok", function (done) {
|
it("publish and ok", function (done) {
|
||||||
setup(done).then(({ publisher, subscriber, publisherSecret, url }) => {
|
setup(done, ({ publisher, publisherSecret, url, done }) => {
|
||||||
// TODO No signEvent, have a convenient way to do this
|
// TODO No signEvent, have a convenient way to do this
|
||||||
signEvent(createTextNote(note), publisherSecret).then((event) => {
|
signEvent(createTextNote(note), publisherSecret).then((event) => {
|
||||||
publisher.on("ok", (params, nostr) => {
|
publisher.on("ok", (params, nostr) => {
|
||||||
@ -61,10 +59,6 @@ describe("text note", async function () {
|
|||||||
assert.equal(params.eventId, event.id)
|
assert.equal(params.eventId, event.id)
|
||||||
assert.equal(params.relay.toString(), url.toString())
|
assert.equal(params.relay.toString(), url.toString())
|
||||||
assert.equal(params.ok, true)
|
assert.equal(params.ok, true)
|
||||||
|
|
||||||
publisher.close()
|
|
||||||
subscriber.close()
|
|
||||||
|
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user