@nostr-dev-kit/ndk-wallet
Version:
194 lines (159 loc) • 6.05 kB
text/typescript
import type { Proof } from "@cashu/cashu-ts";
import type { NDKCashuToken, NDKEvent } from "@nostr-dev-kit/ndk";
import createDebug from "debug";
import { EventEmitter } from "tseep";
import { NDKCashuQuote } from "./quote";
import type { NDKCashuWallet } from "./wallet/index.js";
import { createInTxEvent } from "./wallet/txs.js";
const d = createDebug("ndk-wallet:cashu:deposit");
function randomMint(wallet: NDKCashuWallet) {
const mints = wallet.mints;
const mint = mints[Math.floor(Math.random() * mints.length)];
return mint;
}
export class NDKCashuDeposit extends EventEmitter<{
success: (token: NDKCashuToken) => void;
error: (error: string) => void;
}> {
public mint: string;
public amount: number;
public quoteId: string | undefined;
private wallet: NDKCashuWallet;
public checkTimeout: NodeJS.Timeout | undefined;
public checkIntervalLength = 2500;
public finalized = false;
private quoteEvent?: NDKEvent;
constructor(wallet: NDKCashuWallet, amount: number, mint?: string) {
super();
this.wallet = wallet;
this.mint = mint || randomMint(wallet);
this.amount = amount;
}
static fromQuoteEvent(wallet: NDKCashuWallet, quote: NDKCashuQuote) {
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;
// register deposit with monitor
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)
*/
private async createQuoteEvent(quoteId: string, bolt11: string) {
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: any) {
d("error saving quote on event %s", e.relayErrors);
}
return quoteEvent;
}
private async runCheck() {
if (!this.finalized) await this.finalize();
if (!this.finalized) this.delayCheck();
}
private 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?: number) {
this.runCheck();
if (timeout) {
setTimeout(() => {
clearTimeout(this.checkTimeout);
}, timeout);
}
}
async finalize() {
if (!this.quoteId) throw new Error("No quoteId set.");
let proofs: Proof[];
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: any) {
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 += 5000;
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);
// delete the quote event if it exists
this.destroyQuoteEvent();
} catch (e: any) {
this.emit("error", e.message);
console.error(e);
}
}
private async destroyQuoteEvent() {
if (!this.quoteEvent) return;
const deleteEvent = await this.quoteEvent.delete(undefined, false);
deleteEvent.publish(this.wallet.relaySet);
}
}