diff --git a/bun.lockb b/bun.lockb index 4fdf93f..62dc90d 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/core.test.ts b/core.test.ts new file mode 100644 index 0000000..2b9aba3 --- /dev/null +++ b/core.test.ts @@ -0,0 +1,293 @@ +import { describe, test, expect } from 'bun:test' + +import { + finalizeEvent, + serializeEvent, + getEventHash, + validateEvent, + verifyEvent, + verifiedSymbol, + getPublicKey, + generateSecretKey, +} from './pure.ts' +import { ShortTextNote } from './kinds.ts' +import { hexToBytes } from '@noble/hashes/utils' + +test('private key generation', () => { + expect(generateSecretKey()).toMatch(/[a-f0-9]{64}/) +}) + +test('public key generation', () => { + expect(getPublicKey(generateSecretKey())).toMatch(/[a-f0-9]{64}/) +}) + +test('public key from private key deterministic', () => { + let sk = generateSecretKey() + let pk = getPublicKey(sk) + + for (let i = 0; i < 5; i++) { + expect(getPublicKey(sk)).toEqual(pk) + } +}) + +describe('finishEvent', () => { + test('should create a signed event from a template', () => { + const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + const publicKey = getPublicKey(privateKey) + + const template = { + kind: ShortTextNote, + tags: [], + content: 'Hello, world!', + created_at: 1617932115, + } + + const event = finalizeEvent(template, privateKey) + + expect(event.kind).toEqual(template.kind) + expect(event.tags).toEqual(template.tags) + expect(event.content).toEqual(template.content) + expect(event.created_at).toEqual(template.created_at) + expect(event.pubkey).toEqual(publicKey) + expect(typeof event.id).toEqual('string') + expect(typeof event.sig).toEqual('string') + }) +}) + +describe('serializeEvent', () => { + test('should serialize a valid event object', () => { + const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + const publicKey = getPublicKey(privateKey) + + const unsignedEvent = { + pubkey: publicKey, + created_at: 1617932115, + kind: ShortTextNote, + tags: [], + content: 'Hello, world!', + } + + const serializedEvent = serializeEvent(unsignedEvent) + + expect(serializedEvent).toEqual( + JSON.stringify([ + 0, + publicKey, + unsignedEvent.created_at, + unsignedEvent.kind, + unsignedEvent.tags, + unsignedEvent.content, + ]), + ) + }) + + test('should throw an error for an invalid event object', () => { + const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + const publicKey = getPublicKey(privateKey) + + const invalidEvent = { + kind: ShortTextNote, + tags: [], + created_at: 1617932115, + pubkey: publicKey, // missing content + } + + expect(() => { + // @ts-expect-error + serializeEvent(invalidEvent) + }).toThrow("can't serialize event with wrong or missing properties") + }) +}) + +describe('getEventHash', () => { + test('should return the correct event hash', () => { + const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + const publicKey = getPublicKey(privateKey) + + const unsignedEvent = { + kind: ShortTextNote, + tags: [], + content: 'Hello, world!', + created_at: 1617932115, + pubkey: publicKey, + } + + const eventHash = getEventHash(unsignedEvent) + + expect(typeof eventHash).toEqual('string') + expect(eventHash.length).toEqual(64) + }) +}) + +describe('validateEvent', () => { + test('should return true for a valid event object', () => { + const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + const publicKey = getPublicKey(privateKey) + + const unsignedEvent = { + kind: ShortTextNote, + tags: [], + content: 'Hello, world!', + created_at: 1617932115, + pubkey: publicKey, + } + + const isValid = validateEvent(unsignedEvent) + + expect(isValid).toEqual(true) + }) + + test('should return false for a non object event', () => { + const nonObjectEvent = '' + const isValid = validateEvent(nonObjectEvent) + expect(isValid).toEqual(false) + }) + + test('should return false for an event object with missing properties', () => { + const invalidEvent = { + kind: ShortTextNote, + tags: [], + created_at: 1617932115, // missing content and pubkey + } + + const isValid = validateEvent(invalidEvent) + + expect(isValid).toEqual(false) + }) + + test('should return false for an empty object', () => { + const emptyObj = {} + + const isValid = validateEvent(emptyObj) + + expect(isValid).toEqual(false) + }) + + test('should return false for an object with invalid properties', () => { + const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + const publicKey = getPublicKey(privateKey) + + const invalidEvent = { + kind: 1, + tags: [], + created_at: '1617932115', // should be a number + pubkey: publicKey, + } + + const isValid = validateEvent(invalidEvent) + + expect(isValid).toEqual(false) + }) + + test('should return false for an object with an invalid public key', () => { + const invalidEvent = { + kind: 1, + tags: [], + content: 'Hello, world!', + created_at: 1617932115, + pubkey: 'invalid_pubkey', + } + + const isValid = validateEvent(invalidEvent) + + expect(isValid).toEqual(false) + }) + + test('should return false for an object with invalid tags', () => { + const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + const publicKey = getPublicKey(privateKey) + + const invalidEvent = { + kind: 1, + tags: {}, // should be an array + content: 'Hello, world!', + created_at: 1617932115, + pubkey: publicKey, + } + + const isValid = validateEvent(invalidEvent) + + expect(isValid).toEqual(false) + }) +}) + +describe('verifySignature', () => { + test('should return true for a valid event signature', () => { + const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + const event = finalizeEvent( + { + kind: ShortTextNote, + tags: [], + content: 'Hello, world!', + created_at: 1617932115, + }, + privateKey, + ) + + const isValid = verifyEvent(event) + expect(isValid).toEqual(true) + }) + + test('should return false for an invalid event signature', () => { + const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + const { [verifiedSymbol]: _, ...event } = finalizeEvent( + { + kind: ShortTextNote, + tags: [], + content: 'Hello, world!', + created_at: 1617932115, + }, + privateKey, + ) + + // tamper with the signature + event.sig = event.sig.replace(/^.{3}/g, '666') + + const isValid = verifyEvent(event) + expect(isValid).toEqual(false) + }) + + test('should return false when verifying an event with a different private key', () => { + const privateKey1 = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + + const privateKey2 = hexToBytes('5b4a34f4e4b23c63ad55a35e3f84a3b53d96dbf266edf521a8358f71d19cbf67') + const publicKey2 = getPublicKey(privateKey2) + + const { [verifiedSymbol]: _, ...event } = finalizeEvent( + { + kind: ShortTextNote, + tags: [], + content: 'Hello, world!', + created_at: 1617932115, + }, + privateKey1, + ) + + // verify with different private key + const isValid = verifyEvent({ + ...event, + pubkey: publicKey2, + }) + expect(isValid).toEqual(false) + }) + + test('should return false for an invalid event id', () => { + const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + + const { [verifiedSymbol]: _, ...event } = finalizeEvent( + { + kind: 1, + tags: [], + content: 'Hello, world!', + created_at: 1617932115, + }, + privateKey, + ) + + // tamper with the id + event.id = event.id.replace(/^.{3}/g, '666') + + const isValid = verifyEvent(event) + expect(isValid).toEqual(false) + }) +}) diff --git a/core.ts b/core.ts new file mode 100644 index 0000000..462e8ce --- /dev/null +++ b/core.ts @@ -0,0 +1,50 @@ +export interface Nostr { + generateSecretKey(): Uint8Array + getPublicKey(secretKey: Uint8Array): string + finalizeEvent(event: EventTemplate, secretKey: Uint8Array): VerifiedEvent + verifyEvent(event: Event): event is VerifiedEvent +} + +/** Designates a verified event signature. */ +export const verifiedSymbol = Symbol('verified') + +export interface Event { + kind: number + tags: string[][] + content: string + created_at: number + pubkey: string + id: string + sig: string + [verifiedSymbol]?: boolean +} + +export type EventTemplate = Pick +export type UnsignedEvent = Pick + +/** An event whose signature has been verified. */ +export interface VerifiedEvent extends Event { + [verifiedSymbol]: true +} + +const isRecord = (obj: unknown): obj is Record => obj instanceof Object + +export function validateEvent(event: T): event is T & UnsignedEvent { + if (!isRecord(event)) return false + if (typeof event.kind !== 'number') return false + if (typeof event.content !== 'string') return false + if (typeof event.created_at !== 'number') return false + if (typeof event.pubkey !== 'string') return false + if (!event.pubkey.match(/^[a-f0-9]{64}$/)) return false + + if (!Array.isArray(event.tags)) return false + for (let i = 0; i < event.tags.length; i++) { + let tag = event.tags[i] + if (!Array.isArray(tag)) return false + for (let j = 0; j < tag.length; j++) { + if (typeof tag[j] === 'object') return false + } + } + + return true +} diff --git a/event.test.ts b/event.test.ts deleted file mode 100644 index 42352c6..0000000 --- a/event.test.ts +++ /dev/null @@ -1,339 +0,0 @@ -import { describe, test, expect } from 'bun:test' - -import { - finishEvent, - serializeEvent, - getEventHash, - validateEvent, - verifySignature, - getSignature, - verifiedSymbol, -} from './event.ts' -import { getPublicKey } from './keys.ts' -import { ShortTextNote } from './kinds.ts' - -describe('Event', () => { - describe('finishEvent', () => { - test('should create a signed event from a template', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - const publicKey = getPublicKey(privateKey) - - const template = { - kind: ShortTextNote, - tags: [], - content: 'Hello, world!', - created_at: 1617932115, - } - - const event = finishEvent(template, privateKey) - - expect(event.kind).toEqual(template.kind) - expect(event.tags).toEqual(template.tags) - expect(event.content).toEqual(template.content) - expect(event.created_at).toEqual(template.created_at) - expect(event.pubkey).toEqual(publicKey) - expect(typeof event.id).toEqual('string') - expect(typeof event.sig).toEqual('string') - }) - }) - - describe('serializeEvent', () => { - test('should serialize a valid event object', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - const publicKey = getPublicKey(privateKey) - - const unsignedEvent = { - pubkey: publicKey, - created_at: 1617932115, - kind: ShortTextNote, - tags: [], - content: 'Hello, world!', - } - - const serializedEvent = serializeEvent(unsignedEvent) - - expect(serializedEvent).toEqual( - JSON.stringify([ - 0, - publicKey, - unsignedEvent.created_at, - unsignedEvent.kind, - unsignedEvent.tags, - unsignedEvent.content, - ]), - ) - }) - - test('should throw an error for an invalid event object', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - const publicKey = getPublicKey(privateKey) - - const invalidEvent = { - kind: ShortTextNote, - tags: [], - created_at: 1617932115, - pubkey: publicKey, // missing content - } - - expect(() => { - // @ts-expect-error - serializeEvent(invalidEvent) - }).toThrow("can't serialize event with wrong or missing properties") - }) - }) - - describe('getEventHash', () => { - test('should return the correct event hash', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - const publicKey = getPublicKey(privateKey) - - const unsignedEvent = { - kind: ShortTextNote, - tags: [], - content: 'Hello, world!', - created_at: 1617932115, - pubkey: publicKey, - } - - const eventHash = getEventHash(unsignedEvent) - - expect(typeof eventHash).toEqual('string') - expect(eventHash.length).toEqual(64) - }) - }) - - describe('validateEvent', () => { - test('should return true for a valid event object', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - const publicKey = getPublicKey(privateKey) - - const unsignedEvent = { - kind: ShortTextNote, - tags: [], - content: 'Hello, world!', - created_at: 1617932115, - pubkey: publicKey, - } - - const isValid = validateEvent(unsignedEvent) - - expect(isValid).toEqual(true) - }) - - test('should return false for a non object event', () => { - const nonObjectEvent = '' - const isValid = validateEvent(nonObjectEvent) - expect(isValid).toEqual(false) - }) - - test('should return false for an event object with missing properties', () => { - const invalidEvent = { - kind: ShortTextNote, - tags: [], - created_at: 1617932115, // missing content and pubkey - } - - const isValid = validateEvent(invalidEvent) - - expect(isValid).toEqual(false) - }) - - test('should return false for an empty object', () => { - const emptyObj = {} - - const isValid = validateEvent(emptyObj) - - expect(isValid).toEqual(false) - }) - - test('should return false for an object with invalid properties', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - const publicKey = getPublicKey(privateKey) - - const invalidEvent = { - kind: 1, - tags: [], - created_at: '1617932115', // should be a number - pubkey: publicKey, - } - - const isValid = validateEvent(invalidEvent) - - expect(isValid).toEqual(false) - }) - - test('should return false for an object with an invalid public key', () => { - const invalidEvent = { - kind: 1, - tags: [], - content: 'Hello, world!', - created_at: 1617932115, - pubkey: 'invalid_pubkey', - } - - const isValid = validateEvent(invalidEvent) - - expect(isValid).toEqual(false) - }) - - test('should return false for an object with invalid tags', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - const publicKey = getPublicKey(privateKey) - - const invalidEvent = { - kind: 1, - tags: {}, // should be an array - content: 'Hello, world!', - created_at: 1617932115, - pubkey: publicKey, - } - - const isValid = validateEvent(invalidEvent) - - expect(isValid).toEqual(false) - }) - }) - - describe('verifySignature', () => { - test('should return true for a valid event signature', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - - const event = finishEvent( - { - kind: ShortTextNote, - tags: [], - content: 'Hello, world!', - created_at: 1617932115, - }, - privateKey, - ) - - const isValid = verifySignature(event) - - expect(isValid).toEqual(true) - }) - - test('should return false for an invalid event signature', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - - const { [verifiedSymbol]: _, ...event } = finishEvent( - { - kind: ShortTextNote, - tags: [], - content: 'Hello, world!', - created_at: 1617932115, - }, - privateKey, - ) - - // tamper with the signature - event.sig = event.sig.replace(/^.{3}/g, '666') - - const isValid = verifySignature(event) - - expect(isValid).toEqual(false) - }) - - test('should return false when verifying an event with a different private key', () => { - const privateKey1 = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - - const privateKey2 = '5b4a34f4e4b23c63ad55a35e3f84a3b53d96dbf266edf521a8358f71d19cbf67' - const publicKey2 = getPublicKey(privateKey2) - - const { [verifiedSymbol]: _, ...event } = finishEvent( - { - kind: ShortTextNote, - tags: [], - content: 'Hello, world!', - created_at: 1617932115, - }, - privateKey1, - ) - - // verify with different private key - const isValid = verifySignature({ - ...event, - pubkey: publicKey2, - }) - - expect(isValid).toEqual(false) - }) - - test('should return false for an invalid event id', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - - const { [verifiedSymbol]: _, ...event } = finishEvent( - { - kind: 1, - tags: [], - content: 'Hello, world!', - created_at: 1617932115, - }, - privateKey, - ) - - // tamper with the id - event.id = event.id.replace(/^.{3}/g, '666') - - const isValid = verifySignature(event) - - expect(isValid).toEqual(false) - }) - }) - - describe('getSignature', () => { - test('should produce the correct signature for an event object', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - const publicKey = getPublicKey(privateKey) - - const unsignedEvent = { - kind: ShortTextNote, - tags: [], - content: 'Hello, world!', - created_at: 1617932115, - pubkey: publicKey, - } - - const sig = getSignature(unsignedEvent, privateKey) - - // verify the signature - const isValid = verifySignature({ - ...unsignedEvent, - id: getEventHash(unsignedEvent), - sig, - }) - - expect(typeof sig).toEqual('string') - expect(sig.length).toEqual(128) - expect(isValid).toEqual(true) - }) - - test('should not sign an event with different private key', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - const publicKey = getPublicKey(privateKey) - - const wrongPrivateKey = 'a91e2a9d9e0f70f0877bea0dbf034e8f95d7392a27a7f07da0d14b9e9d456be7' - - const unsignedEvent = { - kind: ShortTextNote, - tags: [], - content: 'Hello, world!', - created_at: 1617932115, - pubkey: publicKey, - } - - const sig = getSignature(unsignedEvent, wrongPrivateKey) - - // verify the signature - // @ts-expect-error - const isValid = verifySignature({ - ...unsignedEvent, - sig, - }) - - expect(typeof sig).toEqual('string') - expect(sig.length).toEqual(128) - expect(isValid).toEqual(false) - }) - }) -}) diff --git a/event.ts b/event.ts deleted file mode 100644 index f31248a..0000000 --- a/event.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { schnorr } from '@noble/curves/secp256k1' -import { sha256 } from '@noble/hashes/sha256' -import { bytesToHex } from '@noble/hashes/utils' - -import { getPublicKey } from './keys.ts' -import { utf8Encoder } from './utils.ts' - -/** Designates a verified event signature. */ -export const verifiedSymbol = Symbol('verified') - -export interface Event { - kind: number - tags: string[][] - content: string - created_at: number - pubkey: string - id: string - sig: string - [verifiedSymbol]?: boolean -} - -export type EventTemplate = Pick -export type UnsignedEvent = Pick - -/** An event whose signature has been verified. */ -export interface VerifiedEvent extends Event { - [verifiedSymbol]: true -} - -export function finishEvent(t: EventTemplate, privateKey: string): VerifiedEvent { - const event = t as VerifiedEvent - event.pubkey = getPublicKey(privateKey) - event.id = getEventHash(event) - event.sig = getSignature(event, privateKey) - event[verifiedSymbol] = true - return event -} - -export function serializeEvent(evt: UnsignedEvent): string { - if (!validateEvent(evt)) throw new Error("can't serialize event with wrong or missing properties") - - return JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content]) -} - -export function getEventHash(event: UnsignedEvent): string { - let eventHash = sha256(utf8Encoder.encode(serializeEvent(event))) - return bytesToHex(eventHash) -} - -const isRecord = (obj: unknown): obj is Record => obj instanceof Object - -export function validateEvent(event: T): event is T & UnsignedEvent { - if (!isRecord(event)) return false - if (typeof event.kind !== 'number') return false - if (typeof event.content !== 'string') return false - if (typeof event.created_at !== 'number') return false - if (typeof event.pubkey !== 'string') return false - if (!event.pubkey.match(/^[a-f0-9]{64}$/)) return false - - if (!Array.isArray(event.tags)) return false - for (let i = 0; i < event.tags.length; i++) { - let tag = event.tags[i] - if (!Array.isArray(tag)) return false - for (let j = 0; j < tag.length; j++) { - if (typeof tag[j] === 'object') return false - } - } - - return true -} - -/** Verify the event's signature. This function mutates the event with a `verified` symbol, making it idempotent. */ -export function verifySignature(event: Event): boolean { - if (typeof event[verifiedSymbol] === 'boolean') return event[verifiedSymbol] - - const hash = getEventHash(event) - if (hash !== event.id) { - return (event[verifiedSymbol] = false) - } - - try { - return (event[verifiedSymbol] = schnorr.verify(event.sig, hash, event.pubkey)) - } catch (err) { - return (event[verifiedSymbol] = false) - } -} - -/** @deprecated Use `getSignature` instead. */ -export function signEvent(event: UnsignedEvent, key: string): string { - console.warn( - 'nostr-tools: `signEvent` is deprecated and will be removed or changed in the future. Please use `getSignature` instead.', - ) - return getSignature(event, key) -} - -/** Calculate the signature for an event. */ -export function getSignature(event: UnsignedEvent, key: string): string { - return bytesToHex(schnorr.sign(getEventHash(event), key)) -} diff --git a/keys.test.ts b/keys.test.ts deleted file mode 100644 index 05df427..0000000 --- a/keys.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { test, expect } from 'bun:test' -import { generatePrivateKey, getPublicKey } from './keys.ts' - -test('private key generation', () => { - expect(generatePrivateKey()).toMatch(/[a-f0-9]{64}/) -}) - -test('public key generation', () => { - expect(getPublicKey(generatePrivateKey())).toMatch(/[a-f0-9]{64}/) -}) - -test('public key from private key deterministic', () => { - let sk = generatePrivateKey() - let pk = getPublicKey(sk) - - for (let i = 0; i < 5; i++) { - expect(getPublicKey(sk)).toEqual(pk) - } -}) diff --git a/keys.ts b/keys.ts deleted file mode 100644 index 03edd1c..0000000 --- a/keys.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { schnorr } from '@noble/curves/secp256k1' -import { bytesToHex } from '@noble/hashes/utils' - -export function generatePrivateKey(): string { - return bytesToHex(schnorr.utils.randomPrivateKey()) -} - -export function getPublicKey(privateKey: string): string { - return bytesToHex(schnorr.getPublicKey(privateKey)) -} diff --git a/package.json b/package.json index d1ef8ed..efa395c 100644 --- a/package.json +++ b/package.json @@ -152,7 +152,8 @@ "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", - "@scure/bip39": "1.2.1" + "@scure/bip39": "1.2.1", + "nostr-wasm": "v0.0.3" }, "peerDependencies": { "typescript": ">=5.0.0" diff --git a/pure.ts b/pure.ts new file mode 100644 index 0000000..e8af48d --- /dev/null +++ b/pure.ts @@ -0,0 +1,59 @@ +import { schnorr } from '@noble/curves/secp256k1' +import { bytesToHex } from '@noble/hashes/utils' +import { Nostr, Event, EventTemplate, UnsignedEvent, VerifiedEvent, verifiedSymbol, validateEvent } from './core' +import { sha256 } from '@noble/hashes/sha256' + +import { utf8Encoder } from './utils.ts' + +class JS implements Nostr { + generateSecretKey(): Uint8Array { + return schnorr.utils.randomPrivateKey() + } + getPublicKey(secretKey: Uint8Array): string { + return bytesToHex(schnorr.getPublicKey(secretKey)) + } + finalizeEvent(t: EventTemplate, secretKey: Uint8Array): VerifiedEvent { + const event = t as VerifiedEvent + event.pubkey = this.getPublicKey(secretKey) + event.id = getEventHash(event) + event.sig = bytesToHex(schnorr.sign(getEventHash(event), secretKey)) + event[verifiedSymbol] = true + return event + } + verifyEvent(event: Event): event is VerifiedEvent { + if (typeof event[verifiedSymbol] === 'boolean') return event[verifiedSymbol] + + const hash = getEventHash(event) + if (hash !== event.id) { + event[verifiedSymbol] = false + return false + } + + try { + const valid = schnorr.verify(event.sig, hash, event.pubkey) + event[verifiedSymbol] = valid + return valid + } catch (err) { + event[verifiedSymbol] = false + return false + } + } +} + +export function serializeEvent(evt: UnsignedEvent): string { + if (!validateEvent(evt)) throw new Error("can't serialize event with wrong or missing properties") + return JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content]) +} + +export function getEventHash(event: UnsignedEvent): string { + let eventHash = sha256(utf8Encoder.encode(serializeEvent(event))) + return bytesToHex(eventHash) +} + +const i = new JS() + +export const generateSecretKey = i.generateSecretKey +export const getPublicKey = i.getPublicKey +export const finalizeEvent = i.finalizeEvent +export const verifyEvent = i.verifyEvent +export * from './core.ts' diff --git a/wasm.ts b/wasm.ts new file mode 100644 index 0000000..e4c1fe6 --- /dev/null +++ b/wasm.ts @@ -0,0 +1,38 @@ +import { bytesToHex } from '@noble/hashes/utils' +import { Nostr as NostrWasm } from 'nostr-wasm' +import { EventTemplate, Event, Nostr, VerifiedEvent, verifiedSymbol } from './core' + +let nw: NostrWasm + +export function setNostrWasm(x: NostrWasm) { + nw = x +} + +class Wasm implements Nostr { + generateSecretKey(): Uint8Array { + return nw.generateSecretKey() + } + getPublicKey(secretKey: Uint8Array): string { + return bytesToHex(nw.getPublicKey(secretKey)) + } + finalizeEvent(t: EventTemplate, secretKey: Uint8Array): VerifiedEvent { + nw.finalizeEvent(t as any, secretKey) + return t as VerifiedEvent + } + verifyEvent(event: Event): event is VerifiedEvent { + try { + nw.verifyEvent(event) + event[verifiedSymbol] = true + return true + } catch (err) { + return false + } + } +} + +const i = new Wasm() +export const generateSecretKey = i.generateSecretKey +export const getPublicKey = i.getPublicKey +export const finalizeEvent = i.finalizeEvent +export const verifyEvent = i.verifyEvent +export * from './core.ts'