forked from Kieran/snort
Merge pull request #315 from ennmichael/nostr-package-1
`nostr` package part 1
This commit is contained in:
commit
cafd820fd9
@ -9,5 +9,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^4.9.5"
|
"typescript": "^4.9.5"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"semi": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
9
packages/nostr/src/error.ts
Normal file
9
packages/nostr/src/error.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* An error in the protocol. This error is thrown when a relay sends invalid or
|
||||||
|
* unexpected data, or otherwise behaves in an unexpected way.
|
||||||
|
*/
|
||||||
|
export class ProtocolError extends Error {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message)
|
||||||
|
}
|
||||||
|
}
|
174
packages/nostr/src/event.ts
Normal file
174
packages/nostr/src/event.ts
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import { ProtocolError } from "./error"
|
||||||
|
import { RawEvent } from "./raw"
|
||||||
|
import * as secp from "@noble/secp256k1"
|
||||||
|
import { PublicKey } from "./keypair"
|
||||||
|
import { parseHex } from "./util"
|
||||||
|
|
||||||
|
// TODO This file is missing proper documentation
|
||||||
|
// TODO Add remaining event types
|
||||||
|
|
||||||
|
export enum EventKind {
|
||||||
|
SetMetadata = 0, // NIP-01
|
||||||
|
TextNote = 1, // NIP-01
|
||||||
|
RecommendServer = 2, // NIP-01
|
||||||
|
ContactList = 3, // NIP-02
|
||||||
|
DirectMessage = 4, // NIP-04
|
||||||
|
Deletion = 5, // NIP-09
|
||||||
|
Repost = 6, // NIP-18
|
||||||
|
Reaction = 7, // NIP-25
|
||||||
|
Relays = 10002, // NIP-65
|
||||||
|
Auth = 22242, // NIP-42
|
||||||
|
PubkeyLists = 30000, // NIP-51a
|
||||||
|
NoteLists = 30001, // NIP-51b
|
||||||
|
TagLists = 30002, // NIP-51c
|
||||||
|
ZapRequest = 9734, // NIP 57
|
||||||
|
ZapReceipt = 9735, // NIP 57
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Event = SetMetadataEvent | TextNoteEvent | UnknownEvent
|
||||||
|
|
||||||
|
interface EventCommon {
|
||||||
|
pubkey: PublicKey
|
||||||
|
createdAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetMetadataEvent extends EventCommon {
|
||||||
|
kind: EventKind.SetMetadata
|
||||||
|
userMetadata: UserMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserMetadata {
|
||||||
|
name: string
|
||||||
|
about: string
|
||||||
|
picture: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TextNoteEvent extends EventCommon {
|
||||||
|
kind: EventKind.TextNote
|
||||||
|
note: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnknownEvent extends EventCommon {
|
||||||
|
kind: Exclude<EventKind, EventKind.SetMetadata | EventKind.TextNote>
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createEvent(raw: RawEvent): Promise<Event> {
|
||||||
|
const pubkey = new PublicKey(raw.pubkey)
|
||||||
|
const createdAt = new Date(raw.created_at * 1000)
|
||||||
|
const event = {
|
||||||
|
pubkey,
|
||||||
|
createdAt,
|
||||||
|
}
|
||||||
|
await checkSignature(raw, event)
|
||||||
|
return (
|
||||||
|
createSetMetadataEvent(raw, event) ??
|
||||||
|
createTextNodeEvent(raw, event) ??
|
||||||
|
createUnknownEvent(raw, event)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSetMetadataEvent(
|
||||||
|
raw: RawEvent,
|
||||||
|
event: EventCommon
|
||||||
|
): SetMetadataEvent | undefined {
|
||||||
|
if (raw.kind !== EventKind.SetMetadata) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const userMetadata = parseJson(raw.content)
|
||||||
|
if (
|
||||||
|
typeof userMetadata["name"] !== "string" ||
|
||||||
|
typeof userMetadata["about"] !== "string" ||
|
||||||
|
typeof userMetadata["picture"] !== "string"
|
||||||
|
) {
|
||||||
|
throw new ProtocolError(`invalid user metadata: ${userMetadata}`)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...event,
|
||||||
|
kind: EventKind.SetMetadata,
|
||||||
|
userMetadata,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTextNodeEvent(
|
||||||
|
raw: RawEvent,
|
||||||
|
event: EventCommon
|
||||||
|
): TextNoteEvent | undefined {
|
||||||
|
if (raw.kind !== EventKind.TextNote) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...event,
|
||||||
|
kind: EventKind.TextNote,
|
||||||
|
note: raw.content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUnknownEvent(raw: RawEvent, event: EventCommon): UnknownEvent {
|
||||||
|
return {
|
||||||
|
...event,
|
||||||
|
kind: raw.kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EventId {
|
||||||
|
#hex: string
|
||||||
|
|
||||||
|
constructor(hex: string | Uint8Array) {
|
||||||
|
this.#hex = parseHex(hex)
|
||||||
|
if (this.#hex.length !== 128) {
|
||||||
|
throw new ProtocolError(`invalid event id: ${this.#hex}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return this.#hex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkSignature(
|
||||||
|
raw: RawEvent,
|
||||||
|
event: EventCommon
|
||||||
|
): Promise<void> {
|
||||||
|
const id = serializeId(raw)
|
||||||
|
const bytes = await secp.schnorr.sign(id.toString(), event.pubkey.toString())
|
||||||
|
const hex = secp.utils.bytesToHex(bytes).toLowerCase()
|
||||||
|
if (hex.toString() !== raw.sig) {
|
||||||
|
throw new ProtocolError("invalid signature: ${hex}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function serializeId(raw: RawEvent): Promise<EventId> {
|
||||||
|
// It's not defined whether JSON.stringify produces a string with whitespace stripped.
|
||||||
|
// Building the JSON string manually this way ensures that there's no whitespace.
|
||||||
|
// In hindsight using JSON as a data format for hashing and signing is not the best
|
||||||
|
// design decision.
|
||||||
|
const serializedTags = `[${raw.tags
|
||||||
|
.map((tag) => `[${tag.map((v) => `"${v}"`).join(",")}]`)
|
||||||
|
.join(",")}]`
|
||||||
|
const serialized = `[0,"${raw.pubkey}",${raw.created_at},${raw.kind},${serializedTags},"${raw.content}"]`
|
||||||
|
const hash = await secp.utils.sha256(Uint8Array.from(charCodes(serialized)))
|
||||||
|
return new EventId(secp.utils.bytesToHex(hash).toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJson(data: string) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(data)
|
||||||
|
} catch (e) {
|
||||||
|
throw new ProtocolError(`invalid json: ${data}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function* charCodes(data: string): Iterable<number> {
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
yield data.charCodeAt(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO This is an example of how this API can be used, remove this later
|
||||||
|
function isItNice(e: Event): void {
|
||||||
|
if (e.kind === EventKind.SetMetadata) {
|
||||||
|
console.log(e.userMetadata)
|
||||||
|
} else if (e.kind === EventKind.TextNote) {
|
||||||
|
console.log(e.note)
|
||||||
|
}
|
||||||
|
}
|
@ -1,87 +1,3 @@
|
|||||||
export * from "./System";
|
export * from "./legacy"
|
||||||
export * from "./Connection";
|
|
||||||
export { default as EventKind } from "./EventKind";
|
|
||||||
export { Subscriptions } from "./Subscriptions";
|
|
||||||
export { default as Event } from "./Event";
|
|
||||||
export { default as Tag } from "./Tag";
|
|
||||||
export * from "./Links";
|
|
||||||
|
|
||||||
export type RawEvent = {
|
// TODO This file should only contain re-exports and only re-export what is needed
|
||||||
id: u256;
|
|
||||||
pubkey: HexKey;
|
|
||||||
created_at: number;
|
|
||||||
kind: number;
|
|
||||||
tags: string[][];
|
|
||||||
content: string;
|
|
||||||
sig: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface TaggedRawEvent extends RawEvent {
|
|
||||||
/**
|
|
||||||
* A list of relays this event was seen on
|
|
||||||
*/
|
|
||||||
relays: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Basic raw key as hex
|
|
||||||
*/
|
|
||||||
export type HexKey = string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional HexKey
|
|
||||||
*/
|
|
||||||
export type MaybeHexKey = HexKey | undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A 256bit hex id
|
|
||||||
*/
|
|
||||||
export type u256 = string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Raw REQ filter object
|
|
||||||
*/
|
|
||||||
export type RawReqFilter = {
|
|
||||||
ids?: u256[];
|
|
||||||
authors?: u256[];
|
|
||||||
kinds?: number[];
|
|
||||||
"#e"?: u256[];
|
|
||||||
"#p"?: u256[];
|
|
||||||
"#t"?: string[];
|
|
||||||
"#d"?: string[];
|
|
||||||
"#r"?: string[];
|
|
||||||
search?: string;
|
|
||||||
since?: number;
|
|
||||||
until?: number;
|
|
||||||
limit?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Medatadata event content
|
|
||||||
*/
|
|
||||||
export type UserMetadata = {
|
|
||||||
name?: string;
|
|
||||||
display_name?: string;
|
|
||||||
about?: string;
|
|
||||||
picture?: string;
|
|
||||||
website?: string;
|
|
||||||
banner?: string;
|
|
||||||
nip05?: string;
|
|
||||||
lud06?: string;
|
|
||||||
lud16?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NIP-51 list types
|
|
||||||
*/
|
|
||||||
export enum Lists {
|
|
||||||
Muted = "mute",
|
|
||||||
Pinned = "pin",
|
|
||||||
Bookmarked = "bookmark",
|
|
||||||
Followed = "follow",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FullRelaySettings {
|
|
||||||
url: string;
|
|
||||||
settings: { read: boolean; write: boolean };
|
|
||||||
}
|
|
||||||
|
38
packages/nostr/src/keypair.ts
Normal file
38
packages/nostr/src/keypair.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { ProtocolError } from "./error"
|
||||||
|
import { parseHex } from "./util"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A 32-byte secp256k1 public key.
|
||||||
|
*/
|
||||||
|
export class PublicKey {
|
||||||
|
#hex: string
|
||||||
|
|
||||||
|
constructor(hex: string | Uint8Array) {
|
||||||
|
this.#hex = parseHex(hex)
|
||||||
|
if (this.#hex.length !== 64) {
|
||||||
|
throw new ProtocolError(`invalid pubkey: ${hex}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return this.#hex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A 32-byte secp256k1 private key.
|
||||||
|
*/
|
||||||
|
export class PrivateKey {
|
||||||
|
#hex: string
|
||||||
|
|
||||||
|
constructor(hex: string | Uint8Array) {
|
||||||
|
this.#hex = parseHex(hex)
|
||||||
|
if (this.#hex.length !== 64) {
|
||||||
|
throw new ProtocolError(`invalid private key: ${this.#hex}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return this.#hex
|
||||||
|
}
|
||||||
|
}
|
1
packages/nostr/src/legacy/.prettierrc
Normal file
1
packages/nostr/src/legacy/.prettierrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
87
packages/nostr/src/legacy/index.ts
Normal file
87
packages/nostr/src/legacy/index.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
export * from "./System";
|
||||||
|
export * from "./Connection";
|
||||||
|
export { default as EventKind } from "./EventKind";
|
||||||
|
export { Subscriptions } from "./Subscriptions";
|
||||||
|
export { default as Event } from "./Event";
|
||||||
|
export { default as Tag } from "./Tag";
|
||||||
|
export * from "./Links";
|
||||||
|
|
||||||
|
export type RawEvent = {
|
||||||
|
id: u256;
|
||||||
|
pubkey: HexKey;
|
||||||
|
created_at: number;
|
||||||
|
kind: number;
|
||||||
|
tags: string[][];
|
||||||
|
content: string;
|
||||||
|
sig: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface TaggedRawEvent extends RawEvent {
|
||||||
|
/**
|
||||||
|
* A list of relays this event was seen on
|
||||||
|
*/
|
||||||
|
relays: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic raw key as hex
|
||||||
|
*/
|
||||||
|
export type HexKey = string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional HexKey
|
||||||
|
*/
|
||||||
|
export type MaybeHexKey = HexKey | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A 256bit hex id
|
||||||
|
*/
|
||||||
|
export type u256 = string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raw REQ filter object
|
||||||
|
*/
|
||||||
|
export type RawReqFilter = {
|
||||||
|
ids?: u256[];
|
||||||
|
authors?: u256[];
|
||||||
|
kinds?: number[];
|
||||||
|
"#e"?: u256[];
|
||||||
|
"#p"?: u256[];
|
||||||
|
"#t"?: string[];
|
||||||
|
"#d"?: string[];
|
||||||
|
"#r"?: string[];
|
||||||
|
search?: string;
|
||||||
|
since?: number;
|
||||||
|
until?: number;
|
||||||
|
limit?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Medatadata event content
|
||||||
|
*/
|
||||||
|
export type UserMetadata = {
|
||||||
|
name?: string;
|
||||||
|
display_name?: string;
|
||||||
|
about?: string;
|
||||||
|
picture?: string;
|
||||||
|
website?: string;
|
||||||
|
banner?: string;
|
||||||
|
nip05?: string;
|
||||||
|
lud06?: string;
|
||||||
|
lud16?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NIP-51 list types
|
||||||
|
*/
|
||||||
|
export enum Lists {
|
||||||
|
Muted = "mute",
|
||||||
|
Pinned = "pin",
|
||||||
|
Bookmarked = "bookmark",
|
||||||
|
Followed = "follow",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FullRelaySettings {
|
||||||
|
url: string;
|
||||||
|
settings: { read: boolean; write: boolean };
|
||||||
|
}
|
134
packages/nostr/src/nostr.ts
Normal file
134
packages/nostr/src/nostr.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import { ProtocolError } from "./error"
|
||||||
|
import { EventId, Event } from "./event"
|
||||||
|
import { RawEvent } from "./raw"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A nostr client.
|
||||||
|
*/
|
||||||
|
export class Nostr {
|
||||||
|
/**
|
||||||
|
* Open connections to relays.
|
||||||
|
*/
|
||||||
|
#conns: Conn[] = []
|
||||||
|
/**
|
||||||
|
* Is this client closed?
|
||||||
|
*/
|
||||||
|
#closed: boolean = false
|
||||||
|
/**
|
||||||
|
* Mapping of subscription IDs to corresponding filters.
|
||||||
|
*/
|
||||||
|
#subscriptions: Map<string, Filters> = new Map()
|
||||||
|
|
||||||
|
#eventCallbacks: EventCallback[] = []
|
||||||
|
#noticeCallbacks: NoticeCallback[] = []
|
||||||
|
#errorCallbacks: ErrorCallback[] = []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new callback for received events.
|
||||||
|
*/
|
||||||
|
onEvent(cb: EventCallback): void {
|
||||||
|
this.#eventCallbacks.push(cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new callback for received notices.
|
||||||
|
*/
|
||||||
|
onNotice(cb: NoticeCallback): void {
|
||||||
|
this.#noticeCallbacks.push(cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new callback for errors.
|
||||||
|
*/
|
||||||
|
onError(cb: ErrorCallback): void {
|
||||||
|
this.#errorCallbacks.push(cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect and start communicating with a relay. This method recreates all existing
|
||||||
|
* subscriptions on the new relay as well.
|
||||||
|
*/
|
||||||
|
async connect(relay: URL | string): Promise<void> {
|
||||||
|
this.#checkClosed()
|
||||||
|
throw new Error("todo try to connect and send subscriptions")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new subscription.
|
||||||
|
*
|
||||||
|
* @param subscriptionId An optional subscription ID, otherwise a random subscription ID will be used.
|
||||||
|
* @returns The subscription ID.
|
||||||
|
*/
|
||||||
|
async subscribe(
|
||||||
|
filters: Filters,
|
||||||
|
subscriptionId?: SubscriptionId | string
|
||||||
|
): Promise<SubscriptionId> {
|
||||||
|
this.#checkClosed()
|
||||||
|
throw new Error("todo subscribe to the relays and add the subscription")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a subscription.
|
||||||
|
*/
|
||||||
|
async unsubscribe(subscriptionId: SubscriptionId): Promise<SubscriptionId> {
|
||||||
|
this.#checkClosed()
|
||||||
|
throw new Error(
|
||||||
|
"todo unsubscribe from the relays and remove the subscription"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish an event.
|
||||||
|
*/
|
||||||
|
async publish(event: Event): Promise<void> {
|
||||||
|
this.#checkClosed()
|
||||||
|
throw new Error("todo")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close connections to all relays. This method can only be called once. After the
|
||||||
|
* connections have been closed, no other methods can be called.
|
||||||
|
*/
|
||||||
|
async close(): Promise<void> {}
|
||||||
|
|
||||||
|
#checkClosed() {
|
||||||
|
if (this.#closed) {
|
||||||
|
throw new Error("the client has been closed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A string uniquely identifying a client subscription.
|
||||||
|
*/
|
||||||
|
export class SubscriptionId {
|
||||||
|
#id: string
|
||||||
|
|
||||||
|
constructor(subscriptionId: string) {
|
||||||
|
this.#id = subscriptionId
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return this.#id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription filters.
|
||||||
|
*/
|
||||||
|
export interface Filters {}
|
||||||
|
|
||||||
|
export type EventCallback = (params: EventParams, nostr: Nostr) => unknown
|
||||||
|
export type NoticeCallback = (notice: string, nostr: Nostr) => unknown
|
||||||
|
export type ErrorCallback = (error: ProtocolError, nostr: Nostr) => unknown
|
||||||
|
|
||||||
|
export interface EventParams {
|
||||||
|
event: Event
|
||||||
|
id: EventId
|
||||||
|
raw: RawEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The connection to a relay.
|
||||||
|
*/
|
||||||
|
class Conn {}
|
41
packages/nostr/src/raw.ts
Normal file
41
packages/nostr/src/raw.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { ProtocolError } from "./error"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raw event to be transferred over the wire.
|
||||||
|
*/
|
||||||
|
export interface RawEvent {
|
||||||
|
id: string
|
||||||
|
pubkey: string
|
||||||
|
created_at: number
|
||||||
|
kind: number
|
||||||
|
tags: string[][]
|
||||||
|
content: string
|
||||||
|
sig: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseRawEvent(data: string): RawEvent {
|
||||||
|
const json = parseJson(data)
|
||||||
|
if (
|
||||||
|
typeof json["id"] !== "string" ||
|
||||||
|
typeof json["pubkey"] !== "string" ||
|
||||||
|
typeof json["created_at"] !== "number" ||
|
||||||
|
typeof json["kind"] !== "number" ||
|
||||||
|
!(json["tags"] instanceof Array) ||
|
||||||
|
!json["tags"].every(
|
||||||
|
(x) => x instanceof Array && x.every((y) => typeof y === "string")
|
||||||
|
) ||
|
||||||
|
typeof json["content"] !== "string" ||
|
||||||
|
typeof json["sig"] !== "string"
|
||||||
|
) {
|
||||||
|
throw new ProtocolError(`invalid event: ${data}`)
|
||||||
|
}
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJson(data: string) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(data)
|
||||||
|
} catch (e) {
|
||||||
|
throw new ProtocolError(`invalid event json: ${data}`)
|
||||||
|
}
|
||||||
|
}
|
22
packages/nostr/src/util.ts
Normal file
22
packages/nostr/src/util.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import * as secp from "@noble/secp256k1"
|
||||||
|
import { ProtocolError } from "./error"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that the input is a valid lowercase hex string.
|
||||||
|
*/
|
||||||
|
export function parseHex(hex: string | Uint8Array): string {
|
||||||
|
if (typeof hex === "string") {
|
||||||
|
const valid = "0123456789abcdef"
|
||||||
|
if (hex.length % 2 != 0) {
|
||||||
|
throw new ProtocolError(`invalid hex string: ${hex}`)
|
||||||
|
}
|
||||||
|
for (const c of hex) {
|
||||||
|
if (!valid.includes(c)) {
|
||||||
|
throw new ProtocolError(`invalid hex string: ${hex}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hex
|
||||||
|
} else {
|
||||||
|
return secp.utils.bytesToHex(hex).toLowerCase()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user