124 lines
3.2 KiB
TypeScript
124 lines
3.2 KiB
TypeScript
import {
|
|
CashuMint,
|
|
CashuWallet,
|
|
MintQuoteState,
|
|
type Proof,
|
|
} from "@cashu/cashu-ts";
|
|
|
|
import { privateKeyFromSeedWords as nostrPrivateKeyFromSeedWords } from "nostr-tools/nip06";
|
|
import { finalizeEvent, nip98 } from "nostr-tools";
|
|
import type { BodyInit } from "bun";
|
|
|
|
interface NpubCashQuote {
|
|
createdAt: number;
|
|
paidAt?: number;
|
|
expiresAt: number;
|
|
mintUrl: string;
|
|
quoteId: string;
|
|
request: string;
|
|
amount: number;
|
|
state: "PAID" | "ISSUED" | "INFLIGHT";
|
|
locked: boolean;
|
|
}
|
|
|
|
interface NpubCashPaginationMetadata {
|
|
total: number;
|
|
limit: number;
|
|
}
|
|
|
|
export default class NpubCash {
|
|
constructor(private mnemonic: string) {}
|
|
|
|
private async fetchWithNip98<T>(
|
|
{ url, method, body }: {
|
|
url: string;
|
|
method: string;
|
|
body?: BodyInit | null;
|
|
},
|
|
): Promise<T> {
|
|
const nostrPrivateKey = nostrPrivateKeyFromSeedWords(this.mnemonic);
|
|
const urlWithoutQueryParams = url.split("?")[0] ?? "";
|
|
const npubCashNip98 = await nip98.getToken(
|
|
urlWithoutQueryParams,
|
|
method,
|
|
(event) => finalizeEvent(event, nostrPrivateKey),
|
|
);
|
|
return fetch(url, {
|
|
method,
|
|
headers: {
|
|
"Authorization": `Nostr ${npubCashNip98}`,
|
|
},
|
|
body,
|
|
}).then((data) => data.json() as Promise<T>);
|
|
}
|
|
|
|
private async getQuotes(since: number, limit: number, offset: number) {
|
|
return this.fetchWithNip98<
|
|
{
|
|
error: false;
|
|
data: { quotes: NpubCashQuote[] };
|
|
metadata: NpubCashPaginationMetadata;
|
|
} | {
|
|
error: true;
|
|
message: string;
|
|
}
|
|
>({
|
|
url:
|
|
`https://npubx.cash/api/v2/wallet/quotes?since=${since}&limit=${limit}&offset=${offset}`,
|
|
method: "GET",
|
|
});
|
|
}
|
|
|
|
private async getAllPaidQuotes(since: number) {
|
|
const quotes: NpubCashQuote[] = [];
|
|
let offset = 0;
|
|
while (true) {
|
|
const currentQuotes = await this.getQuotes(since, 50, offset);
|
|
if (currentQuotes.error === false) {
|
|
quotes.push(...currentQuotes.data.quotes);
|
|
if (quotes.length >= currentQuotes.metadata.total) {
|
|
break;
|
|
}
|
|
} else {
|
|
throw new Error(currentQuotes.message ?? "Unknown error");
|
|
}
|
|
offset += 50;
|
|
}
|
|
return quotes.sort((a, b) => a.createdAt - b.createdAt).filter(
|
|
(quote) => quote.state === "PAID",
|
|
);
|
|
}
|
|
|
|
async *tryRedeemUnredeemedCashuQuotes(
|
|
latestRedeemedCashuQuoteTimestamp: number,
|
|
) {
|
|
const proofs: Proof[] = [];
|
|
const quotes: NpubCashQuote[] = await this.getAllPaidQuotes(
|
|
latestRedeemedCashuQuoteTimestamp,
|
|
);
|
|
for (const quote of quotes) {
|
|
const mint = new CashuMint(quote.mintUrl);
|
|
const wallet = new CashuWallet(mint);
|
|
const req = await mint.checkMintQuote(quote.quoteId);
|
|
if (
|
|
req.state === MintQuoteState.PAID && typeof quote.paidAt === "number"
|
|
) {
|
|
const newProofs = await wallet.mintProofs(
|
|
quote.amount,
|
|
quote.quoteId,
|
|
);
|
|
proofs.push(...newProofs);
|
|
const amountReceived = newProofs.reduce(
|
|
(sum, p) => sum + p.amount,
|
|
0,
|
|
);
|
|
yield {
|
|
quoteId: quote.quoteId,
|
|
amountSat: amountReceived,
|
|
timestamp: quote.paidAt,
|
|
};
|
|
}
|
|
}
|
|
return proofs;
|
|
}
|
|
}
|