delete legacy code from nostr pkg
This commit is contained in:
parent
f86053c14f
commit
86ec7f41d7
@ -1,4 +1,3 @@
|
|||||||
export * from "./legacy"
|
|
||||||
import "./nostr-object"
|
import "./nostr-object"
|
||||||
|
|
||||||
// TODO This file should only contain re-exports and only re-export what is needed
|
// TODO This file should only contain re-exports and only re-export what is needed
|
||||||
|
@ -1 +0,0 @@
|
|||||||
{}
|
|
@ -1,463 +0,0 @@
|
|||||||
import { v4 as uuid } from "uuid";
|
|
||||||
|
|
||||||
import { DefaultConnectTimeout } from "./Const";
|
|
||||||
import { ConnectionStats } from "./ConnectionStats";
|
|
||||||
import { RawEvent, ReqCommand, TaggedRawEvent, u256 } from "./index";
|
|
||||||
import { RelayInfo } from "./RelayInfo";
|
|
||||||
import { unwrap } from "./Util";
|
|
||||||
|
|
||||||
export type CustomHook = (state: Readonly<StateSnapshot>) => void;
|
|
||||||
export type AuthHandler = (
|
|
||||||
challenge: string,
|
|
||||||
relay: string
|
|
||||||
) => Promise<RawEvent | undefined>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Relay settings
|
|
||||||
*/
|
|
||||||
export interface RelaySettings {
|
|
||||||
read: boolean;
|
|
||||||
write: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Snapshot of connection stats
|
|
||||||
*/
|
|
||||||
export interface StateSnapshot {
|
|
||||||
connected: boolean;
|
|
||||||
disconnects: number;
|
|
||||||
avgLatency: number;
|
|
||||||
events: {
|
|
||||||
received: number;
|
|
||||||
send: number;
|
|
||||||
};
|
|
||||||
info?: RelayInfo;
|
|
||||||
pendingRequests: Array<string>;
|
|
||||||
activeRequests: Array<string>;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Connection {
|
|
||||||
Id: string;
|
|
||||||
Address: string;
|
|
||||||
Socket: WebSocket | null = null;
|
|
||||||
|
|
||||||
PendingRaw: Array<object> = [];
|
|
||||||
PendingRequests: Array<{
|
|
||||||
cmd: ReqCommand,
|
|
||||||
cb: () => void
|
|
||||||
}> = [];
|
|
||||||
ActiveRequests = new Set<string>();
|
|
||||||
|
|
||||||
Settings: RelaySettings;
|
|
||||||
Info?: RelayInfo;
|
|
||||||
ConnectTimeout: number = DefaultConnectTimeout;
|
|
||||||
Stats: ConnectionStats = new ConnectionStats();
|
|
||||||
StateHooks: Map<string, CustomHook> = new Map();
|
|
||||||
HasStateChange: boolean = true;
|
|
||||||
CurrentState: StateSnapshot;
|
|
||||||
LastState: Readonly<StateSnapshot>;
|
|
||||||
IsClosed: boolean;
|
|
||||||
ReconnectTimer: ReturnType<typeof setTimeout> | null;
|
|
||||||
EventsCallback: Map<u256, (msg: boolean[]) => void>;
|
|
||||||
OnConnected?: () => void;
|
|
||||||
OnEvent?: (sub: string, e: TaggedRawEvent) => void;
|
|
||||||
OnEose?: (sub: string) => void;
|
|
||||||
OnDisconnect?: (id: string) => void;
|
|
||||||
Auth?: AuthHandler;
|
|
||||||
AwaitingAuth: Map<string, boolean>;
|
|
||||||
Authed = false;
|
|
||||||
Ephemeral: boolean;
|
|
||||||
EphemeralTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
||||||
Down = true;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
addr: string,
|
|
||||||
options: RelaySettings,
|
|
||||||
auth?: AuthHandler,
|
|
||||||
ephemeral: boolean = false
|
|
||||||
) {
|
|
||||||
this.Id = uuid();
|
|
||||||
this.Address = addr;
|
|
||||||
this.Settings = options;
|
|
||||||
this.CurrentState = {
|
|
||||||
connected: false,
|
|
||||||
disconnects: 0,
|
|
||||||
avgLatency: 0,
|
|
||||||
events: {
|
|
||||||
received: 0,
|
|
||||||
send: 0,
|
|
||||||
},
|
|
||||||
} as StateSnapshot;
|
|
||||||
this.LastState = Object.freeze({ ...this.CurrentState });
|
|
||||||
this.IsClosed = false;
|
|
||||||
this.ReconnectTimer = null;
|
|
||||||
this.EventsCallback = new Map();
|
|
||||||
this.AwaitingAuth = new Map();
|
|
||||||
this.Auth = auth;
|
|
||||||
this.Ephemeral = ephemeral;
|
|
||||||
}
|
|
||||||
|
|
||||||
ResetEphemeralTimeout() {
|
|
||||||
if (this.EphemeralTimeout) {
|
|
||||||
clearTimeout(this.EphemeralTimeout);
|
|
||||||
}
|
|
||||||
if (this.Ephemeral) {
|
|
||||||
this.EphemeralTimeout = setTimeout(() => {
|
|
||||||
this.Close();
|
|
||||||
}, 30_000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async Connect() {
|
|
||||||
try {
|
|
||||||
if (this.Info === undefined) {
|
|
||||||
const u = new URL(this.Address);
|
|
||||||
const rsp = await fetch(
|
|
||||||
`${u.protocol === "wss:" ? "https:" : "http:"}//${u.host}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
accept: "application/nostr+json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (rsp.ok) {
|
|
||||||
const data = await rsp.json();
|
|
||||||
for (const [k, v] of Object.entries(data)) {
|
|
||||||
if (v === "unset" || v === "" || v === "~") {
|
|
||||||
data[k] = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.Info = data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Could not load relay information", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.Socket) {
|
|
||||||
this.Id = uuid();
|
|
||||||
this.Socket.onopen = null;
|
|
||||||
this.Socket.onmessage = null;
|
|
||||||
this.Socket.onerror = null;
|
|
||||||
this.Socket.onclose = null;
|
|
||||||
}
|
|
||||||
this.IsClosed = false;
|
|
||||||
this.Socket = new WebSocket(this.Address);
|
|
||||||
this.Socket.onopen = () => this.OnOpen();
|
|
||||||
this.Socket.onmessage = (e) => this.OnMessage(e);
|
|
||||||
this.Socket.onerror = (e) => this.OnError(e);
|
|
||||||
this.Socket.onclose = (e) => this.OnClose(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
Close() {
|
|
||||||
this.IsClosed = true;
|
|
||||||
if (this.ReconnectTimer !== null) {
|
|
||||||
clearTimeout(this.ReconnectTimer);
|
|
||||||
this.ReconnectTimer = null;
|
|
||||||
}
|
|
||||||
this.Socket?.close();
|
|
||||||
this.#UpdateState();
|
|
||||||
}
|
|
||||||
|
|
||||||
OnOpen() {
|
|
||||||
this.ConnectTimeout = DefaultConnectTimeout;
|
|
||||||
console.log(`[${this.Address}] Open!`);
|
|
||||||
this.Down = false;
|
|
||||||
if (this.Ephemeral) {
|
|
||||||
this.ResetEphemeralTimeout();
|
|
||||||
}
|
|
||||||
this.OnConnected?.();
|
|
||||||
this.#sendPendingRaw();
|
|
||||||
}
|
|
||||||
|
|
||||||
OnClose(e: CloseEvent) {
|
|
||||||
if (!this.IsClosed) {
|
|
||||||
this.ConnectTimeout = this.ConnectTimeout * 2;
|
|
||||||
console.log(
|
|
||||||
`[${this.Address}] Closed (${e.reason}), trying again in ${(
|
|
||||||
this.ConnectTimeout / 1000
|
|
||||||
)
|
|
||||||
.toFixed(0)
|
|
||||||
.toLocaleString()} sec`
|
|
||||||
);
|
|
||||||
this.ReconnectTimer = setTimeout(() => {
|
|
||||||
this.Connect();
|
|
||||||
}, this.ConnectTimeout);
|
|
||||||
this.Stats.Disconnects++;
|
|
||||||
} else {
|
|
||||||
console.log(`[${this.Address}] Closed!`);
|
|
||||||
this.ReconnectTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.OnDisconnect?.(this.Id);
|
|
||||||
this.#ResetQueues();
|
|
||||||
// reset connection Id on disconnect, for query-tracking
|
|
||||||
this.Id = uuid();
|
|
||||||
this.#UpdateState();
|
|
||||||
}
|
|
||||||
|
|
||||||
OnMessage(e: MessageEvent) {
|
|
||||||
if (e.data.length > 0) {
|
|
||||||
const msg = JSON.parse(e.data);
|
|
||||||
const tag = msg[0];
|
|
||||||
switch (tag) {
|
|
||||||
case "AUTH": {
|
|
||||||
this._OnAuthAsync(msg[1])
|
|
||||||
.then(() => this.#sendPendingRaw())
|
|
||||||
.catch(console.error);
|
|
||||||
this.Stats.EventsReceived++;
|
|
||||||
this.#UpdateState();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "EVENT": {
|
|
||||||
this.OnEvent?.(msg[1], {
|
|
||||||
...msg[2],
|
|
||||||
relays: [this.Address]
|
|
||||||
});
|
|
||||||
this.Stats.EventsReceived++;
|
|
||||||
this.#UpdateState();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "EOSE": {
|
|
||||||
this.OnEose?.(msg[1]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "OK": {
|
|
||||||
// feedback to broadcast call
|
|
||||||
console.debug(`${this.Address} OK: `, msg);
|
|
||||||
const id = msg[1];
|
|
||||||
if (this.EventsCallback.has(id)) {
|
|
||||||
const cb = unwrap(this.EventsCallback.get(id));
|
|
||||||
this.EventsCallback.delete(id);
|
|
||||||
cb(msg);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "NOTICE": {
|
|
||||||
console.warn(`[${this.Address}] NOTICE: ${msg[1]}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
console.warn(`Unknown tag: ${tag}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OnError(e: Event) {
|
|
||||||
console.error(e);
|
|
||||||
this.#UpdateState();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send event on this connection
|
|
||||||
*/
|
|
||||||
SendEvent(e: RawEvent) {
|
|
||||||
if (!this.Settings.write) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const req = ["EVENT", e];
|
|
||||||
this.#SendJson(req);
|
|
||||||
this.Stats.EventsSent++;
|
|
||||||
this.#UpdateState();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send event on this connection and wait for OK response
|
|
||||||
*/
|
|
||||||
async SendAsync(e: RawEvent, timeout = 5000) {
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
if (!this.Settings.write) {
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const t = setTimeout(() => {
|
|
||||||
resolve();
|
|
||||||
}, timeout);
|
|
||||||
this.EventsCallback.set(e.id, () => {
|
|
||||||
clearTimeout(t);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
const req = ["EVENT", e];
|
|
||||||
this.#SendJson(req);
|
|
||||||
this.Stats.EventsSent++;
|
|
||||||
this.#UpdateState();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook status for connection
|
|
||||||
*/
|
|
||||||
StatusHook(fnHook: CustomHook) {
|
|
||||||
const id = uuid();
|
|
||||||
this.StateHooks.set(id, fnHook);
|
|
||||||
return () => {
|
|
||||||
this.StateHooks.delete(id);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current state of this connection
|
|
||||||
*/
|
|
||||||
GetState() {
|
|
||||||
if (this.HasStateChange) {
|
|
||||||
this.LastState = Object.freeze({ ...this.CurrentState });
|
|
||||||
this.HasStateChange = false;
|
|
||||||
}
|
|
||||||
return this.LastState;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Using relay document to determine if this relay supports a feature
|
|
||||||
*/
|
|
||||||
SupportsNip(n: number) {
|
|
||||||
return this.Info?.supported_nips?.some((a) => a === n) ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queue or send command to the relay
|
|
||||||
* @param cmd The REQ to send to the server
|
|
||||||
*/
|
|
||||||
QueueReq(cmd: ReqCommand, cbSent: () => void) {
|
|
||||||
if (this.ActiveRequests.size >= this.#maxSubscriptions) {
|
|
||||||
this.PendingRequests.push({
|
|
||||||
cmd, cb: cbSent
|
|
||||||
});
|
|
||||||
console.debug("Queuing:", this.Address, cmd);
|
|
||||||
} else {
|
|
||||||
this.ActiveRequests.add(cmd[1]);
|
|
||||||
this.#SendJson(cmd);
|
|
||||||
cbSent();
|
|
||||||
}
|
|
||||||
this.#UpdateState();
|
|
||||||
}
|
|
||||||
|
|
||||||
CloseReq(id: string) {
|
|
||||||
if (this.ActiveRequests.delete(id)) {
|
|
||||||
this.#SendJson(["CLOSE", id]);
|
|
||||||
this.OnEose?.(id);
|
|
||||||
this.#SendQueuedRequests();
|
|
||||||
}
|
|
||||||
this.#UpdateState();
|
|
||||||
}
|
|
||||||
|
|
||||||
#SendQueuedRequests() {
|
|
||||||
const canSend = this.#maxSubscriptions - this.ActiveRequests.size;
|
|
||||||
if (canSend > 0) {
|
|
||||||
for (let x = 0; x < canSend; x++) {
|
|
||||||
const p = this.PendingRequests.shift();
|
|
||||||
if (p) {
|
|
||||||
this.ActiveRequests.add(p.cmd[1]);
|
|
||||||
this.#SendJson(p.cmd);
|
|
||||||
p.cb();
|
|
||||||
console.debug("Sent pending REQ", this.Address, p.cmd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#ResetQueues() {
|
|
||||||
this.ActiveRequests.clear();
|
|
||||||
this.PendingRequests = [];
|
|
||||||
this.PendingRaw = [];
|
|
||||||
this.#UpdateState();
|
|
||||||
}
|
|
||||||
|
|
||||||
#UpdateState() {
|
|
||||||
this.CurrentState.connected = this.Socket?.readyState === WebSocket.OPEN;
|
|
||||||
this.CurrentState.events.received = this.Stats.EventsReceived;
|
|
||||||
this.CurrentState.events.send = this.Stats.EventsSent;
|
|
||||||
this.CurrentState.avgLatency =
|
|
||||||
this.Stats.Latency.length > 0
|
|
||||||
? this.Stats.Latency.reduce((acc, v) => acc + v, 0) /
|
|
||||||
this.Stats.Latency.length
|
|
||||||
: 0;
|
|
||||||
this.CurrentState.disconnects = this.Stats.Disconnects;
|
|
||||||
this.CurrentState.info = this.Info;
|
|
||||||
this.CurrentState.id = this.Id;
|
|
||||||
this.CurrentState.pendingRequests = [...this.PendingRequests.map(a => a.cmd[1])];
|
|
||||||
this.CurrentState.activeRequests = [...this.ActiveRequests];
|
|
||||||
this.Stats.Latency = this.Stats.Latency.slice(-20); // trim
|
|
||||||
this.HasStateChange = true;
|
|
||||||
this.#NotifyState();
|
|
||||||
}
|
|
||||||
|
|
||||||
#NotifyState() {
|
|
||||||
const state = this.GetState();
|
|
||||||
for (const [, h] of this.StateHooks) {
|
|
||||||
h(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#SendJson(obj: object) {
|
|
||||||
const authPending = !this.Authed && (this.AwaitingAuth.size > 0 || this.Info?.limitation?.auth_required === true);
|
|
||||||
if (this.Socket?.readyState !== WebSocket.OPEN || authPending) {
|
|
||||||
this.PendingRaw.push(obj);
|
|
||||||
if (this.Socket?.readyState === WebSocket.CLOSED && this.Ephemeral && this.IsClosed) {
|
|
||||||
this.Connect()
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#sendPendingRaw();
|
|
||||||
this.#sendOnWire(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
#sendPendingRaw() {
|
|
||||||
while (this.PendingRaw.length > 0) {
|
|
||||||
const next = this.PendingRaw.shift();
|
|
||||||
if (next) {
|
|
||||||
this.#sendOnWire(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#sendOnWire(obj: unknown) {
|
|
||||||
if (this.Socket?.readyState !== WebSocket.OPEN) {
|
|
||||||
throw new Error(`Socket is not open, state is ${this.Socket?.readyState}`);
|
|
||||||
}
|
|
||||||
const json = JSON.stringify(obj);
|
|
||||||
this.Socket.send(json);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async _OnAuthAsync(challenge: string): Promise<void> {
|
|
||||||
const authCleanup = () => {
|
|
||||||
this.AwaitingAuth.delete(challenge);
|
|
||||||
};
|
|
||||||
if (!this.Auth) {
|
|
||||||
throw new Error("Auth hook not registered");
|
|
||||||
}
|
|
||||||
this.AwaitingAuth.set(challenge, true);
|
|
||||||
const authEvent = await this.Auth(challenge, this.Address);
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
if (!authEvent) {
|
|
||||||
authCleanup();
|
|
||||||
return Promise.reject("no event");
|
|
||||||
}
|
|
||||||
|
|
||||||
const t = setTimeout(() => {
|
|
||||||
authCleanup();
|
|
||||||
resolve();
|
|
||||||
}, 10_000);
|
|
||||||
|
|
||||||
this.EventsCallback.set(authEvent.id, (msg: boolean[]) => {
|
|
||||||
clearTimeout(t);
|
|
||||||
authCleanup();
|
|
||||||
if (msg.length > 3 && msg[2] === true) {
|
|
||||||
this.Authed = true;
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.#sendOnWire(["AUTH", authEvent]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get #maxSubscriptions() {
|
|
||||||
return this.Info?.limitation?.max_subscriptions ?? 25;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
/**
|
|
||||||
* Stats class for tracking metrics per connection
|
|
||||||
*/
|
|
||||||
export class ConnectionStats {
|
|
||||||
/**
|
|
||||||
* Last n records of how long between REQ->EOSE
|
|
||||||
*/
|
|
||||||
Latency: number[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Total number of REQ's sent on this connection
|
|
||||||
*/
|
|
||||||
Subs: number = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Count of REQ which took too long and where abandoned
|
|
||||||
*/
|
|
||||||
SubsTimeout: number = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Total number of EVENT messages received
|
|
||||||
*/
|
|
||||||
EventsReceived: number = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Total number of EVENT messages sent
|
|
||||||
*/
|
|
||||||
EventsSent: number = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Total number of times this connection was lost
|
|
||||||
*/
|
|
||||||
Disconnects: number = 0;
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
/**
|
|
||||||
* Websocket re-connect timeout
|
|
||||||
*/
|
|
||||||
export const DefaultConnectTimeout = 2000;
|
|
@ -1,29 +0,0 @@
|
|||||||
enum EventKind {
|
|
||||||
Unknown = -1,
|
|
||||||
SetMetadata = 0,
|
|
||||||
TextNote = 1,
|
|
||||||
RecommendServer = 2,
|
|
||||||
ContactList = 3, // NIP-02
|
|
||||||
DirectMessage = 4, // NIP-04
|
|
||||||
Deletion = 5, // NIP-09
|
|
||||||
Repost = 6, // NIP-18
|
|
||||||
Reaction = 7, // NIP-25
|
|
||||||
BadgeAward = 8, // NIP-58
|
|
||||||
SnortSubscriptions = 1000, // NIP-XX
|
|
||||||
Polls = 6969, // NIP-69
|
|
||||||
FileHeader = 1063, // NIP-94
|
|
||||||
Relays = 10002, // NIP-65
|
|
||||||
Ephemeral = 20_000,
|
|
||||||
Auth = 22242, // NIP-42
|
|
||||||
PubkeyLists = 30000, // NIP-51a
|
|
||||||
NoteLists = 30001, // NIP-51b
|
|
||||||
TagLists = 30002, // NIP-51c
|
|
||||||
Badge = 30009, // NIP-58
|
|
||||||
ProfileBadges = 30008, // NIP-58
|
|
||||||
ZapstrTrack = 31337,
|
|
||||||
ZapRequest = 9734, // NIP 57
|
|
||||||
ZapReceipt = 9735, // NIP 57
|
|
||||||
HttpAuthentication = 27235, // NIP XX - HTTP Authentication
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EventKind;
|
|
@ -1,88 +0,0 @@
|
|||||||
import * as utils from "@noble/curves/abstract/utils";
|
|
||||||
import { bech32 } from "bech32";
|
|
||||||
import { HexKey } from ".";
|
|
||||||
|
|
||||||
export enum NostrPrefix {
|
|
||||||
PublicKey = "npub",
|
|
||||||
PrivateKey = "nsec",
|
|
||||||
Note = "note",
|
|
||||||
|
|
||||||
// TLV prefixes
|
|
||||||
Profile = "nprofile",
|
|
||||||
Event = "nevent",
|
|
||||||
Relay = "nrelay",
|
|
||||||
Address = "naddr",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum TLVEntryType {
|
|
||||||
Special = 0,
|
|
||||||
Relay = 1,
|
|
||||||
Author = 2,
|
|
||||||
Kind = 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TLVEntry {
|
|
||||||
type: TLVEntryType;
|
|
||||||
length: number;
|
|
||||||
value: string | HexKey | number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function encodeTLV(prefix: NostrPrefix, id: string, relays?: string[], kind?: number, author?: string) {
|
|
||||||
const enc = new TextEncoder();
|
|
||||||
const buf = prefix === NostrPrefix.Address ? enc.encode(id) : utils.hexToBytes(id);
|
|
||||||
|
|
||||||
const tl0 = [0, buf.length, ...buf];
|
|
||||||
const tl1 =
|
|
||||||
relays
|
|
||||||
?.map((a) => {
|
|
||||||
const data = enc.encode(a);
|
|
||||||
return [1, data.length, ...data];
|
|
||||||
})
|
|
||||||
.flat() ?? [];
|
|
||||||
|
|
||||||
const tl2 = author ? [2, 32, ...utils.hexToBytes(author)] : [];
|
|
||||||
const tl3 = kind ? [3, 4, ...new Uint8Array(new Uint32Array([kind]).buffer).reverse()] : []
|
|
||||||
|
|
||||||
return bech32.encode(prefix, bech32.toWords([...tl0, ...tl1, ...tl2, ...tl3]), 1_000);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decodeTLV(str: string) {
|
|
||||||
const decoded = bech32.decode(str, 1_000);
|
|
||||||
const data = bech32.fromWords(decoded.words);
|
|
||||||
|
|
||||||
const entries: TLVEntry[] = [];
|
|
||||||
let x = 0;
|
|
||||||
while (x < data.length) {
|
|
||||||
const t = data[x];
|
|
||||||
const l = data[x + 1];
|
|
||||||
const v = data.slice(x + 2, x + 2 + l);
|
|
||||||
entries.push({
|
|
||||||
type: t,
|
|
||||||
length: l,
|
|
||||||
value: decodeTLVEntry(t, decoded.prefix, new Uint8Array(v)),
|
|
||||||
});
|
|
||||||
x += 2 + l;
|
|
||||||
}
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeTLVEntry(type: TLVEntryType, prefix: string, data: Uint8Array) {
|
|
||||||
switch (type) {
|
|
||||||
case TLVEntryType.Special: {
|
|
||||||
if (prefix === NostrPrefix.Address) {
|
|
||||||
return new TextDecoder("ASCII").decode(data);
|
|
||||||
} else {
|
|
||||||
return utils.bytesToHex(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case TLVEntryType.Author: {
|
|
||||||
return utils.bytesToHex(data);
|
|
||||||
}
|
|
||||||
case TLVEntryType.Kind: {
|
|
||||||
return new Uint32Array(new Uint8Array(data.reverse()).buffer)[0];
|
|
||||||
}
|
|
||||||
case TLVEntryType.Relay: {
|
|
||||||
return new TextDecoder("ASCII").decode(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
export enum Nips {
|
|
||||||
Search = 50,
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
export interface RelayInfo {
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
pubkey?: string;
|
|
||||||
contact?: string;
|
|
||||||
supported_nips?: number[];
|
|
||||||
software?: string;
|
|
||||||
version?: string;
|
|
||||||
limitation?: {
|
|
||||||
payment_required: boolean;
|
|
||||||
max_subscriptions: number;
|
|
||||||
max_filters: number;
|
|
||||||
max_event_tags: number;
|
|
||||||
auth_required: boolean
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
import { HexKey, u256 } from "./index";
|
|
||||||
import { unwrap } from "./Util";
|
|
||||||
|
|
||||||
export default class Tag {
|
|
||||||
Original: string[];
|
|
||||||
Key: string;
|
|
||||||
Event?: u256;
|
|
||||||
PubKey?: HexKey;
|
|
||||||
Relay?: string;
|
|
||||||
Marker?: string;
|
|
||||||
Hashtag?: string;
|
|
||||||
DTag?: string;
|
|
||||||
ATag?: string;
|
|
||||||
Index: number;
|
|
||||||
Invalid: boolean;
|
|
||||||
LNURL?: string;
|
|
||||||
|
|
||||||
constructor(tag: string[], index: number) {
|
|
||||||
this.Original = tag;
|
|
||||||
this.Key = tag[0];
|
|
||||||
this.Index = index;
|
|
||||||
this.Invalid = false;
|
|
||||||
|
|
||||||
switch (this.Key) {
|
|
||||||
case "e": {
|
|
||||||
// ["e", <event-id>, <relay-url>, <marker>]
|
|
||||||
this.Event = tag[1];
|
|
||||||
this.Relay = tag.length > 2 ? tag[2] : undefined;
|
|
||||||
this.Marker = tag.length > 3 ? tag[3] : undefined;
|
|
||||||
if (!this.Event) {
|
|
||||||
this.Invalid = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "p": {
|
|
||||||
// ["p", <pubkey>]
|
|
||||||
this.PubKey = tag[1];
|
|
||||||
if (!this.PubKey) {
|
|
||||||
this.Invalid = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "d": {
|
|
||||||
this.DTag = tag[1];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "a": {
|
|
||||||
this.ATag = tag[1];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "t": {
|
|
||||||
this.Hashtag = tag[1];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "delegation": {
|
|
||||||
this.PubKey = tag[1];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "zap": {
|
|
||||||
this.LNURL = tag[1];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ToObject(): string[] | null {
|
|
||||||
switch (this.Key) {
|
|
||||||
case "e": {
|
|
||||||
let ret = ["e", this.Event, this.Relay, this.Marker];
|
|
||||||
const trimEnd = ret.reverse().findIndex((a) => a !== undefined);
|
|
||||||
ret = ret.reverse().slice(0, ret.length - trimEnd);
|
|
||||||
return <string[]>ret;
|
|
||||||
}
|
|
||||||
case "p": {
|
|
||||||
return this.PubKey ? ["p", this.PubKey] : null;
|
|
||||||
}
|
|
||||||
case "t": {
|
|
||||||
return ["t", unwrap(this.Hashtag)];
|
|
||||||
}
|
|
||||||
case "d": {
|
|
||||||
return ["d", unwrap(this.DTag)];
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return this.Original;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
import * as utils from "@noble/curves/abstract/utils";
|
|
||||||
import { bech32 } from "bech32";
|
|
||||||
|
|
||||||
export function unwrap<T>(v: T | undefined | null): T {
|
|
||||||
if (v === undefined || v === null) {
|
|
||||||
throw new Error("missing value");
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert hex to bech32
|
|
||||||
*/
|
|
||||||
export function hexToBech32(hrp: string, hex?: string) {
|
|
||||||
if (typeof hex !== "string" || hex.length === 0 || hex.length % 2 !== 0) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const buf = utils.hexToBytes(hex);
|
|
||||||
return bech32.encode(hrp, bech32.toWords(buf));
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Invalid hex", hex, e);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sanitizeRelayUrl(url: string) {
|
|
||||||
try {
|
|
||||||
return new URL(url).toString();
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
export * from "./Connection";
|
|
||||||
export { default as EventKind } from "./EventKind";
|
|
||||||
export { default as Tag } from "./Tag";
|
|
||||||
export * from "./Links";
|
|
||||||
export * from "./Nips";
|
|
||||||
|
|
||||||
import { RelaySettings } from ".";
|
|
||||||
export type RawEvent = {
|
|
||||||
id: u256;
|
|
||||||
pubkey: HexKey;
|
|
||||||
created_at: number;
|
|
||||||
kind: number;
|
|
||||||
tags: Array<Array<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;
|
|
||||||
|
|
||||||
export type ReqCommand = [cmd: "REQ", id: string, ...filters: Array<RawReqFilter>];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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",
|
|
||||||
Badges = "profile_badges",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FullRelaySettings {
|
|
||||||
url: string;
|
|
||||||
settings: RelaySettings;
|
|
||||||
}
|
|
@ -8,8 +8,7 @@ import {
|
|||||||
parsePublicKey,
|
parsePublicKey,
|
||||||
PublicKey,
|
PublicKey,
|
||||||
} from "../src/crypto"
|
} from "../src/crypto"
|
||||||
import { RawEvent } from "../src"
|
import { RawEvent, signEvent, Unsigned } from "../src/event"
|
||||||
import { signEvent, Unsigned } from "../src/event"
|
|
||||||
|
|
||||||
export const relayUrl = new URL("ws://localhost:12648")
|
export const relayUrl = new URL("ws://localhost:12648")
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user