diff --git a/packages/app/__tests__/thunk.test.ts b/packages/app/__tests__/thunk.test.ts index 037c0c5..f239dbb 100644 --- a/packages/app/__tests__/thunk.test.ts +++ b/packages/app/__tests__/thunk.test.ts @@ -1,6 +1,5 @@ import {PublishStatus, LOCAL_RELAY_URL} from "@welshman/net" -import {NOTE, DIRECT_MESSAGE, WRAP, makeEvent} from "@welshman/util" -import {getPubkey, makeSecret, prep} from "@welshman/signer" +import {NOTE, DIRECT_MESSAGE, WRAP, makeEvent, getPubkey, makeSecret, prep} from "@welshman/util" import {afterEach, beforeEach, describe, expect, it, vi} from "vitest" import {repository, tracker} from "../src/core" import {addSession, dropSession, makeNip01Session} from "../src/session" diff --git a/packages/app/src/session.ts b/packages/app/src/session.ts index a7ef5ba..3735c7e 100644 --- a/packages/app/src/session.ts +++ b/packages/app/src/session.ts @@ -8,6 +8,7 @@ import { HashedEvent, StampedEvent, SignedEvent, + getPubkey, } from "@welshman/util" import { Nip59, @@ -17,7 +18,6 @@ import { Nip07Signer, Nip01Signer, Nip55Signer, - getPubkey, ISigner, } from "@welshman/signer" import {WrapManager} from "@welshman/net" diff --git a/packages/app/src/thunk.ts b/packages/app/src/thunk.ts index 9d50aa2..214a392 100644 --- a/packages/app/src/thunk.ts +++ b/packages/app/src/thunk.ts @@ -12,7 +12,14 @@ import { nth, without, } from "@welshman/lib" -import {HashedEvent, EventTemplate, SignedEvent, isSignedEvent, WRAPPED_KINDS} from "@welshman/util" +import { + HashedEvent, + EventTemplate, + SignedEvent, + isSignedEvent, + WRAPPED_KINDS, + prep, +} from "@welshman/util" import { publish, PublishStatus, @@ -20,7 +27,7 @@ import { PublishOptions, PublishResultsByRelay, } from "@welshman/net" -import {ISigner, Nip59, prep} from "@welshman/signer" +import {ISigner, Nip59} from "@welshman/signer" import {repository, tracker} from "./core.js" import {pubkey, signer, wrapManager} from "./session.js" diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 01cad3f..17c0b49 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -2,4 +2,4 @@ export * from "./nodeviews/index.js" export * from "./extensions/index.js" export * from "./plugins/index.js" export {Editor, NodeViewProps} from "@tiptap/core" -export {UploadTask, FileAttributes} from "nostr-editor" +export {UploadTask, FileAttributes, editorProps} from "nostr-editor" diff --git a/packages/lib/src/Tools.ts b/packages/lib/src/Tools.ts index 8618b26..98ffe41 100644 --- a/packages/lib/src/Tools.ts +++ b/packages/lib/src/Tools.ts @@ -1032,6 +1032,13 @@ export const tryCatch = (f: () => T, onError?: (e: Error) => void): T | undef return undefined } +/** + * Throws an error with the given message + */ +export const thrower = (message: string) => () => { + throw new Error(message) +} + /** * Creates function that only executes once * @param f - Function to wrap diff --git a/packages/net/__tests__/adapter.test.ts b/packages/net/__tests__/adapter.test.ts index f2f42dd..7088e7d 100644 --- a/packages/net/__tests__/adapter.test.ts +++ b/packages/net/__tests__/adapter.test.ts @@ -1,6 +1,5 @@ import {describe, expect, it, vi, beforeEach, afterEach} from "vitest" -import {makeEvent} from "@welshman/util" -import {prep, getPubkey, makeSecret} from "@welshman/signer" +import {makeEvent, prep, getPubkey, makeSecret} from "@welshman/util" import {AdapterEvent, SocketAdapter, LocalAdapter, getAdapter} from "../src/adapter" import {Repository, LOCAL_RELAY_URL} from "../src/repository" import {ClientMessage, RelayMessage} from "../src/message" diff --git a/packages/signer/src/nip59.ts b/packages/signer/src/nip59.ts index 8c16117..09bcce0 100644 --- a/packages/signer/src/nip59.ts +++ b/packages/signer/src/nip59.ts @@ -1,5 +1,14 @@ -import {isHashedEvent, SignedEvent, HashedEvent, StampedEvent, WRAP, SEAL} from "@welshman/util" -import {prep, hash, decrypt, ISigner} from "./util.js" +import { + isHashedEvent, + SignedEvent, + HashedEvent, + StampedEvent, + WRAP, + SEAL, + prep, + hash, +} from "@welshman/util" +import {decrypt, ISigner} from "./util.js" import {Nip01Signer} from "./signers/nip01.js" export const seen = new Map() diff --git a/packages/signer/src/signers/nip01.ts b/packages/signer/src/signers/nip01.ts index 70318c1..f2e41a6 100644 --- a/packages/signer/src/signers/nip01.ts +++ b/packages/signer/src/signers/nip01.ts @@ -1,16 +1,5 @@ -import {StampedEvent} from "@welshman/util" -import { - nip04, - nip44, - own, - hash, - sign, - getPubkey, - ISigner, - SignOptions, - signWithOptions, - makeSecret, -} from "../util.js" +import {StampedEvent, own, hash, sign, getPubkey, makeSecret} from "@welshman/util" +import {nip04, nip44, ISigner, SignOptions, signWithOptions} from "../util.js" export class Nip01Signer implements ISigner { #pubkey: string diff --git a/packages/signer/src/signers/nip07.ts b/packages/signer/src/signers/nip07.ts index e3f1f23..29167b8 100644 --- a/packages/signer/src/signers/nip07.ts +++ b/packages/signer/src/signers/nip07.ts @@ -1,14 +1,6 @@ import {noop} from "@welshman/lib" -import {StampedEvent} from "@welshman/util" -import { - hash, - own, - signWithOptions, - SignOptions, - Sign, - ISigner, - EncryptionImplementation, -} from "../util.js" +import {StampedEvent, hash, own} from "@welshman/util" +import {signWithOptions, SignOptions, Sign, ISigner, EncryptionImplementation} from "../util.js" export type Nip07 = { signEvent: Sign diff --git a/packages/signer/src/signers/nip46.ts b/packages/signer/src/signers/nip46.ts index b1ce1bc..747205b 100644 --- a/packages/signer/src/signers/nip46.ts +++ b/packages/signer/src/signers/nip46.ts @@ -5,17 +5,11 @@ import { TrustedEvent, StampedEvent, NOSTR_CONNECT, -} from "@welshman/util" -import {publish, request, AdapterContext} from "@welshman/net" -import { - ISigner, - EncryptionImplementation, - signWithOptions, - SignOptions, - decrypt, hash, own, -} from "../util.js" +} from "@welshman/util" +import {publish, request, AdapterContext} from "@welshman/net" +import {ISigner, EncryptionImplementation, signWithOptions, SignOptions, decrypt} from "../util.js" import {Nip01Signer} from "./nip01.js" export type Nip46Context = { diff --git a/packages/signer/src/signers/nip55.ts b/packages/signer/src/signers/nip55.ts index 209b7b9..2ac14fe 100644 --- a/packages/signer/src/signers/nip55.ts +++ b/packages/signer/src/signers/nip55.ts @@ -1,7 +1,7 @@ import {NostrSignerPlugin, AppInfo} from "nostr-signer-capacitor-plugin" import * as nip19 from "nostr-tools/nip19" -import {SignedEvent, StampedEvent} from "@welshman/util" -import {hash, own, signWithOptions, SignOptions, ISigner} from "../util.js" +import {SignedEvent, StampedEvent, hash, own} from "@welshman/util" +import {signWithOptions, SignOptions, ISigner} from "../util.js" export const getNip55 = async (): Promise => { const {apps} = await NostrSignerPlugin.getInstalledSignerApps() diff --git a/packages/signer/src/util.ts b/packages/signer/src/util.ts index 339fed3..735e679 100644 --- a/packages/signer/src/util.ts +++ b/packages/signer/src/util.ts @@ -1,52 +1,8 @@ -import {schnorr} from "@noble/curves/secp256k1" -import {bytesToHex, hexToBytes} from "@noble/hashes/utils" +import {hexToBytes} from "@noble/hashes/utils" import * as nt04 from "nostr-tools/nip04" import * as nt44 from "nostr-tools/nip44" -import {generateSecretKey, getPublicKey, getEventHash} from "nostr-tools/pure" -import {Emitter, cached, now} from "@welshman/lib" -import { - SignedEvent, - HashedEvent, - EventTemplate, - StampedEvent, - OwnedEvent, - isStampedEvent, - isOwnedEvent, - isHashedEvent, -} from "@welshman/util" - -export const makeSecret = () => bytesToHex(generateSecretKey()) - -export const getPubkey = (secret: string) => getPublicKey(hexToBytes(secret)) - -export const getHash = (event: OwnedEvent) => getEventHash(event) - -export const getSig = (event: HashedEvent, secret: string) => - bytesToHex(schnorr.sign(event.id, secret)) - -export const stamp = (event: EventTemplate, created_at = now()) => ({...event, created_at}) - -export const own = (event: StampedEvent, pubkey: string) => ({...event, pubkey}) - -export const hash = (event: OwnedEvent) => ({...event, id: getHash(event)}) - -export const prep = (event: EventTemplate, pubkey: string, created_at = now()) => { - if (!isStampedEvent(event as StampedEvent)) { - event = stamp(event, created_at) - } - - if (!isOwnedEvent(event as OwnedEvent)) { - event = own(event as StampedEvent, pubkey) - } - - if (!isHashedEvent(event as HashedEvent)) { - event = hash(event as OwnedEvent) - } - - return event as HashedEvent -} - -export const sign = (event: HashedEvent, secret: string) => ({...event, sig: getSig(event, secret)}) +import {Emitter, cached} from "@welshman/lib" +import {SignedEvent, StampedEvent} from "@welshman/util" export const nip04 = { detect: (m: string) => m.includes("?iv="), diff --git a/packages/util/package.json b/packages/util/package.json index 54c255c..6b3e5fd 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -20,6 +20,7 @@ "prepublishOnly": "pnpm run build" }, "dependencies": { + "@noble/curves": "^1.8.1", "@types/ws": "^8.5.13", "@welshman/lib": "workspace:*", "js-base64": "^3.7.7", diff --git a/packages/util/src/Keys.ts b/packages/util/src/Keys.ts new file mode 100644 index 0000000..38f7451 --- /dev/null +++ b/packages/util/src/Keys.ts @@ -0,0 +1,46 @@ +import {schnorr} from "@noble/curves/secp256k1" +import {bytesToHex, hexToBytes} from "@noble/curves/abstract/utils" +import {generateSecretKey, getPublicKey, getEventHash} from "nostr-tools/pure" +import {now} from "@welshman/lib" +import { + HashedEvent, + EventTemplate, + StampedEvent, + OwnedEvent, + isStampedEvent, + isOwnedEvent, + isHashedEvent, +} from "./Events.js" + +export const makeSecret = () => bytesToHex(generateSecretKey()) + +export const getPubkey = (secret: string) => getPublicKey(hexToBytes(secret)) + +export const getHash = (event: OwnedEvent) => getEventHash(event) + +export const getSig = (event: HashedEvent, secret: string) => + bytesToHex(schnorr.sign(event.id, secret)) + +export const stamp = (event: EventTemplate, created_at = now()) => ({...event, created_at}) + +export const own = (event: StampedEvent, pubkey: string) => ({...event, pubkey}) + +export const hash = (event: OwnedEvent) => ({...event, id: getHash(event)}) + +export const sign = (event: HashedEvent, secret: string) => ({...event, sig: getSig(event, secret)}) + +export const prep = (event: EventTemplate, pubkey: string, created_at = now()) => { + if (!isStampedEvent(event as StampedEvent)) { + event = stamp(event, created_at) + } + + if (!isOwnedEvent(event as OwnedEvent)) { + event = own(event as StampedEvent, pubkey) + } + + if (!isHashedEvent(event as HashedEvent)) { + event = hash(event as OwnedEvent) + } + + return event as HashedEvent +} diff --git a/packages/util/src/Kinds.ts b/packages/util/src/Kinds.ts index 5b66395..8d6ba99 100644 --- a/packages/util/src/Kinds.ts +++ b/packages/util/src/Kinds.ts @@ -139,9 +139,16 @@ export const MESSAGING_RELAYS = 10050 export const BLOSSOM_SERVERS = 10063 export const FILE_SERVERS = 10096 export const RELAY_MEMBERS = 13534 +export const PROMENADE_REGISTER_ACCOUNT = 16430 export const LIGHTNING_PUB_RPC = 21000 export const CLIENT_AUTH = 22242 export const BLOSSOM_AUTH = 24242 +export const PROMENADE_SHARD_SHARE = 26428 +export const PROMENADE_SHARD_ACK = 26429 +export const PROMENADE_CONFIG = 26430 +export const PROMENADE_COMMIT = 26431 +export const PROMENADE_REQUEST = 26432 +export const PROMENADE_RESULT = 26433 export const RELAY_JOIN = 28934 export const RELAY_INVITE = 28935 export const RELAY_LEAVE = 28936 diff --git a/packages/util/src/Pow.ts b/packages/util/src/Pow.ts new file mode 100644 index 0000000..61e5fc1 --- /dev/null +++ b/packages/util/src/Pow.ts @@ -0,0 +1,151 @@ +import {call} from "@welshman/lib" +import {getTag} from "./Tags.js" +import {makeSecret, own, getPubkey} from "./Keys.js" +import {makeEvent, OwnedEvent, HashedEvent} from "./Events.js" + +export let benchmark = 0 + +export const benchmarkDifficulty = 15 + +export const estimateWork = (difficulty: number) => + Math.ceil(benchmark * Math.pow(2, difficulty - benchmarkDifficulty)) + +const workerCode = ` +// Utility function to convert bytes to hex +function bytesToHex(bytes) { + return Array.from(bytes) + .map(b => b.toString(16).padStart(2, '0')) + .join('') +} + +// Utility function to calculate proof of work from hash +function getPow(id) { + let count = 0 + + for (let i = 0; i < 32; i++) { + const nibble = id[i] + if (nibble === 0) { + count += 8 + } else { + count += Math.clz32(nibble) - 24 + break + } + } + + return count +} + +self.onmessage = async function (ev) { + const {event, difficulty, start = 0, step = 1} = ev.data + + let count = start + + const tag = ["nonce", count.toString(), difficulty.toString()] + + event.tags.push(tag) + + const encoder = new TextEncoder() + + while (true) { + count += step + tag[1] = count.toString() + + // Create the event array as specified by NIP-01 + const eventArray = [0, event.pubkey, event.created_at, event.kind, event.tags, event.content] + const eventString = JSON.stringify(eventArray) + + // Use Web Crypto API for SHA-256 hashing + const messageBuffer = encoder.encode(eventString) + const hashBuffer = await crypto.subtle.digest('SHA-256', messageBuffer) + const hashArray = new Uint8Array(hashBuffer) + + const pow = getPow(hashArray) + + if (pow >= difficulty) { + event.id = bytesToHex(hashArray) + break + } + } + + postMessage(event) +} +` + +const createPowWorker = (): Worker => { + if (typeof Worker === "undefined") { + throw new Error("Worker is not available in this environment") + } + + const blob = new Blob([workerCode], {type: "application/javascript"}) + const url = URL.createObjectURL(blob) + const worker = new Worker(url, {type: "module"}) + + // Clean up the blob URL after worker is created + URL.revokeObjectURL(url) + + return worker +} + +export type ProofOfWork = { + worker: Worker + result: Promise +} + +export const makePow = (event: OwnedEvent, difficulty: number): ProofOfWork => { + const worker = createPowWorker() + + const result = new Promise((resolve, reject) => { + worker.onmessage = (e: MessageEvent) => { + resolve(e.data) + worker.terminate() + } + + worker.onerror = (e: ErrorEvent) => { + reject(e.error || e) + worker.terminate() + } + + worker.postMessage({difficulty, event}) + }) + + return {worker, result} +} + +export const getPow = (event: HashedEvent): number => { + const difficulty = parseInt(getTag("nonce", event.tags)?.[2] || "") + + if (isNaN(difficulty)) return 0 + + let count = 0 + + // Convert hex string to array of bytes + for (let i = 0; i < event.id.length; i += 2) { + const byte = parseInt(event.id.slice(i, i + 2), 16) + if (byte === 0) { + count += 8 + } else { + count += Math.clz32(byte) - 24 + break + } + } + + return count >= difficulty ? difficulty : 0 +} + +// Generate a simple pow to estimate the device capacities +call(() => { + // Only run benchmark if Worker is available (browser environment) + if (typeof Worker === "undefined") { + return + } + + const secret = makeSecret() + const pubkey = getPubkey(secret) + const event = own(makeEvent(1, {}), pubkey) + const pow = makePow(event, benchmarkDifficulty) + const start = Date.now() + + pow.result.then(() => { + benchmark = Date.now() - start + }) +}) diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index 89de1df..bc26418 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -4,12 +4,14 @@ export * from "./Encryptable.js" export * from "./Events.js" export * from "./Filters.js" export * from "./Handler.js" +export * from "./Keys.js" export * from "./Kinds.js" export * from "./Links.js" export * from "./List.js" export * from "./Nip42.js" export * from "./Nip86.js" export * from "./Nip98.js" +export * from "./Pow.js" export * from "./Profile.js" export * from "./Relay.js" export * from "./Room.js" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de61e86..ad02825 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -439,6 +439,9 @@ importers: packages/util: dependencies: + '@noble/curves': + specifier: ^1.8.1 + version: 1.8.1 '@types/ws': specifier: ^8.5.13 version: 8.18.1