diff --git a/packages/app/src/session.ts b/packages/app/src/session.ts index d6734a6..b698bff 100644 --- a/packages/app/src/session.ts +++ b/packages/app/src/session.ts @@ -49,7 +49,12 @@ export const getSigner = memoize((session: Session) => { case "nip01": return new Nip01Signer(session.secret!) case "nip46": - return new Nip46Signer(Nip46Broker.get(session.pubkey, session.secret!, session.handler!)) + return new Nip46Signer( + Nip46Broker.get({ + secret: session.secret!, + handler: session.handler!, + }) + ) case "nip55": return new Nip55Signer(session.signer!) default: diff --git a/packages/net/src/Subscribe.ts b/packages/net/src/Subscribe.ts index 2c34081..e0ee69a 100644 --- a/packages/net/src/Subscribe.ts +++ b/packages/net/src/Subscribe.ts @@ -20,6 +20,7 @@ import {Connection} from './Connection' export enum SubscriptionEvent { Eose = "eose", + Send = "send", Close = "close", Event = "event", Complete = "complete", @@ -120,6 +121,7 @@ export const mergeSubscriptions = (subs: Subscription[]) => { propagateEvent(SubscriptionEvent.FailedFilter) propagateEvent(SubscriptionEvent.Invalid) propagateEvent(SubscriptionEvent.Eose) + propagateEvent(SubscriptionEvent.Send) propagateEvent(SubscriptionEvent.Close) } @@ -136,6 +138,7 @@ export const optimizeSubscriptions = (subs: Subscription[]) => { const abortedSubs = new Set() const closedSubs = new Set() const eosedSubs = new Set() + const sentSubs = new Set() const mergedSubs = [] for (const {relays, filters} of ctx.net.optimizeSubscriptions(group)) { @@ -201,6 +204,7 @@ export const optimizeSubscriptions = (subs: Subscription[]) => { } }) + propagateFinality(SubscriptionEvent.Send, sentSubs) propagateFinality(SubscriptionEvent.Eose, eosedSubs) propagateFinality(SubscriptionEvent.Close, closedSubs) propagateFinality(SubscriptionEvent.Complete, completedSubs) @@ -300,6 +304,8 @@ const _executeSubscription = (sub: Subscription) => { for (const filtersChunk of chunk(8, filters)) { subs.push(executor.subscribe(filtersChunk, {onEvent, onEose})) } + + emitter.emit(SubscriptionEvent.Send) }) } else { onComplete() diff --git a/packages/signer/src/signers/nip01.ts b/packages/signer/src/signers/nip01.ts index cb6b9a5..39eb605 100644 --- a/packages/signer/src/signers/nip01.ts +++ b/packages/signer/src/signers/nip01.ts @@ -2,19 +2,19 @@ import {StampedEvent} from '@welshman/util' import {nip04, nip44, own, hash, sign, getPubkey, ISigner, makeSecret} from "../util" export class Nip01Signer implements ISigner { - private pubkey: string + #pubkey: string constructor(private secret: string) { - this.pubkey = getPubkey(this.secret) + this.#pubkey = getPubkey(this.secret) } static fromSecret = (secret: string) => new Nip01Signer(secret) static ephemeral = () => new Nip01Signer(makeSecret()) - getPubkey = async () => this.pubkey + getPubkey = async () => this.#pubkey - sign = async (event: StampedEvent) => sign(hash(own(event, this.pubkey)), this.secret) + sign = async (event: StampedEvent) => sign(hash(own(event, this.#pubkey)), this.secret) nip04 = { encrypt: async (pubkey: string, message: string) => diff --git a/packages/signer/src/signers/nip46.ts b/packages/signer/src/signers/nip46.ts index 587697b..e8904c2 100644 --- a/packages/signer/src/signers/nip46.ts +++ b/packages/signer/src/signers/nip46.ts @@ -1,19 +1,23 @@ -import {finalizeEvent, getPublicKey} from "nostr-tools" -import {hexToBytes} from '@noble/hashes/utils' -import {Emitter, tryCatch, randomId, sleep, equals, now} from "@welshman/lib" +import {Emitter, tryCatch, randomId, equals, now} from "@welshman/lib" import {createEvent, TrustedEvent, StampedEvent, NOSTR_CONNECT} from "@welshman/util" import {subscribe, publish, Subscription} from "@welshman/net" -import {nip04, nip44, ISigner, decrypt, hash, own} from '../util' +import {ISigner, decrypt, hash, own} from '../util' import {Nip01Signer} from './nip01' -export type Algorithm = "nip04" | "nip44" +export type Nip46Algorithm = "nip04" | "nip44" export type Nip46Handler = { relays: string[] - pubkey?: string + pubkey: string domain?: string } +export type Nip46BrokerParams = { + secret: string + handler: Nip46Handler + algorithm?: Nip46Algorithm +} + export type Nip46Response = { id: string error?: string @@ -23,87 +27,89 @@ export type Nip46Response = { let singleton: Nip46Broker export class Nip46Broker extends Emitter { - #sub: Subscription #signer: ISigner - #ready = sleep(500) + #handler: Nip46Handler + #algorithm: Nip46Algorithm #closed = false - #connectResult: any + #connectResult?: string + #sub?: Subscription - static get(pubkey: string, secret: string, handler: Nip46Handler, algorithm: Algorithm = "nip04") { - if ( - singleton?.pubkey !== pubkey || - singleton?.secret !== secret || - !equals(singleton?.handler, handler) || - singleton?.algorithm !== algorithm - ) { + static get(params: Nip46BrokerParams) { + if (!singleton?.hasParams(params)) { singleton?.teardown() - singleton = new Nip46Broker(pubkey, secret, handler, algorithm) + singleton = new Nip46Broker(params) } return singleton } - constructor( - readonly pubkey: string, - readonly secret: string, - readonly handler: Nip46Handler, - readonly algorithm: Algorithm - ) { + constructor(private params: Nip46BrokerParams) { super() - this.#signer = new Nip01Signer(secret) - this.#sub = this.subscribe() + this.#handler = params.handler + this.#algorithm = params.algorithm || 'nip04' + this.#signer = new Nip01Signer(params.secret) } - subscribe = () => { - const sub = subscribe({ - relays: this.handler.relays, - filters: [ - { - since: now() - 30, - kinds: [NOSTR_CONNECT], - "#p": [getPublicKey(hexToBytes(this.secret))], - }, - ], - }) - - sub.emitter.on("event", async (url: string, e: TrustedEvent) => { - const json = await decrypt(this.#signer, e.pubkey, e.content) - const res = await tryCatch(() => JSON.parse(json)) - - if (!res.id) { - console.error(`Invalid nostr-connect response: ${json}`) - } - - if (res.result === "auth_url") { - this.emit(`auth-${res.id}`, res) - } else { - this.emit(`res-${res.id}`, res) - } - }) - - sub.emitter.on("complete", () => { - if (!this.#closed) { - this.#sub = this.subscribe() - } - }) - - return sub + hasParams(params: Nip46BrokerParams) { + return equals(this.params, params) } - request = async (method: string, params: string[], admin = false) => { - // nsecbunker has a race condition - await this.#ready + #subscribe = async () => { + const pubkey = await this.#signer.getPubkey() + + return new Promise(resolve => { + if (this.#sub) { + this.#sub.close() + } + + this.#sub = subscribe({ + relays: this.#handler.relays, + filters: [{since: now() - 30, kinds: [NOSTR_CONNECT], "#p": [pubkey]}], + }) + + this.#sub.emitter.on('send', resolve) + + this.#sub.emitter.on("event", async (url: string, e: TrustedEvent) => { + const json = await decrypt(this.#signer, e.pubkey, e.content) + const res = await tryCatch(() => JSON.parse(json)) + + if (!res.id) { + console.error(`Invalid nostr-connect response: ${json}`) + } + + if (res.result === "auth_url") { + this.emit(`auth-${res.id}`, res) + } else { + this.emit(`res-${res.id}`, res) + } + }) + + this.#sub.emitter.on("complete", () => { + this.#sub = undefined + }) + }) + } + + request = async (method: string, params: string[]) => { + if (this.#closed) { + throw new Error("Attempted to make a nip46 request with a closed broker") + } + + if (!this.#sub) { + await this.#subscribe() + } const id = randomId() - const pubkey = admin ? this.handler.pubkey! : this.pubkey + const recipient = this.#handler.pubkey const payload = JSON.stringify({id, method, params}) - const crypt = this.algorithm === "nip04" ? nip04 : nip44 - const content = await crypt.encrypt(pubkey, this.secret, payload) - const template = createEvent(NOSTR_CONNECT, {content, tags: [["p", pubkey]]}) - const event = finalizeEvent(template, this.secret as any) + const content = await this.#signer[this.#algorithm].encrypt(recipient, payload) + const template = createEvent(NOSTR_CONNECT, {content, tags: [["p", recipient]]}) - publish({event, relays: this.handler.relays}) + publish({ + relays: this.#handler.relays, + event: await this.#signer.sign(template), + }) this.once(`auth-${id}`, res => { window.open(res.error, "Coracle", "width=600,height=800,popup=yes") @@ -121,16 +127,16 @@ export class Nip46Broker extends Emitter { } createAccount = (username: string, perms = "") => { - if (!this.handler.domain) { + if (!this.#handler.domain) { throw new Error("Unable to create an account without a handler domain") } - return this.request("create_account", [username, this.handler.domain, "", perms], true) + return this.request("create_account", [username, this.#handler.domain, "", perms]) } connect = async (token = "", perms = "") => { if (!this.#connectResult) { - const params = [this.pubkey, token, perms] + const params = ["", token, perms] this.#connectResult = await this.request("connect", params) } @@ -138,6 +144,8 @@ export class Nip46Broker extends Emitter { return this.#connectResult === "ack" } + getPublicKey = () => this.request("get_public_key", []) + signEvent = async (event: StampedEvent) => { return JSON.parse(await this.request("sign_event", [JSON.stringify(event)]) as string) } @@ -165,12 +173,20 @@ export class Nip46Broker extends Emitter { } export class Nip46Signer implements ISigner { + #pubkey?: string + constructor(private broker: Nip46Broker) {} - getPubkey = async () => this.broker.pubkey + getPubkey = async () => { + if (!this.#pubkey) { + this.#pubkey = await this.broker.getPublicKey() + } - sign = (template: StampedEvent) => - this.broker.signEvent(hash(own(template, this.broker.pubkey))) + return this.#pubkey + } + + sign = async (template: StampedEvent) => + this.broker.signEvent(hash(own(template, await this.getPubkey()))) nip04 = { encrypt: this.broker.nip04Encrypt, diff --git a/watch.sh b/watch.sh index b9c3ea9..d4e4ef1 100755 --- a/watch.sh +++ b/watch.sh @@ -1,7 +1,5 @@ #!/bin/bash -./build.sh - for package in $(./get_packages.py); do npx onchange packages/$package -e '**/build/**' -k -- ./build_and_link.sh $package & done