@nostr-dev-kit/ndk-wallet
Version:
288 lines (227 loc) • 8.42 kB
text/typescript
import { CashuMint, CashuWallet, type MintQuoteResponse } from "@cashu/cashu-ts";
import type NDK from "@nostr-dev-kit/ndk";
import {
type LnPaymentInfo,
NDKEventId,
type NDKPaymentConfirmationCashu,
type NDKPaymentConfirmationLN,
NDKPool,
NDKPrivateKeySigner,
NDKRelay,
NDKRelayAuthPolicies,
NDKRelaySet,
type NDKUser,
} from "@nostr-dev-kit/ndk";
import createDebug from "debug";
import { EventEmitter } from "tseep";
import { mintProofs } from "../../utils/cashu.js";
import type { NutPayment } from "../cashu/pay/nut.js";
import {
NDKWallet,
type NDKWalletBalance,
type NDKWalletEvents,
NDKWalletStatus,
type NDKWalletTypes,
} from "../index.js";
import { MintInterface, getCashuWallet } from "../mint.js";
import { redeemNutzaps } from "./nutzap.js";
import { sendReq } from "./req.js";
import type {
NDKNWCGetInfoResult,
NDKNWCMakeInvoiceResult,
NDKNWCRequestMap,
NDKNWCResponseBase,
NDKNWCResponseMap,
} from "./types.js";
const d = createDebug("ndk-wallet:nwc");
export type NDKNWCWalletEvents = NDKWalletEvents & {
connecting: () => void;
error: () => void;
timeout: (method: keyof NDKNWCRequestMap) => void;
};
export class NDKNWCWallet extends NDKWallet {
get type(): NDKWalletTypes {
return "nwc";
}
public status = NDKWalletStatus.INITIAL;
public walletId = "nwc";
public pairingCode?: string;
public walletService?: NDKUser;
public relaySet?: NDKRelaySet;
public signer?: NDKPrivateKeySigner;
private _balance?: NDKWalletBalance;
private cachedInfo?: NDKNWCGetInfoResult;
public pool?: NDKPool;
public timeout?: number;
/**
*
* @param ndk
* @param timeout A timeeout to use for all operations.
*/
constructor(
ndk: NDK,
{
timeout,
pairingCode,
pubkey,
relayUrls,
secret,
}: {
timeout?: number;
pairingCode?: string;
pubkey?: string;
relayUrls?: string[];
secret?: string;
},
) {
super(ndk);
if (pairingCode) {
const u = new URL(pairingCode);
pubkey = u.host ?? u.pathname;
relayUrls = u.searchParams.getAll("relay");
secret = u.searchParams.get("secret") as string;
this.pairingCode = pairingCode;
}
if (!pubkey || !relayUrls || !secret) throw new Error("Incomplete initialization parameters");
this.timeout = timeout;
this.walletService = this.ndk.getUser({ pubkey });
this.pool = this.getPool(relayUrls);
this.relaySet = NDKRelaySet.fromRelayUrls(relayUrls, this.ndk, true, this.pool);
// Initialize signer
this.signer = new NDKPrivateKeySigner(secret);
this.pool.on("connect", () => {
this.status = NDKWalletStatus.READY;
this.emit("ready");
});
this.pool.on("relay:disconnect", () => (this.status = NDKWalletStatus.LOADING));
this.pool.connect();
if (this.pool.connectedRelays().length > 0) {
this.status = NDKWalletStatus.READY;
this.emit("ready");
}
}
private getPool(relayUrls: string[]) {
for (const pool of this.ndk.pools) if (pool.name === "NWC") return pool;
return new NDKPool(relayUrls, [], this.ndk, { name: "NWC" });
}
async lnPay(payment: LnPaymentInfo): Promise<NDKPaymentConfirmationLN | undefined> {
if (!this.signer) throw new Error("Wallet not initialized");
d("lnPay", payment.pr);
// Create and sign NWC request event
const res = await this.req("pay_invoice", { invoice: payment.pr });
d("lnPay res", res);
if (res.result) {
return {
preimage: res.result.preimage,
};
}
this.updateBalance();
throw new Error(res.error?.message || "Payment failed");
}
/**
* Pay by minting tokens.
*
* This creates a quote on a mint, pays it using NWC and then mints the tokens.
*
* @param payment - The payment to pay
* @param onLnPayment - A callback that is called when an LN payment will be processed
* @returns The payment confirmation
*/
async cashuPay(
payment: NutPayment,
onLnInvoice?: (pr: string) => void,
onLnPayment?: (mint: string, invoice: string) => void,
): Promise<NDKPaymentConfirmationCashu | undefined> {
if (!payment.mints) throw new Error("No mints provided");
for (const mint of payment.mints) {
let amount = payment.amount;
amount = amount / 1000;
const wallet = new CashuWallet(new CashuMint(mint), { unit: "sat" });
let quote: MintQuoteResponse | undefined;
try {
quote = await wallet.createMintQuote(amount);
d("cashuPay quote", quote);
onLnInvoice?.(quote.request);
} catch (e) {
console.error("error creating mint quote", e);
throw e;
}
if (!quote) throw new Error("Didnt receive a mint quote");
// todo check that the amount of the invoice matches the amount we want to pay
try {
const res = await this.req("pay_invoice", { invoice: quote.request });
if (res.result?.preimage) {
onLnPayment?.(mint, res.result.preimage);
}
d("cashuPay res", res);
} catch (e: any) {
const message = e?.error?.message || e?.message || "unknown error";
console.error("error paying invoice", e, { message });
throw new Error(message);
}
this.updateBalance();
return mintProofs(wallet, quote, amount, mint, payment.p2pk);
}
}
/**
* Redeem a set of nutzaps into an NWC wallet.
*
* This function gets an invoice from the NWC wallet until the total amount of the nutzaps is enough to pay for the invoice
* when accounting for fees.
*
* @param cashuWallet - The cashu wallet to redeem the nutzaps into
* @param nutzaps - The nutzaps to redeem
* @param proofs - The proofs to redeem
* @param mint - The mint to redeem the nutzaps into
* @param privkey - The private key needed to redeem p2pk proofs.
*/
public redeemNutzaps = redeemNutzaps.bind(this);
/**
* Fetch the balance of this wallet
*/
async updateBalance(): Promise<void> {
const res = await this.req("get_balance", {});
if (!res.result) throw new Error("Failed to get balance");
if (res.error) throw new Error(res.error.message);
// update the cached balance property
this._balance = {
amount: res.result?.balance ?? 0,
};
// balance is always in sats
this._balance.amount /= 1000;
this.emit("balance_updated");
}
/**
* Get the balance of this wallet
*/
get balance(): NDKWalletBalance | undefined {
return this._balance;
}
req = sendReq.bind(this) as <M extends keyof NDKNWCRequestMap>(
method: M,
params: NDKNWCRequestMap[M],
) => Promise<NDKNWCResponseBase<NDKNWCResponseMap[M]>>;
async getInfo(refetch = false) {
if (refetch) {
this.cachedInfo = undefined;
}
if (this.cachedInfo) return this.cachedInfo;
const res = await this.req("get_info", {});
d("info", res);
if (!res.result) throw new Error("Failed to get info");
if (res.error) throw new Error(res.error.message);
this.cachedInfo = res.result;
if (res.result.alias) this.walletId = res.result.alias;
return res.result;
}
async listTransactions() {
const res = await this.req("list_transactions", {});
if (!res.result) throw new Error("Failed to list transactions");
return res.result;
}
async makeInvoice(amount: number, description: string): Promise<NDKNWCMakeInvoiceResult> {
const res = await this.req("make_invoice", { amount, description });
if (!res.result) throw new Error("Failed to make invoice");
return res.result;
}
}