Files
snort/packages/app/src/Wallet/NostrWalletConnect.ts

191 lines
4.9 KiB
TypeScript

import { Connection, RawEvent } from "@snort/nostr";
import { EventBuilder } from "System";
import { EventExt } from "System/EventExt";
import { LNWallet, WalletError, WalletErrorCode, WalletInfo, WalletInvoice, WalletInvoiceState } from "Wallet";
interface WalletConnectConfig {
relayUrl: string;
walletPubkey: string;
secret: string;
}
interface QueueObj {
resolve: (o: string) => void;
reject: (e: Error) => void;
}
interface WalletConnectResponse<T> {
result_type?: string;
result?: T;
error?: {
code:
| "RATE_LIMITED"
| "NOT_IMPLEMENTED"
| "INSUFFICIENT_BALANCE"
| "QUOTA_EXCEEDED"
| "RESTRICTED"
| "UNAUTHORIZED"
| "INTERNAL"
| "OTHER";
message: string;
};
}
export class NostrConnectWallet implements LNWallet {
#config: WalletConnectConfig;
#conn?: Connection;
#commandQueue: Map<string, QueueObj>;
constructor(cfg: string) {
this.#config = NostrConnectWallet.parseConfigUrl(cfg);
this.#commandQueue = new Map();
}
static parseConfigUrl(url: string) {
const uri = new URL(url.replace("nostrwalletconnect://", "http://").replace("nostr+walletconnect://", "http://"));
return {
relayUrl: uri.searchParams.get("relay"),
walletPubkey: uri.host,
secret: uri.searchParams.get("secret"),
} as WalletConnectConfig;
}
isReady(): boolean {
return true;
}
async getInfo() {
await this.login();
return await new Promise<WalletInfo>((resolve, reject) => {
this.#commandQueue.set("info", {
resolve: (o: string) => {
resolve({
alias: "NWC",
chains: o.split(" "),
} as WalletInfo);
},
reject,
});
this.#conn?.QueueReq(["REQ", "info", { kinds: [13194], limit: 1 }], () => {
// ignored
});
});
}
async login() {
if (this.#conn) return true;
return await new Promise<boolean>(resolve => {
this.#conn = new Connection(this.#config.relayUrl, { read: true, write: true });
this.#conn.OnConnected = () => resolve(true);
this.#conn.OnEvent = (s, e) => {
this.#onReply(s, e);
};
this.#conn.Connect();
});
}
async close() {
this.#conn?.Close();
return true;
}
async getBalance() {
return 0;
}
createInvoice() {
return Promise.reject(new WalletError(WalletErrorCode.GeneralError, "Not implemented"));
}
async payInvoice(pr: string) {
await this.login();
const rsp = await this.#rpc<WalletConnectResponse<WalletInvoice>>("pay_invoice", {
invoice: pr,
});
if (!rsp.error) {
return {
...rsp.result,
pr,
state: WalletInvoiceState.Paid,
} as WalletInvoice;
} else {
throw new WalletError(WalletErrorCode.GeneralError, rsp.error.message);
}
}
getInvoices() {
return Promise.resolve([]);
}
async #onReply(sub: string, e: RawEvent) {
if (sub === "info") {
const pending = this.#commandQueue.get("info");
if (!pending) {
throw new WalletError(WalletErrorCode.GeneralError, "No pending info command found");
}
pending.resolve(e.content);
this.#commandQueue.delete("info");
return;
}
if (e.kind !== 23195) {
throw new WalletError(WalletErrorCode.GeneralError, "Unknown event kind");
}
const replyTo = e.tags.find(a => a[0] === "e");
if (!replyTo) {
throw new WalletError(WalletErrorCode.GeneralError, "Missing e-tag in command response");
}
const pending = this.#commandQueue.get(replyTo[1]);
if (!pending) {
throw new WalletError(WalletErrorCode.GeneralError, "No pending command found");
}
pending.resolve(e.content);
this.#commandQueue.delete(replyTo[1]);
this.#conn?.CloseReq(sub);
}
async #rpc<T>(method: string, params: Record<string, string>) {
if (!this.#conn) throw new WalletError(WalletErrorCode.GeneralError, "Not implemented");
const payload = JSON.stringify({
method,
params,
});
const eb = new EventBuilder();
eb.kind(23194)
.content(await EventExt.encryptData(payload, this.#config.walletPubkey, this.#config.secret))
.tag(["p", this.#config.walletPubkey]);
const evCommand = await eb.buildAndSign(this.#config.secret);
this.#conn.QueueReq(
[
"REQ",
evCommand.id.slice(0, 12),
{
kinds: [23195],
authors: [this.#config.walletPubkey],
["#e"]: [evCommand.id],
},
],
() => {
// ignored
}
);
await this.#conn.SendAsync(evCommand);
return await new Promise<T>((resolve, reject) => {
this.#commandQueue.set(evCommand.id, {
resolve: async (o: string) => {
const reply = JSON.parse(await EventExt.decryptData(o, this.#config.secret, this.#config.walletPubkey));
console.debug("NWC", reply);
resolve(reply);
},
reject,
});
});
}
}