diff --git a/src/daemon/wallet/sdk-mint-fallback.ts b/src/daemon/wallet/sdk-mint-fallback.ts index 7895254..dc96fd2 100644 --- a/src/daemon/wallet/sdk-mint-fallback.ts +++ b/src/daemon/wallet/sdk-mint-fallback.ts @@ -13,6 +13,7 @@ type TopUpOptions = { baseUrl: string; amount: number; token?: string; + excludeMints?: string[]; }; type TopUpResult = { @@ -298,11 +299,12 @@ export function installCreateProviderTokenFallback( const originalCreateProviderToken = balanceManager.createProviderToken.bind(balanceManager); balanceManager.createProviderToken = async (options: TopUpOptions): Promise => { - const candidates = await getTopUpMintCandidates( + const inheritedExclusions = new Set(options.excludeMints ?? []); + const candidates = (await getTopUpMintCandidates( options.mintUrl, walletClient, walletAdapter, - ); + )).filter((mintUrl) => !inheritedExclusions.has(mintUrl)); let lastResult: TopUpResult | undefined; for (const [index, mintUrl] of candidates.entries()) { @@ -312,7 +314,19 @@ export function installCreateProviderTokenFallback( ); } - const result = await originalCreateProviderToken({ ...options, mintUrl }); + // The SDK treats mintUrl as a preference, not a requirement: its own + // balance-based candidate can still win and make every outer retry hit + // the same failed mint. Excluding every other known candidate makes this + // attempt use the mint selected by this fallback loop. + const excludeMints = uniqueMintUrls([ + ...inheritedExclusions, + ...candidates.filter((candidate) => candidate !== mintUrl), + ]); + const result = await originalCreateProviderToken({ + ...options, + mintUrl, + excludeMints, + }); if (result.success) { return result; } diff --git a/tests/sdk-mint-fallback.test.ts b/tests/sdk-mint-fallback.test.ts index 585a8d2..eabb80d 100644 --- a/tests/sdk-mint-fallback.test.ts +++ b/tests/sdk-mint-fallback.test.ts @@ -269,6 +269,59 @@ describe("SDK top-up mint fallback", () => { expect(attempts).toEqual(["https://mint-a.example", "https://mint-b.example"]); }); + test("forces the requested fallback mint by excluding previously failed mints", async () => { + const attempts: Array<{ preferred: string; selected: string; excluded: string[] }> = []; + const availableMints = ["https://mint-a.example", "https://mint-b.example"]; + const balanceManager = { + topUp: async (_options: { mintUrl: string }) => ({ success: false, message: "not patched" }), + // Reproduce the SDK's selection order: its automatic balance choice wins + // over preferredMintUrl unless the earlier mint is explicitly excluded. + createProviderToken: async (options: { mintUrl: string; excludeMints?: string[] }) => { + const excluded = options.excludeMints ?? []; + const selected = availableMints.find((mint) => !excluded.includes(mint))!; + attempts.push({ preferred: options.mintUrl, selected, excluded }); + if (selected === "https://mint-a.example") { + return { success: false, error: "Send failed: Not enough proofs to send" }; + } + return { success: true, token: "cashu-token", selectedMintUrl: selected, amountSpent: 21 }; + }, + }; + const client = { getBalanceManager: () => balanceManager }; + const walletAdapter = { + getBalances: async () => ({ + "https://mint-a.example": 2_916, + "https://mint-b.example": 30_000, + }), + }; + + installMintFallbackTopUp( + client, + createCocodClient(availableMints), + walletAdapter, + { log: () => undefined, warn: () => undefined, error: () => undefined }, + ); + + const result = await balanceManager.createProviderToken({ + mintUrl: "https://mint-a.example", + baseUrl: "https://provider.example", + amount: 389, + }); + + expect(result.success).toBe(true); + expect(attempts).toEqual([ + { + preferred: "https://mint-a.example", + selected: "https://mint-a.example", + excluded: ["https://mint-b.example"], + }, + { + preferred: "https://mint-b.example", + selected: "https://mint-b.example", + excluded: ["https://mint-a.example"], + }, + ]); + }); + test("createProviderToken falls back to routstr-core invoice + NWC payment", async () => { const createAttempts: string[] = []; const balanceManager = {