import type { Proof, SendResponse } from "@cashu/cashu-ts"; import { type CashuPaymentInfo, normalizeUrl } from "@nostr-dev-kit/ndk"; import { ensureIsCashuPubkey, mintProofs } from "../../../utils/cashu"; import { getBolt11Amount } from "../../../utils/ln"; import { walletForMint } from "../mint"; import type { MintUrl } from "../mint/utils"; import { type WalletOperation, withProofReserve } from "../wallet/effect"; import type { NDKCashuWallet } from "../wallet/index.js"; import { payLn } from "./ln"; export type NutPayment = CashuPaymentInfo & { amount: number }; /** * Generates proof to satisfy a payment. * * This function exclusively creates the sendable proofs in the mint, it doesn't modify * the state of the wallet, send tokens or generate any type of event. * * When no recipientMints are provided, the function will mint in one of the mints the wallet has enough balance for. */ export async function createToken( wallet: NDKCashuWallet, amount: number, recipientMints?: MintUrl[], p2pk?: string, ): Promise | null> { 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; } /** * Generates sendable proofs in a specific mint. * @param pay * @param mint * @returns */ async function createTokenInMint( wallet: NDKCashuWallet, mint: MintUrl, amount: number, p2pk?: string, ): Promise | null> { 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: any) {} return null; } /** * Iterate through the mints to find one that can satisfy a minting request * for the desired amount in any of the mints the recipient accepts. */ async function createTokenWithMintTransfer( wallet: NDKCashuWallet, amount: number, recipientMints: MintUrl[], p2pk?: string, ): Promise | null> { const generateQuote = async () => { const generateQuoteFromSomeMint = async (mint: MintUrl) => { const targetMintWallet = await walletForMint(mint); if (!targetMintWallet) throw new Error(`unable to load wallet for mint ${mint}`); const quote = await targetMintWallet.createMintQuote(amount); return { quote, mint, targetMintWallet }; }; const quotesPromises = recipientMints.map(generateQuoteFromSomeMint); const { quote, mint, targetMintWallet } = await Promise.any(quotesPromises); if (!quote) { throw new Error("failed to get quote from any mint"); } return { quote, mint, targetMintWallet }; }; // generate quote const { quote, mint: targetMint, targetMintWallet } = await generateQuote(); if (!quote) { return null; } // TODO: create a CashuDeposit event? const invoiceAmount = getBolt11Amount(quote.request); if (!invoiceAmount) throw new Error("invoice amount is required"); const invoiceAmountInSat = invoiceAmount / 1000; 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, }; } /** * The result of generating proofs to pay something, whether it's funded with a swap or LN. */ export type TokenCreationResult = { proofs: Proof[]; mint: MintUrl; }; export type TokenWithMint = SendResponse & { mint: MintUrl; fee?: number }; /** * Finds mints in common in the intersection of the arrays of mints * @example * const user1Mints = ["mint1", "mint2"]; * const user2Mints = ["mint2", "mint3"]; * const user3Mints = ["mint1", "mint2"]; * * findMintsInCommon([user1Mints, user2Mints, user3Mints]); * * // returns ["mint2"] */ export function findMintsInCommon(mintCollections: string[][]) { const mintCounts = new Map(); for (const mints of mintCollections) { for (const mint of mints) { const normalizedMint = normalizeUrl(mint); if (!mintCounts.has(normalizedMint)) { mintCounts.set(normalizedMint, 1); } else { mintCounts.set(normalizedMint, mintCounts.get(normalizedMint)! + 1); } } } const commonMints: string[] = []; for (const [mint, count] of mintCounts.entries()) { if (count === mintCollections.length) { commonMints.push(mint); } } return commonMints; }