fix: force fallback loop to use selected mint by excluding failed mints

The SDK treats mintUrl as a preference, not a requirement — its own
balance-based candidate could win and make every outer retry hit the
same failed mint. Thread excludeMints through TopUpOptions and, for
each fallback candidate, exclude every other known candidate so the
SDK is forced to use the loop's chosen mint.
This commit is contained in:
Paperclip Deployment Engineer
2026-08-07 21:11:17 +00:00
parent ad6fb62160
commit 412ddc8774
2 changed files with 70 additions and 3 deletions
+17 -3
View File
@@ -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<TopUpResult> => {
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;
}
+53
View File
@@ -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 = {