@nostr-dev-kit/ndk-wallet
Version:
1,548 lines (1,514 loc) • 94.8 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
NDKCashuDeposit: () => NDKCashuDeposit,
NDKCashuWallet: () => NDKCashuWallet,
NDKCashuWalletBackup: () => NDKCashuWalletBackup,
NDKNWCWallet: () => NDKNWCWallet,
NDKNutzapMonitor: () => NDKNutzapMonitor,
NDKWallet: () => NDKWallet,
NDKWalletStatus: () => NDKWalletStatus,
NDKWebLNWallet: () => NDKWebLNWallet,
WalletState: () => WalletState,
calculateNewState: () => calculateNewState,
consolidateMintTokens: () => consolidateMintTokens,
consolidateTokens: () => consolidateTokens,
getBolt11Amount: () => getBolt11Amount,
getBolt11Description: () => getBolt11Description,
getBolt11ExpiresAt: () => getBolt11ExpiresAt,
getCashuMintRecommendations: () => getCashuMintRecommendations,
migrateCashuWallet: () => migrateCashuWallet,
update: () => update
});
module.exports = __toCommonJS(index_exports);
// src/nutzap-monitor/index.ts
var import_ndk11 = require("@nostr-dev-kit/ndk");
var import_ndk12 = require("@nostr-dev-kit/ndk");
var import_tseep4 = require("tseep");
// src/wallets/cashu/wallet/index.ts
var import_cashu_ts4 = require("@cashu/cashu-ts");
var import_ndk8 = require("@nostr-dev-kit/ndk");
// src/wallets/index.ts
var import_tseep = require("tseep");
// src/wallets/cashu/mint.ts
var import_cashu_ts = require("@cashu/cashu-ts");
var mintWallets = /* @__PURE__ */ new Map();
var mintWalletPromises = /* @__PURE__ */ new Map();
function mintKey(mint, unit, pk) {
if (unit === "sats") {
unit = "sat";
}
if (pk) {
const pkStr = new TextDecoder().decode(pk);
return `${mint}-${unit}-${pkStr}`;
}
return `${mint}-${unit}`;
}
async function walletForMint(mint, {
pk,
timeout = 5e3,
mintInfo,
mintKeys,
onMintInfoNeeded,
onMintInfoLoaded,
onMintKeysNeeded,
onMintKeysLoaded
} = {}) {
mintInfo ??= await onMintInfoNeeded?.(mint);
mintKeys ??= await onMintKeysNeeded?.(mint);
if (!mintInfo && onMintInfoLoaded) {
mintInfo = await import_cashu_ts.CashuMint.getInfo(mint);
onMintInfoLoaded?.(mint, mintInfo);
}
const unit = "sat";
const key = mintKey(mint, unit, pk);
if (mintWallets.has(key)) {
return mintWallets.get(key);
}
if (mintWalletPromises.has(key)) {
return mintWalletPromises.get(key);
}
if (!mintInfo) {
if (onMintInfoNeeded) {
mintInfo = await onMintInfoNeeded(mint);
}
if (!mintInfo && onMintInfoLoaded) {
mintInfo = await import_cashu_ts.CashuMint.getInfo(mint);
onMintInfoLoaded(mint, mintInfo);
}
}
if (!mintKeys && onMintKeysNeeded) {
mintKeys = await onMintKeysNeeded(mint);
}
const wallet = new import_cashu_ts.CashuWallet(new import_cashu_ts.CashuMint(mint), {
unit,
bip39seed: pk,
mintInfo,
keys: mintKeys
});
const loadPromise = new Promise(async (resolve) => {
try {
const timeoutPromise = new Promise((_, rejectTimeout) => {
setTimeout(() => {
rejectTimeout(new Error("timeout loading mint"));
}, timeout);
});
await Promise.race([wallet.loadMint(), timeoutPromise]);
mintWallets.set(key, wallet);
mintWalletPromises.delete(key);
if (wallet.keys) {
onMintKeysLoaded?.(mint, wallet.keys);
}
resolve(wallet);
} catch (e) {
console.error("[WALLET] error loading mint", mint, e.message);
mintWalletPromises.delete(key);
resolve(null);
}
});
mintWalletPromises.set(key, loadPromise);
return loadPromise;
}
// src/wallets/mint.ts
async function getCashuWallet(mint) {
if (this.cashuWallets.has(mint)) return this.cashuWallets.get(mint);
const w = await walletForMint(mint, {
onMintInfoNeeded: this.onMintInfoNeeded,
onMintInfoLoaded: this.onMintInfoLoaded,
onMintKeysNeeded: this.onMintKeysNeeded,
onMintKeysLoaded: this.onMintKeysLoaded
});
if (!w) throw new Error(`unable to load wallet for mint ${mint}`);
this.cashuWallets.set(mint, w);
return w;
}
// src/wallets/index.ts
var NDKWalletStatus = /* @__PURE__ */ ((NDKWalletStatus2) => {
NDKWalletStatus2["INITIAL"] = "initial";
NDKWalletStatus2["LOADING"] = "loading";
NDKWalletStatus2["READY"] = "ready";
NDKWalletStatus2["FAILED"] = "failed";
return NDKWalletStatus2;
})(NDKWalletStatus || {});
var NDKWallet = class extends import_tseep.EventEmitter {
cashuWallets = /* @__PURE__ */ new Map();
onMintInfoNeeded;
onMintInfoLoaded;
onMintKeysNeeded;
onMintKeysLoaded;
getCashuWallet = getCashuWallet.bind(this);
ndk;
constructor(ndk) {
super();
this.ndk = ndk;
}
status = "initial" /* INITIAL */;
get type() {
throw new Error("Not implemented");
}
/**
* An ID of this wallet
*/
walletId = "unknown";
/**
* Get the balance of this wallet
*/
get balance() {
throw new Error("Not implemented");
}
/**
* 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 nutzapIds - The IDs of the nutzaps to redeem
* @param proofs - The proofs to redeem
* @param privkey - The private key needed to redeem p2pk proofs.
*/
redeemNutzaps(_nutzaps, _privkey, _opts) {
throw new Error("Not implemented");
}
};
// src/wallets/cashu/deposit-monitor.ts
var import_tseep2 = require("tseep");
var NDKCashuDepositMonitor = class extends import_tseep2.EventEmitter {
deposits = /* @__PURE__ */ new Map();
addDeposit(deposit) {
const { quoteId } = deposit;
if (!quoteId) throw new Error("deposit has no quote ID");
if (this.deposits.has(quoteId)) return false;
deposit.once("success", (_token) => {
this.removeDeposit(quoteId);
});
this.deposits.set(quoteId, deposit);
this.emit("change");
return true;
}
removeDeposit(quoteId) {
this.deposits.delete(quoteId);
this.emit("change");
}
};
// src/wallets/cashu/deposit.ts
var import_debug = __toESM(require("debug"), 1);
var import_tseep3 = require("tseep");
// src/wallets/cashu/quote.ts
var import_ndk = require("@nostr-dev-kit/ndk");
var import_ndk2 = require("@nostr-dev-kit/ndk");
// src/utils/ln.ts
var import_light_bolt11_decoder = require("light-bolt11-decoder");
function getBolt11ExpiresAt(bolt11) {
const decoded = (0, import_light_bolt11_decoder.decode)(bolt11);
const expiry = decoded.expiry;
const timestamp = decoded.sections.find((section) => section.name === "timestamp").value;
if (typeof expiry === "number" && typeof timestamp === "number") {
return expiry + timestamp;
}
return void 0;
}
function getBolt11Amount(bolt11) {
const decoded = (0, import_light_bolt11_decoder.decode)(bolt11);
const section = decoded.sections.find((section2) => section2.name === "amount");
const val = section?.value;
return Number(val);
}
function getBolt11Description(bolt11) {
const decoded = (0, import_light_bolt11_decoder.decode)(bolt11);
const section = decoded.sections.find((section2) => section2.name === "description");
const val = section?.value;
return val;
}
// src/wallets/cashu/quote.ts
var NDKCashuQuote = class _NDKCashuQuote extends import_ndk2.NDKEvent {
quoteId;
mint;
amount;
unit;
_wallet;
static kind = import_ndk.NDKKind.CashuQuote;
constructor(ndk, event) {
super(ndk, event);
this.kind ??= import_ndk.NDKKind.CashuQuote;
}
static async from(event) {
const quote = new _NDKCashuQuote(event.ndk, event);
const original = event;
try {
await quote.decrypt();
} catch {
quote.content = original.content;
}
try {
const content = JSON.parse(quote.content);
quote.quoteId = content.quoteId;
quote.mint = content.mint;
quote.amount = content.amount;
quote.unit = content.unit;
} catch (_e) {
return;
}
return quote;
}
set wallet(wallet) {
this._wallet = wallet;
}
set invoice(invoice) {
const bolt11Expiry = getBolt11ExpiresAt(invoice);
if (bolt11Expiry) this.tags.push(["expiration", bolt11Expiry.toString()]);
}
async save() {
if (!this.ndk) throw new Error("NDK is required");
this.content = JSON.stringify({
quoteId: this.quoteId,
mint: this.mint,
amount: this.amount,
unit: this.unit
});
await this.encrypt(this.ndk.activeUser, void 0, "nip44");
await this.sign();
await this.publish(this._wallet?.relaySet);
}
};
// src/wallets/cashu/wallet/txs.ts
var import_ndk3 = require("@nostr-dev-kit/ndk");
async function createOutTxEvent(ndk, paymentRequest, paymentResult, relaySet, { nutzaps } = {}) {
let description = paymentRequest.paymentDescription;
let amount;
if (paymentRequest.pr) {
amount = getBolt11Amount(paymentRequest.pr);
description ??= getBolt11Description(paymentRequest.pr);
if (amount) amount /= 1e3;
} else {
amount = paymentRequest.amount;
}
if (!amount) {
console.error("BUG: Unable to find amount for paymentRequest", paymentRequest);
}
const txEvent = new import_ndk3.NDKCashuWalletTx(ndk);
txEvent.direction = "out";
txEvent.amount = amount ?? 0;
txEvent.mint = paymentResult.mint;
txEvent.description = description;
if (paymentResult.fee) txEvent.fee = paymentResult.fee;
if (paymentRequest.target) {
txEvent.tags.push(paymentRequest.target.tagReference());
if (!(paymentRequest.target instanceof import_ndk3.NDKUser)) {
txEvent.tags.push(["p", paymentRequest.target.pubkey]);
}
}
if (nutzaps) {
txEvent.description ??= "nutzap redeem";
for (const nutzap of nutzaps) txEvent.addRedeemedNutzap(nutzap);
}
if (paymentResult.stateUpdate?.created) txEvent.createdTokens = [paymentResult.stateUpdate.created];
if (paymentResult.stateUpdate?.deleted) txEvent.destroyedTokenIds = paymentResult.stateUpdate.deleted;
if (paymentResult.stateUpdate?.reserved) txEvent.reservedTokens = [paymentResult.stateUpdate.reserved];
await txEvent.sign();
txEvent.publish(relaySet);
return txEvent;
}
async function createInTxEvent(ndk, proofs, mint, updateStateResult, { nutzaps, fee, description }, relaySet) {
const txEvent = new import_ndk3.NDKCashuWalletTx(ndk);
const amount = (0, import_ndk3.proofsTotalBalance)(proofs);
txEvent.direction = "in";
txEvent.amount = amount;
txEvent.mint = mint;
txEvent.description = description;
if (updateStateResult.created) txEvent.createdTokens = [updateStateResult.created];
if (updateStateResult.deleted) txEvent.destroyedTokenIds = updateStateResult.deleted;
if (updateStateResult.reserved) txEvent.reservedTokens = [updateStateResult.reserved];
if (nutzaps) for (const nutzap of nutzaps) txEvent.addRedeemedNutzap(nutzap);
if (fee) txEvent.fee = fee;
await txEvent.sign();
txEvent.publish(relaySet);
return txEvent;
}
// src/wallets/cashu/deposit.ts
var d = (0, import_debug.default)("ndk-wallet:cashu:deposit");
function randomMint(wallet) {
const mints = wallet.mints;
const mint = mints[Math.floor(Math.random() * mints.length)];
return mint;
}
var NDKCashuDeposit = class _NDKCashuDeposit extends import_tseep3.EventEmitter {
mint;
amount;
quoteId;
wallet;
checkTimeout;
checkIntervalLength = 2500;
finalized = false;
quoteEvent;
constructor(wallet, amount, mint) {
super();
this.wallet = wallet;
this.mint = mint || randomMint(wallet);
this.amount = amount;
}
static fromQuoteEvent(wallet, quote) {
if (!quote.amount) throw new Error("quote has no amount");
if (!quote.mint) throw new Error("quote has no mint");
const deposit = new _NDKCashuDeposit(wallet, quote.amount, quote.mint);
deposit.quoteId = quote.quoteId;
return deposit;
}
/**
* Creates a quote ID and start monitoring for payment.
*
* Once a payment is received, the deposit will emit a "success" event.
*
* @param pollTime - time in milliseconds between checks
* @returns
*/
async start(pollTime = 2500) {
const cashuWallet = await this.wallet.getCashuWallet(this.mint);
const quote = await cashuWallet.createMintQuote(this.amount);
d("created quote %s for %d %s", quote.quote, this.amount, this.mint);
this.quoteId = quote.quote;
this.wallet.depositMonitor.addDeposit(this);
setTimeout(this.check.bind(this, pollTime), pollTime);
this.createQuoteEvent(quote.quote, quote.request).then((event) => this.quoteEvent = event);
return quote.request;
}
/**
* This generates a 7374 event containing the quote ID
* with an optional expiration set to the bolt11 expiry (if there is one)
*/
async createQuoteEvent(quoteId, bolt11) {
const { ndk } = this.wallet;
const quoteEvent = new NDKCashuQuote(ndk);
quoteEvent.quoteId = quoteId;
quoteEvent.mint = this.mint;
quoteEvent.amount = this.amount;
quoteEvent.wallet = this.wallet;
quoteEvent.invoice = bolt11;
try {
await quoteEvent.save();
d("saved quote on event %s", quoteEvent.rawEvent());
} catch (e) {
d("error saving quote on event %s", e.relayErrors);
}
return quoteEvent;
}
async runCheck() {
if (!this.finalized) await this.finalize();
if (!this.finalized) this.delayCheck();
}
delayCheck() {
setTimeout(() => {
this.runCheck();
this.checkIntervalLength += 500;
}, this.checkIntervalLength);
}
/**
* Check if the deposit has been finalized.
* @param timeout A timeout in milliseconds to wait before giving up.
*/
async check(timeout) {
this.runCheck();
if (timeout) {
setTimeout(() => {
clearTimeout(this.checkTimeout);
}, timeout);
}
}
async finalize() {
if (!this.quoteId) throw new Error("No quoteId set.");
let proofs;
try {
d("Checking for minting status of %s", this.quoteId);
const cashuWallet = await this.wallet.getCashuWallet(this.mint);
const proofsWeHave = await this.wallet.state.getProofs({ mint: this.mint });
proofs = await cashuWallet.mintProofs(this.amount, this.quoteId, {
proofsWeHave
});
if (proofs.length === 0) return;
} catch (e) {
if (e.message.match(/not paid/i)) return;
if (e.message.match(/already issued/i)) {
d("Mint is saying the quote has already been issued, destroying quote event: %s", e.message);
this.destroyQuoteEvent();
this.finalized = true;
return;
}
if (e.message.match(/rate limit/i)) {
d("Mint seems to be rate limiting, lowering check interval");
this.checkIntervalLength += 5e3;
return;
}
d(e.message);
return;
}
try {
this.finalized = true;
const updateRes = await this.wallet.state.update(
{
store: proofs,
mint: this.mint
},
"Deposit"
);
const tokenEvent = updateRes.created;
if (!tokenEvent) throw new Error("no token event created");
createInTxEvent(
this.wallet.ndk,
proofs,
this.mint,
updateRes,
{ description: "Deposit" },
this.wallet.relaySet
);
this.emit("success", tokenEvent);
this.destroyQuoteEvent();
} catch (e) {
this.emit("error", e.message);
console.error(e);
}
}
async destroyQuoteEvent() {
if (!this.quoteEvent) return;
const deleteEvent = await this.quoteEvent.delete(void 0, false);
deleteEvent.publish(this.wallet.relaySet);
}
};
// src/wallets/cashu/event-handlers/index.ts
var import_ndk5 = require("@nostr-dev-kit/ndk");
// src/wallets/cashu/event-handlers/deletion.ts
async function handleEventDeletion(event) {
const deletedIds = event.getMatchingTags("e").map((tag) => tag[1]);
for (const deletedId of deletedIds) {
this.state.removeTokenId(deletedId);
}
}
// src/wallets/cashu/event-handlers/quote.ts
async function handleQuote(event) {
const quote = await NDKCashuQuote.from(event);
if (!quote) return;
const deposit = NDKCashuDeposit.fromQuoteEvent(this, quote);
if (this.depositMonitor.addDeposit(deposit)) {
deposit.finalize();
}
}
// src/wallets/cashu/event-handlers/token.ts
var import_ndk4 = require("@nostr-dev-kit/ndk");
var _cumulativeTime = 0;
var _cumulativeCalls = 0;
async function handleToken(event) {
if (this.state.tokens.has(event.id)) return;
const startTime = Date.now();
const token = await import_ndk4.NDKCashuToken.from(event);
if (!token) {
_cumulativeTime += Date.now() - startTime;
_cumulativeCalls++;
return;
}
_cumulativeTime += Date.now() - startTime;
_cumulativeCalls++;
for (const deletedTokenId of token.deletedTokens) {
this.state.removeTokenId(deletedTokenId);
}
this.state.addToken(token);
}
setInterval(() => {
}, 5e3);
// src/wallets/cashu/event-handlers/index.ts
var handlers = {
[import_ndk5.NDKKind.CashuToken]: handleToken,
[import_ndk5.NDKKind.CashuQuote]: handleQuote,
[import_ndk5.NDKKind.EventDeletion]: handleEventDeletion
};
var balanceUpdateTimer = null;
async function eventHandler(event) {
const handler = handlers[event.kind];
if (handler) {
if (balanceUpdateTimer) clearTimeout(balanceUpdateTimer);
await handler.call(this, event);
balanceUpdateTimer = setTimeout(() => {
this.emit("balance_updated");
}, 100);
}
}
async function eventDupHandler(_event, _relay, _timeSinceFirstSeen, _sub, _fromCache) {
}
// src/wallets/cashu/validate.ts
var import_cashu_ts2 = require("@cashu/cashu-ts");
var import_debug2 = __toESM(require("debug"), 1);
var d2 = (0, import_debug2.default)("ndk-wallet:cashu:validate");
async function consolidateTokens() {
d2("checking %d tokens for spent proofs", this.state.tokens.size);
const mints = new Set(
this.state.getMintsProofs({ validStates: /* @__PURE__ */ new Set(["available", "reserved", "deleted"]) }).keys()
);
d2("found %d mints", mints.size);
mints.forEach((mint) => {
consolidateMintTokens(mint, this);
});
}
async function consolidateMintTokens(mint, wallet, allProofs, onResult, onFailure) {
allProofs ??= wallet.state.getProofs({ mint, includeDeleted: true, onlyAvailable: false });
const _wallet = await walletForMint(mint);
if (!_wallet) {
return;
}
let proofStates = [];
try {
proofStates = await _wallet.checkProofsStates(allProofs);
} catch (e) {
onFailure?.(e.message);
return;
}
const spentProofs = [];
const unspentProofs = [];
const pendingProofs = [];
allProofs.forEach((proof, index) => {
const { state } = proofStates[index];
if (state === import_cashu_ts2.CheckStateEnum.SPENT) {
spentProofs.push(proof);
} else if (state === import_cashu_ts2.CheckStateEnum.UNSPENT) {
unspentProofs.push(proof);
} else {
pendingProofs.push(proof);
}
});
const walletChange = {
mint,
store: unspentProofs,
destroy: spentProofs
};
onResult?.(walletChange);
const _totalSpentProofs = spentProofs.reduce((acc, proof) => acc + proof.amount, 0);
if (walletChange.destroy?.length === 0) return;
walletChange.store?.push(...pendingProofs);
const totalPendingProofs = pendingProofs.reduce((acc, proof) => acc + proof.amount, 0);
wallet.state.reserveProofs(pendingProofs, totalPendingProofs);
return wallet.state.update(walletChange, "Consolidate");
}
// src/wallets/cashu/pay/ln.ts
var import_cashu_ts3 = require("@cashu/cashu-ts");
// src/wallets/cashu/wallet/fee.ts
function calculateFee(intendedAmount, providedProofs, returnedProofs) {
const totalProvided = providedProofs.reduce((acc, p) => acc + p.amount, 0);
const totalReturned = returnedProofs.reduce((acc, p) => acc + p.amount, 0);
const totalFee = totalProvided - intendedAmount - totalReturned;
if (totalFee < 0) {
throw new Error("Invalid fee calculation: received more proofs than sent to mint");
}
return totalFee;
}
// src/wallets/cashu/wallet/effect.ts
async function withProofReserve(wallet, cashuWallet, mint, amountWithFees, amountWithoutFees, cb) {
cashuWallet ??= await wallet.getCashuWallet(mint);
const availableMintProofs = wallet.state.getProofs({ mint, onlyAvailable: true });
const proofs = cashuWallet.selectProofsToSend(availableMintProofs, amountWithFees);
const fetchedAmount = proofs.send.reduce((a, b) => a + b.amount, 0);
if (fetchedAmount < amountWithFees) return null;
wallet.state.reserveProofs(proofs.send, amountWithFees);
let cbResult = null;
let proofsChange = null;
let updateRes = null;
try {
cbResult = await cb(proofs.send, availableMintProofs);
if (!cbResult) return null;
proofsChange = {
mint,
store: cbResult.change,
destroy: proofs.send
};
updateRes = await wallet.state.update(proofsChange);
} catch (e) {
wallet.state.unreserveProofs(proofs.send, amountWithFees, "available");
throw e;
}
if (!cbResult) return null;
return {
result: cbResult.result,
proofsChange,
stateUpdate: updateRes,
mint,
fee: calculateFee(amountWithoutFees, proofs.send, cbResult.change)
};
}
// src/wallets/cashu/pay/ln.ts
async function payLn(wallet, pr, { amount, unit } = {}) {
let invoiceAmount = getBolt11Amount(pr);
if (!invoiceAmount) throw new Error("invoice amount is required");
invoiceAmount = invoiceAmount / 1e3;
if (amount && unit) {
if (unit === "msat") {
amount = amount / 1e3;
}
}
const eligibleMints = wallet.getMintsWithBalance(invoiceAmount + 3);
if (!eligibleMints.length) {
return null;
}
for (const mint of eligibleMints) {
try {
const result = await executePayment(mint, pr, amount ?? invoiceAmount, wallet);
if (result) {
if (amount) {
result.fee = calculateFee(
amount,
result.proofsChange?.destroy ?? [],
result.proofsChange?.store ?? []
);
}
return result;
}
} catch (error) {
wallet.warn(`Failed to execute payment with min ${mint}: ${error}`);
}
}
return null;
}
async function executePayment(mint, pr, amountWithoutFees, wallet) {
const cashuWallet = await wallet.getCashuWallet(mint);
try {
const meltQuote = await cashuWallet.createMeltQuote(pr);
const amountToSend = meltQuote.amount + meltQuote.fee_reserve;
const result = await withProofReserve(
wallet,
cashuWallet,
mint,
amountToSend,
amountWithoutFees,
async (proofsToUse, _allOurProofs) => {
const meltResult = await cashuWallet.meltProofs(meltQuote, proofsToUse);
if (meltResult.quote.state === import_cashu_ts3.MeltQuoteState.PAID) {
return {
result: {
preimage: meltResult.quote.payment_preimage ?? ""
},
change: meltResult.change
};
}
return null;
}
);
return result;
} catch (e) {
if (e instanceof Error) {
if (e.message.match(/already spent/i)) {
setTimeout(() => {
consolidateMintTokens(mint, wallet);
}, 2500);
} else {
throw e;
}
}
return null;
}
}
// src/wallets/cashu/pay/nut.ts
var import_ndk6 = require("@nostr-dev-kit/ndk");
// src/utils/cashu.ts
function ensureIsCashuPubkey(pubkey) {
if (!pubkey) return;
let _pubkey = pubkey;
if (_pubkey.length === 64) _pubkey = `02${_pubkey}`;
if (_pubkey.length !== 66) throw new Error("Invalid pubkey");
return _pubkey;
}
async function mintProofs(wallet, quote, amount, mint, p2pk) {
const mintTokenAttempt = (resolve, reject, attempt) => {
const pubkey = ensureIsCashuPubkey(p2pk);
wallet.mintProofs(amount, quote.quote, { pubkey }).then((mintProofs2) => {
console.debug("minted tokens", mintProofs2);
resolve({
proofs: mintProofs2,
mint
});
}).catch((e) => {
attempt++;
if (attempt <= 3) {
console.error("error minting tokens", e);
setTimeout(() => mintTokenAttempt(resolve, reject, attempt), attempt * 1500);
} else {
reject(e);
}
});
};
return new Promise((resolve, reject) => {
mintTokenAttempt(resolve, reject, 0);
});
}
// src/wallets/cashu/pay/nut.ts
async function createToken(wallet, amount, recipientMints, p2pk) {
p2pk = ensureIsCashuPubkey(p2pk);
const myMintsWithEnoughBalance = wallet.getMintsWithBalance(amount);
const hasRecipientMints = recipientMints && recipientMints.length > 0;
const mintsInCommon = hasRecipientMints ? findMintsInCommon([recipientMints, myMintsWithEnoughBalance]) : myMintsWithEnoughBalance;
for (const mint of mintsInCommon) {
try {
const res = await createTokenInMint(wallet, mint, amount, p2pk);
if (res) {
return res;
}
} catch (_e) {
}
}
if (hasRecipientMints) {
return await createTokenWithMintTransfer(wallet, amount, recipientMints, p2pk);
}
return null;
}
async function createTokenInMint(wallet, mint, amount, p2pk) {
const cashuWallet = await wallet.getCashuWallet(mint);
try {
const result = await withProofReserve(
wallet,
cashuWallet,
mint,
amount,
amount,
async (proofsToUse, allOurProofs) => {
const sendResult = await cashuWallet.send(amount, proofsToUse, {
pubkey: p2pk,
proofsWeHave: allOurProofs
});
return {
result: {
proofs: sendResult.send,
mint
},
change: sendResult.keep,
mint
};
}
);
return result;
} catch (_e) {
}
return null;
}
async function createTokenWithMintTransfer(wallet, amount, recipientMints, p2pk) {
const generateQuote = async () => {
const generateQuoteFromSomeMint = async (mint3) => {
const targetMintWallet3 = await walletForMint(mint3);
if (!targetMintWallet3) throw new Error(`unable to load wallet for mint ${mint3}`);
const quote3 = await targetMintWallet3.createMintQuote(amount);
return { quote: quote3, mint: mint3, targetMintWallet: targetMintWallet3 };
};
const quotesPromises = recipientMints.map(generateQuoteFromSomeMint);
const { quote: quote2, mint: mint2, targetMintWallet: targetMintWallet2 } = await Promise.any(quotesPromises);
if (!quote2) {
throw new Error("failed to get quote from any mint");
}
return { quote: quote2, mint: mint2, targetMintWallet: targetMintWallet2 };
};
const { quote, mint: targetMint, targetMintWallet } = await generateQuote();
if (!quote) {
return null;
}
const invoiceAmount = getBolt11Amount(quote.request);
if (!invoiceAmount) throw new Error("invoice amount is required");
const invoiceAmountInSat = invoiceAmount / 1e3;
if (invoiceAmountInSat > amount)
throw new Error(`invoice amount is more than the amount passed in (${invoiceAmountInSat} vs ${amount})`);
const payLNResult = await payLn(wallet, quote.request, { amount });
if (!payLNResult) {
return null;
}
const { proofs, mint } = await mintProofs(targetMintWallet, quote, amount, targetMint, p2pk);
return {
...payLNResult,
result: { proofs, mint },
fee: payLNResult.fee
};
}
function findMintsInCommon(mintCollections) {
const mintCounts = /* @__PURE__ */ new Map();
for (const mints of mintCollections) {
for (const mint of mints) {
const normalizedMint = (0, import_ndk6.normalizeUrl)(mint);
if (!mintCounts.has(normalizedMint)) {
mintCounts.set(normalizedMint, 1);
} else {
mintCounts.set(normalizedMint, mintCounts.get(normalizedMint) + 1);
}
}
}
const commonMints = [];
for (const [mint, count] of mintCounts.entries()) {
if (count === mintCollections.length) {
commonMints.push(mint);
}
}
return commonMints;
}
// src/wallets/cashu/wallet/payment.ts
var PaymentHandler = class {
wallet;
constructor(wallet) {
this.wallet = wallet;
}
/**
* Pay a LN invoice with this wallet. This will used cashu proofs to pay a bolt11.
*/
async lnPay(payment, createTxEvent = true) {
if (!payment.pr) throw new Error("pr is required");
const invoiceAmount = getBolt11Amount(payment.pr);
if (!invoiceAmount) throw new Error("invoice amount is required");
if (payment.amount && invoiceAmount > payment.amount) {
throw new Error("invoice amount is more than the amount passed in");
}
const res = await payLn(this.wallet, payment.pr, {
amount: payment.amount,
unit: payment.unit
});
if (!res?.result?.preimage) return;
if (createTxEvent) {
createOutTxEvent(this.wallet.ndk, payment, res, this.wallet.relaySet);
}
return res.result;
}
/**
* Swaps tokens to a specific amount, optionally locking to a p2pk.
*/
async cashuPay(payment) {
const satPayment = { ...payment };
if (satPayment.unit?.startsWith("msat")) {
satPayment.amount = satPayment.amount / 1e3;
satPayment.unit = "sat";
}
let createResult = await createToken(this.wallet, satPayment.amount, payment.mints, payment.p2pk);
if (!createResult) {
if (payment.allowIntramintFallback) {
createResult = await createToken(this.wallet, satPayment.amount, void 0, payment.p2pk);
}
if (!createResult) {
return;
}
}
createOutTxEvent(this.wallet.ndk, satPayment, createResult, this.wallet.relaySet);
return createResult.result;
}
};
// src/wallets/cashu/wallet/state/balance.ts
function getBalance(opts) {
const proofs = this.getProofEntries(opts);
return proofs.reduce((sum, proof) => sum + proof.proof.amount, 0);
}
function getMintsBalances({ onlyAvailable } = { onlyAvailable: true }) {
const balances = {};
const proofs = this.getProofEntries({ onlyAvailable });
for (const proof of proofs) {
if (!proof.mint) continue;
balances[proof.mint] ??= 0;
balances[proof.mint] += proof.proof.amount;
}
return balances;
}
// src/wallets/cashu/wallet/state/proofs.ts
function addProof(proofEntry) {
this.proofs.set(proofEntry.proof.C, proofEntry);
this.journal.push({
memo: "Added proof",
timestamp: Date.now(),
metadata: {
type: "proof",
id: proofEntry.proof.C,
amount: proofEntry.proof.amount,
mint: proofEntry.mint
}
});
}
function reserveProofs(proofs, amount) {
for (const proof of proofs) {
this.updateProof(proof, { state: "reserved" });
}
this.reserveAmounts.push(amount);
}
function unreserveProofs(proofs, amount, newState) {
for (const proof of proofs) {
this.updateProof(proof, { state: newState });
}
const index = this.reserveAmounts.indexOf(amount);
if (index !== -1) {
this.reserveAmounts.splice(index, 1);
} else {
throw new Error(`BUG: Amount ${amount} not found in reserveAmounts`);
}
}
function getProofEntries(opts = {}) {
const proofs = /* @__PURE__ */ new Map();
const validStates = /* @__PURE__ */ new Set(["available"]);
let { mint, onlyAvailable, includeDeleted } = opts;
onlyAvailable ??= true;
if (!onlyAvailable) validStates.add("reserved");
if (includeDeleted) validStates.add("deleted");
for (const proofEntry of this.proofs.values()) {
if (mint && proofEntry.mint !== mint) continue;
if (!validStates.has(proofEntry.state)) continue;
if (!proofEntry.proof) continue;
proofs.set(proofEntry.proof.C, proofEntry);
}
return Array.from(proofs.values());
}
function updateProof(proof, state) {
const proofC = proof.C;
const currentState = this.proofs.get(proofC);
if (!currentState) throw new Error("Proof not found");
const newState = { ...currentState, ...state };
this.proofs.set(proofC, newState);
this.journal.push({
memo: `Updated proof state: ${JSON.stringify(state)}`,
timestamp: Date.now(),
metadata: {
type: "proof",
id: proofC,
amount: proof.amount,
mint: currentState.mint
}
});
}
// src/wallets/cashu/wallet/state/token.ts
function addToken(token) {
if (!token.mint) throw new Error("BUG: Token has no mint");
const currentEntry = this.tokens.get(token.id);
const state = currentEntry?.state ?? "available";
this.tokens.set(token.id, { token, state });
let _added = 0;
let _invalid = 0;
for (const proof of token.proofs) {
const val = maybeAssociateProofWithToken(this, proof, token, state);
if (val === false) {
_invalid++;
} else {
_added++;
}
}
}
function maybeAssociateProofWithToken(walletState, proof, token, state) {
const proofC = proof.C;
const proofEntry = walletState.proofs.get(proofC);
if (!proofEntry) {
walletState.addProof({
mint: token.mint,
state,
tokenId: token.id,
timestamp: token.created_at,
proof
});
return true;
}
if (proofEntry.tokenId) {
if (proofEntry.tokenId === token.id) {
return null;
}
const existingTokenEntry = walletState.tokens.get(proofEntry.tokenId);
if (!existingTokenEntry) {
throw new Error(
`BUG: Token id ${proofEntry.tokenId} not found, was expected to be associated with proof ${proofC}`
);
}
const existingToken = existingTokenEntry.token;
if (existingToken) {
if (existingToken.created_at && (!token.created_at || token.created_at < existingToken.created_at)) {
return false;
}
}
walletState.updateProof(proof, { tokenId: token.id, state });
return true;
}
walletState.updateProof(proof, { tokenId: token.id, state });
return true;
}
function removeTokenId(tokenId) {
const currentEntry = this.tokens.get(tokenId) || {};
this.tokens.set(tokenId, { ...currentEntry, state: "deleted" });
for (const proofEntry of this.proofs.values()) {
const { proof } = proofEntry;
if (proofEntry.tokenId === tokenId) {
if (!proof) {
throw new Error("BUG: Proof entry has no proof");
}
this.updateProof(proof, { state: "deleted" });
}
}
}
// src/wallets/cashu/wallet/state/update.ts
var import_ndk7 = require("@nostr-dev-kit/ndk");
async function update(stateChange, _memo) {
updateInternalState(this, stateChange);
this.wallet.emit("balance_updated");
return updateExternalState(this, stateChange);
}
function updateInternalState(walletState, stateChange) {
if (stateChange.store && stateChange.store.length > 0) {
for (const proof of stateChange.store) {
walletState.addProof({
mint: stateChange.mint,
state: "available",
proof,
timestamp: Date.now()
});
}
}
if (stateChange.destroy && stateChange.destroy.length > 0) {
for (const proof of stateChange.destroy) {
walletState.updateProof(proof, { state: "deleted" });
}
}
if (stateChange.reserve && stateChange.reserve.length > 0) {
throw new Error("BUG: Proofs should not be reserved via update");
}
}
async function updateExternalState(walletState, stateChange) {
const newState = calculateNewState(walletState, stateChange);
if (newState.deletedTokenIds.size > 0) {
const deleteEvent = new import_ndk7.NDKEvent(walletState.wallet.ndk, {
kind: import_ndk7.NDKKind.EventDeletion,
tags: [
["k", import_ndk7.NDKKind.CashuToken.toString()],
...Array.from(newState.deletedTokenIds).map((id) => ["e", id])
]
});
await deleteEvent.sign();
publishWithRetry(walletState, deleteEvent, walletState.wallet.relaySet);
for (const tokenId of newState.deletedTokenIds) {
walletState.removeTokenId(tokenId);
}
}
const res = {};
if (newState.saveProofs.length > 0) {
const newToken = await createTokenEvent(walletState, stateChange.mint, newState);
res.created = newToken;
}
return res;
}
async function publishWithRetry(walletState, event, relaySet, retryTimeout = 10 * 1e3) {
let publishResult;
publishResult = await event.publish(relaySet);
let type;
if (event.kind === import_ndk7.NDKKind.EventDeletion) type = "deletion";
if (event.kind === import_ndk7.NDKKind.CashuToken) type = "token";
if (event.kind === import_ndk7.NDKKind.CashuWallet) type = "wallet";
const journalEntryMetadata = {
type,
id: event.id,
relayUrl: relaySet?.relayUrls.join(",")
};
if (publishResult) {
walletState.journal.push({
memo: `Publish kind:${event.kind} succeesfully`,
timestamp: Date.now(),
metadata: journalEntryMetadata
});
return publishResult;
}
walletState.journal.push({
memo: "Publish failed",
timestamp: Date.now(),
metadata: journalEntryMetadata
});
setTimeout(() => {
publishWithRetry(walletState, event, relaySet, retryTimeout);
}, retryTimeout);
}
async function createTokenEvent(walletState, mint, newState) {
const newToken = new import_ndk7.NDKCashuToken(walletState.wallet.ndk);
newToken.mint = mint;
newToken.proofs = newState.saveProofs;
await newToken.toNostrEvent();
walletState.addToken(newToken);
newToken.deletedTokens = Array.from(newState.deletedTokenIds);
await newToken.sign();
walletState.addToken(newToken);
publishWithRetry(walletState, newToken, walletState.wallet.relaySet);
return newToken;
}
function calculateNewState(walletState, stateChange) {
const destroyProofs = /* @__PURE__ */ new Set();
for (const proof of stateChange.destroy || []) destroyProofs.add(proof.C);
const proofsToStore = /* @__PURE__ */ new Map();
let tokensToDelete;
for (const proof of stateChange.store || []) proofsToStore.set(proof.C, proof);
tokensToDelete = getAffectedTokens(walletState, stateChange);
for (const token of tokensToDelete.values()) {
for (const proof of token.proofs) {
if (destroyProofs.has(proof.C)) continue;
proofsToStore.set(proof.C, proof);
}
}
return {
deletedTokenIds: new Set(tokensToDelete.keys()),
deletedProofs: destroyProofs,
reserveProofs: [],
saveProofs: Array.from(proofsToStore.values())
};
}
function getAffectedTokens(walletState, stateChange) {
const tokens = /* @__PURE__ */ new Map();
for (const proof of stateChange.destroy || []) {
const proofEntry = walletState.proofs.get(proof.C);
if (!proofEntry) {
continue;
}
const tokenId = proofEntry.tokenId;
if (!tokenId) {
continue;
}
const tokenEntry = walletState.tokens.get(tokenId);
if (!tokenEntry?.token) {
continue;
}
tokens.set(tokenId, tokenEntry.token);
}
return tokens;
}
// src/wallets/cashu/wallet/state/index.ts
var WalletState = class {
constructor(wallet, reservedProofCs = /* @__PURE__ */ new Set()) {
this.wallet = wallet;
this.reservedProofCs = reservedProofCs;
}
/**
* the amounts that are intended to be reserved
* this is the net amount we are trying to pay out,
* excluding fees and coin sizes
* e.g. we might want to pay 5 sats, have 2 sats in fees
* and we're using 2 inputs that add up to 8, the reserve amount is 5
* while the reserve proofs add up to 8
*/
reserveAmounts = [];
/**
* Source of truth of the proofs this wallet has/had.
*/
proofs = /* @__PURE__ */ new Map();
/**
* The tokens that are known to this wallet.
*/
tokens = /* @__PURE__ */ new Map();
journal = [];
/** This is a debugging function that dumps the state of the wallet */
dump() {
const res = {
proofs: Array.from(this.proofs.values()),
balances: this.getMintsBalance(),
totalBalance: this.getBalance(),
tokens: Array.from(this.tokens.values())
};
return res;
}
/***************************
* Tokens
***************************/
addToken = addToken.bind(this);
removeTokenId = removeTokenId.bind(this);
/***************************
* Proof management
***************************/
addProof = addProof.bind(this);
/**
* Reserves a number of selected proofs and a specific amount.
*
* The amount and total of the proofs don't need to match. We
* might want to use 5 sats and have 2 proofs of 4 sats each.
* In that case, the reserve amount is 5, while the reserve proofs
* add up to 8.
*/
reserveProofs = reserveProofs.bind(this);
/**
* Unreserves a number of selected proofs and a specific amount.
*/
unreserveProofs = unreserveProofs.bind(this);
/**
* Returns all proof entries, optionally filtered by mint and state
*/
getProofEntries = getProofEntries.bind(this);
/**
* Updates information about a proof
*/
updateProof = updateProof.bind(this);
/**
* Returns all proofs, optionally filtered by mint and state
* @param opts.mint - optional mint to filter by
* @param opts.onlyAvailable - only include available proofs @default true
* @param opts.includeDeleted - include deleted proofs @default false
*/
getProofs(opts) {
return this.getProofEntries(opts).map((entry) => entry.proof);
}
getTokens(opts = { onlyAvailable: true }) {
const proofEntries = this.getProofEntries(opts);
const tokens = /* @__PURE__ */ new Map();
for (const proofEntry of proofEntries) {
const tokenId = proofEntry.tokenId ?? null;
const current = tokens.get(tokenId) ?? {
tokenId,
mint: proofEntry.mint,
proofEntries: []
};
current.token ??= tokenId ? this.tokens.get(tokenId)?.token : void 0;
current.proofEntries.push(proofEntry);
tokens.set(tokenId, current);
}
return tokens;
}
/**
* Gets a list of proofs for each mint
* @returns
*/
getMintsProofs({
validStates = /* @__PURE__ */ new Set(["available"])
} = {}) {
const mints = /* @__PURE__ */ new Map();
for (const entry of this.proofs.values()) {
if (!entry.mint || !entry.proof) continue;
if (!validStates.has(entry.state)) continue;
const current = mints.get(entry.mint) || [];
current.push(entry.proof);
mints.set(entry.mint, current);
}
return mints;
}
/***************************
* Balance
***************************/
/**
* Returns the balance of the wallet, optionally filtered by mint and state
*
* @params opts.mint - optional mint to filter by
* @params opts.onlyAvailable - only include available proofs @default true
*/
getBalance = getBalance.bind(this);
/**
* Returns the balances of the different mints
*
* @params opts.onlyAvailable - only include available proofs @default true
*/
getMintsBalance = getMintsBalances.bind(this);
/***************************
* State update
***************************/
update = update.bind(this);
};
// src/wallets/cashu/wallet/index.ts
var NDKCashuWallet = class _NDKCashuWallet extends NDKWallet {
get type() {
return "nip-60";
}
_p2pk;
sub;
status = "initial" /* INITIAL */;
static kind = import_ndk8.NDKKind.CashuWallet;
static kinds = [import_ndk8.NDKKind.CashuWallet];
mints = [];
privkeys = /* @__PURE__ */ new Map();
signer;
walletId = "nip-60";
depositMonitor = new NDKCashuDepositMonitor();
/**
* Warnings that have been raised
*/
warnings = [];
paymentHandler;
state;
relaySet;
constructor(ndk) {
super(ndk);
this.ndk = ndk;
this.paymentHandler = new PaymentHandler(this);
this.state = new WalletState(this);
}
/**
* Generates a backup event for this wallet
*/
async backup(publish = true) {
if (this.privkeys.size === 0) throw new Error("no privkey to backup");
const backup = new NDKCashuWalletBackup(this.ndk);
const privkeys = [];
for (const [_pubkey, signer] of this.privkeys.entries()) {
privkeys.push(signer.privateKey);
}
backup.privkeys = privkeys;
backup.mints = this.mints;
if (publish) backup.save(this.relaySet);
return backup;
}
consolidateTokens = consolidateTokens.bind(this);
/**
* Generates nuts that can be used to send to someone.
*
* Note that this function does not send anything, it just generates a specific amount of proofs.
* @param amounts
* @returns
*/
async mintNuts(amounts) {
let result;
const totalAmount = amounts.reduce((acc, amount) => acc + amount, 0);
for (const mint of this.mints) {
const wallet = await this.getCashuWallet(mint);
const mintProofs2 = await this.state.getProofs({ mint });
result = await wallet.send(totalAmount, mintProofs2, {
proofsWeHave: mintProofs2,
includeFees: true,
outputAmounts: {
sendAmounts: amounts
}
});
if (result.send.length > 0) {
const change = { store: result?.keep ?? [], destroy: result.send, mint };
const updateRes = await this.state.update(change);
createOutTxEvent(
this.ndk,
{
paymentDescription: "minted nuts",
amount: amounts.reduce((acc, amount) => acc + amount, 0)
},
{
result: { proofs: result.send, mint },
proofsChange: change,
stateUpdate: updateRes,
mint,
fee: 0
},
this.relaySet
);
this.emit("balance_updated");
return result;
}
}
}
/**
* Loads a wallet information from an event
* @param event
*/
async loadFromEvent(event) {
const _event = new import_ndk8.NDKEvent(event.ndk, event.rawEvent());
await _event.decrypt();
const content = JSON.parse(_event.content);
for (const tag of content) {
if (tag[0] === "mint") {
this.mints.push(tag[1]);
} else if (tag[0] === "privkey") {
await this.addPrivkey(tag[1]);
}
}
await this.getP2pk();
}
static async from(event) {
if (!event.ndk) throw new Error("no ndk instance on event");
const wallet = new _NDKCashuWallet(event.ndk);
await wallet.loadFromEvent(event);
return wallet;
}
/**
* Starts monitoring the wallet.
*
* Use `since` to start syncing state from a specific timestamp. This should be
* used by storing at the app level a time in which we know we were able to communicate
* with the relays, for example, by saving the time the wallet has emitted a "ready" event.
*/
start(opts) {
const activeUser = this.ndk?.activeUser;
if (this.status === "ready" /* READY */) return Promise.resolve();
this.status = "loading" /* LOADING */;
const pubkey = opts?.pubkey ?? activeUser?.pubkey;
if (!pubkey) throw new Error("no pubkey");
const filters = [
{ kinds: [import_ndk8.NDKKind.CashuToken], authors: [pubkey] },
{ kinds: [import_ndk8.NDKKind.CashuQuote], authors: [pubkey] },
{
kinds: [import_ndk8.NDKKind.EventDeletion],
authors: [pubkey],
"#k": [import_ndk8.NDKKind.CashuToken.toString()]
}
];
if (opts?.since) {
filters[0].since = opts.since;
filters[1].since = opts.since;
filters[2].since = opts.since;
}
const subOpts = opts ?? {};
subOpts.subId ??= "cashu-wallet-state";
return new Promise((resolve) => {
this.sub = this.ndk.subscribe(filters, { ...subOpts, relaySet: this.relaySet }, false);
this.sub.on("event:dup", eventDupHandler.bind(this));
this.sub.on("event", (event) => {
eventHandler.call(this, event);
});
this.sub.on("eose", () => {
this.emit("ready");
this.status = "ready" /* READY */;
resolve();
});
this.sub.start();
});
}
stop() {
this.sub?.stop();
this.status = "initial" /* INITIAL */;
}
/**
* Returns the p2pk of this wallet or generates a new one if we don't have one
*/
async getP2pk() {
if (this._p2pk) return this._p2pk;
if (this.privkeys.size === 0) {
const signer = import_ndk8.NDKPrivateKeySigner.generate();
await this.addPrivkey(signer.privateKey);
}
return this.p2pk;
}
/**
* If this wallet has access to more than one privkey, this will return all of them.
*/
get p2pks() {
return Array.from(this.privkeys.keys());
}
async addPrivkey(privkey) {
const signer = new import_ndk8.NDKPrivateKeySigner(privkey);
const user = await signer.user();
this.privkeys.set(user.pubkey, signer);
this._p2pk ??= user.pubkey;
return this._p2pk;
}
get p2pk() {
if (!this._p2pk) throw new Error("p2pk not set");
return this._p2pk;
}
set p2pk(pubkey) {
if (this.privkeys.has(pubkey)) {
this.signer = this.privkeys.get(pubkey);
this.p2pk = pubkey;
} else {
throw new Error(`privkey for ${pubkey} not found`);
}
}
/**
* Generates the payload for a wallet event
*/
walletPayload() {
const privkeys = Arra