mirror of
https://github.com/Routstr/routstrd.git
synced 2026-08-09 03:44:38 +00:00
Add mint fallback for Cashu topups
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "routstrd",
|
||||
"version": "0.3.10",
|
||||
"version": "0.3.11",
|
||||
"module": "src/index.ts",
|
||||
"type": "module",
|
||||
"private": false,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type CocodState,
|
||||
} from "../wallet/cocod-client";
|
||||
import { decodeCashuTokenAmount } from "../wallet";
|
||||
import { receiveBolt11WithMintFallback } from "../wallet/mint-fallback";
|
||||
import { getClientsFromStore } from "../../utils/clients";
|
||||
import { getUsageSummary } from "./usage-summary";
|
||||
|
||||
@@ -47,6 +48,7 @@ type DaemonDeps = {
|
||||
/** Nostr hex pubkey for routstr review/model events (kind 38425/38423). */
|
||||
routstrPubkey?: string;
|
||||
providerManager: ProviderManager;
|
||||
routeClient: any;
|
||||
refundClient: any;
|
||||
requestResponseLogSink?: RequestResponseLogSink;
|
||||
};
|
||||
@@ -302,6 +304,7 @@ export function createDaemonRequestHandler(deps: {
|
||||
routstrPubkey?: string;
|
||||
usageTrackingDriver: UsageTrackingDriver;
|
||||
providerManager: ProviderManager;
|
||||
routeClient: any;
|
||||
refundClient: any;
|
||||
requestResponseLogSink?: RequestResponseLogSink;
|
||||
}) {
|
||||
@@ -373,8 +376,24 @@ export function createDaemonRequestHandler(deps: {
|
||||
await respond(res, async () => {
|
||||
const body = await readJsonBody(req);
|
||||
const amount = getRequiredPositiveNumberField(body, "amount");
|
||||
const mintUrl = optionalStringField(body, "mintUrl");
|
||||
const invoice = await deps.walletClient.receiveBolt11(amount, mintUrl);
|
||||
const requestedMintUrl = optionalStringField(body, "mintUrl");
|
||||
let invoice: string;
|
||||
let mintUrl: string | undefined = requestedMintUrl;
|
||||
|
||||
if (requestedMintUrl) {
|
||||
invoice = await deps.walletClient.receiveBolt11(amount, requestedMintUrl);
|
||||
} else {
|
||||
const mints = await deps.walletClient.listMints();
|
||||
const result = await receiveBolt11WithMintFallback(
|
||||
deps.walletClient,
|
||||
amount,
|
||||
mints,
|
||||
"[wallet]",
|
||||
);
|
||||
invoice = result.invoice;
|
||||
mintUrl = result.mintUrl;
|
||||
}
|
||||
|
||||
return { output: { invoice, amount, mintUrl } };
|
||||
});
|
||||
return;
|
||||
@@ -1273,6 +1292,7 @@ export function createDaemonRequestHandler(deps: {
|
||||
usageTrackingDriver: deps.usageTrackingDriver,
|
||||
sdkStore: deps.store,
|
||||
providerManager: deps.providerManager,
|
||||
client: deps.routeClient,
|
||||
logger: reqLogger,
|
||||
...(deps.requestResponseLogSink
|
||||
? { requestResponseLogSink: deps.requestResponseLogSink }
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
import { createWalletAdapter } from "./wallet";
|
||||
import type { AutoRefillConfig } from "./wallet/auto-refill";
|
||||
import { createCocodClient } from "./wallet/cocod-client";
|
||||
import { installMintFallbackTopUp } from "./wallet/sdk-mint-fallback";
|
||||
import { createModelService } from "./models";
|
||||
import { createDaemonRequestHandler } from "./http";
|
||||
import { FileRequestResponseLogSink } from "./request-response-log-sink";
|
||||
@@ -129,6 +130,22 @@ async function main(): Promise<void> {
|
||||
nwcConnectionString: config.nwc?.connectionString,
|
||||
});
|
||||
|
||||
const routeClient = new RoutstrClient(
|
||||
walletAdapter,
|
||||
storageAdapter,
|
||||
discoveryAdapter,
|
||||
"min",
|
||||
config.mode || "apikeys",
|
||||
{
|
||||
usageTrackingDriver,
|
||||
sdkStore: store,
|
||||
providerManager,
|
||||
logger: daemonSdkLogger,
|
||||
requestResponseLogSink,
|
||||
},
|
||||
);
|
||||
installMintFallbackTopUp(routeClient, walletClient, walletAdapter, daemonSdkLogger);
|
||||
|
||||
const refundClient = new RoutstrClient(
|
||||
walletAdapter,
|
||||
storageAdapter,
|
||||
@@ -158,6 +175,7 @@ async function main(): Promise<void> {
|
||||
routstrPubkey: config.routstrPubkey,
|
||||
usageTrackingDriver,
|
||||
providerManager,
|
||||
routeClient,
|
||||
refundClient,
|
||||
requestResponseLogSink,
|
||||
}),
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import type { CocodClient } from "./cocod-client";
|
||||
import type { WalletConnect } from "applesauce-wallet-connect";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { receiveBolt11WithMintFallback } from "./mint-fallback";
|
||||
|
||||
export interface AutoRefillConfig {
|
||||
/** Minimum sats balance before triggering a refill */
|
||||
@@ -94,19 +95,24 @@ export function startAutoRefillLoop(
|
||||
`[auto-refill] Balance ${totalBalance} sats < threshold ${config.threshold}. Refilling ${config.amount} sats...`,
|
||||
);
|
||||
|
||||
// Get active mint
|
||||
// Get configured mints. If the active mint is unreachable, fall back to
|
||||
// later configured mints before giving up.
|
||||
const mints = await cocod.listMints();
|
||||
const mintUrl = mints[0];
|
||||
if (!mintUrl) {
|
||||
if (mints.length === 0) {
|
||||
logger.error("[auto-refill] No active mint configured");
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 1: Create a BOLT-11 invoice via cocod to fund the Cashu wallet
|
||||
logger.log(
|
||||
`[auto-refill] Creating BOLT-11 invoice for ${config.amount} sats via ${mintUrl}...`,
|
||||
`[auto-refill] Creating BOLT-11 invoice for ${config.amount} sats via ${mints[0]}...`,
|
||||
);
|
||||
const { invoice, mintUrl } = await receiveBolt11WithMintFallback(
|
||||
cocod,
|
||||
config.amount,
|
||||
mints,
|
||||
"[auto-refill]",
|
||||
);
|
||||
const invoice = await cocod.receiveBolt11(config.amount, mintUrl);
|
||||
|
||||
// Step 2: Pay the invoice via NWC (applesauce)
|
||||
logger.log(`[auto-refill] Paying invoice via NWC...`);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { RelayPool } from "applesauce-relay";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { createCocodClient, type CocodClient } from "./cocod-client";
|
||||
import { startAutoRefillLoop, type AutoRefillConfig } from "./auto-refill";
|
||||
import { receiveBolt11WithMintFallback } from "./mint-fallback";
|
||||
|
||||
export function decodeCashuTokenAmount(token: string): {
|
||||
amount: number;
|
||||
@@ -151,16 +152,33 @@ export async function createWalletAdapter(
|
||||
return { success: false, invoice: "", error: "NWC not connected" };
|
||||
}
|
||||
|
||||
// Ensure we have an active mint
|
||||
// Ensure we have configured mints. If the active mint is unreachable,
|
||||
// invoice creation will fall back to later configured mints.
|
||||
await syncMintState();
|
||||
const mintUrl = activeMintUrl;
|
||||
if (!mintUrl) {
|
||||
const configuredMints = await client.listMints();
|
||||
const mintCandidates = [activeMintUrl, ...configuredMints].filter(
|
||||
(mintUrl): mintUrl is string => typeof mintUrl === "string" && mintUrl.length > 0,
|
||||
);
|
||||
if (mintCandidates.length === 0) {
|
||||
logger.error("[nwc] No active mint configured");
|
||||
return { success: false, invoice: "", error: "No active mint configured" };
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: Check initial balance
|
||||
// Step 1: Create a BOLT-11 invoice via cocod
|
||||
logger.log(`[nwc] Creating ${amount}-sat Lightning invoice via cocod...`);
|
||||
const { invoice, mintUrl } = await receiveBolt11WithMintFallback(
|
||||
client,
|
||||
amount,
|
||||
mintCandidates,
|
||||
"[nwc]",
|
||||
);
|
||||
logger.log(`[nwc] Invoice: ${invoice}`);
|
||||
if (mintUrl !== activeMintUrl) {
|
||||
logger.log(`[nwc] Using fallback mint: ${mintUrl}`);
|
||||
}
|
||||
|
||||
// Step 2: Check initial balance
|
||||
logger.log(`[nwc] Checking initial cocod balance on mint ${mintUrl}...`);
|
||||
let initialBalance: number | null = null;
|
||||
try {
|
||||
@@ -171,11 +189,6 @@ export async function createWalletAdapter(
|
||||
logger.log("[nwc] Could not retrieve initial balance");
|
||||
}
|
||||
|
||||
// Step 2: Create a BOLT-11 invoice via cocod
|
||||
logger.log(`[nwc] Creating ${amount}-sat Lightning invoice via cocod...`);
|
||||
const invoice = await client.receiveBolt11(amount, mintUrl);
|
||||
logger.log(`[nwc] Invoice: ${invoice}`);
|
||||
|
||||
// Step 3: Pay it via NWC
|
||||
logger.log("[nwc] Paying invoice via NWC...");
|
||||
const { preimage, fees_paid } = await wallet.payInvoice(invoice);
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { CocodClient } from "./cocod-client";
|
||||
import { logger } from "../../utils/logger";
|
||||
|
||||
export interface Bolt11InvoiceResult {
|
||||
invoice: string;
|
||||
mintUrl: string;
|
||||
}
|
||||
|
||||
function uniqueMintUrls(mints: Array<string | undefined | null>): string[] {
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
for (const mint of mints) {
|
||||
const trimmed = mint?.trim();
|
||||
if (!trimmed || seen.has(trimmed)) continue;
|
||||
seen.add(trimmed);
|
||||
result.push(trimmed);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function inspectForMintUnreachable(value: unknown): boolean {
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
if (/mint[_-]unreachable/i.test(trimmed) || /mint\b.*\bunreachable/i.test(trimmed)) return true;
|
||||
|
||||
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
||||
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
||||
try {
|
||||
return inspectForMintUnreachable(JSON.parse(trimmed));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!value || typeof value !== "object") return false;
|
||||
|
||||
if (value instanceof Error) {
|
||||
return inspectForMintUnreachable(value.message) ||
|
||||
inspectForMintUnreachable((value as Error & { cause?: unknown }).cause);
|
||||
}
|
||||
|
||||
return Object.values(value).some((entry) => inspectForMintUnreachable(entry));
|
||||
}
|
||||
|
||||
export function isMintUnreachableError(error: unknown): boolean {
|
||||
return inspectForMintUnreachable(error);
|
||||
}
|
||||
|
||||
export async function receiveBolt11WithMintFallback(
|
||||
cocod: CocodClient,
|
||||
amount: number,
|
||||
mintUrls: string[],
|
||||
context: string,
|
||||
): Promise<Bolt11InvoiceResult> {
|
||||
const candidates = uniqueMintUrls(mintUrls);
|
||||
if (candidates.length === 0) {
|
||||
throw new Error("No active mint configured");
|
||||
}
|
||||
|
||||
let lastMintUnreachableError: unknown;
|
||||
for (const [index, mintUrl] of candidates.entries()) {
|
||||
try {
|
||||
if (index > 0) {
|
||||
logger.log(
|
||||
`${context} Retrying top-up with fallback mint ${mintUrl} (${index + 1}/${candidates.length})...`,
|
||||
);
|
||||
}
|
||||
const invoice = await cocod.receiveBolt11(amount, mintUrl);
|
||||
return { invoice, mintUrl };
|
||||
} catch (error) {
|
||||
if (!isMintUnreachableError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
lastMintUnreachableError = error;
|
||||
logger.warn(
|
||||
`${context} Mint unreachable while creating top-up invoice via ${mintUrl}.` +
|
||||
(index + 1 < candidates.length ? " Trying next configured mint." : " No fallback mints left."),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastMintUnreachableError instanceof Error
|
||||
? lastMintUnreachableError
|
||||
: new Error("All configured mints are unreachable");
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import type { CocodClient } from "./cocod-client";
|
||||
import { isMintUnreachableError } from "./mint-fallback";
|
||||
|
||||
type LoggerLike = {
|
||||
log: (...args: unknown[]) => void;
|
||||
warn: (...args: unknown[]) => void;
|
||||
};
|
||||
|
||||
type TopUpOptions = {
|
||||
mintUrl: string;
|
||||
baseUrl: string;
|
||||
amount: number;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
type TopUpResult = {
|
||||
success: boolean;
|
||||
message?: unknown;
|
||||
error?: unknown;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
type BalanceManagerLike = {
|
||||
topUp(options: TopUpOptions): Promise<TopUpResult>;
|
||||
};
|
||||
|
||||
type RoutstrClientLike = {
|
||||
getBalanceManager(): unknown;
|
||||
};
|
||||
|
||||
type WalletAdapterLike = {
|
||||
getBalances(): Promise<Record<string, number>>;
|
||||
};
|
||||
|
||||
const PATCH_MARKER = Symbol.for("routstrd.mintFallbackTopUpPatched");
|
||||
|
||||
function uniqueMintUrls(mints: Array<string | undefined | null>): string[] {
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
for (const mint of mints) {
|
||||
const trimmed = mint?.trim();
|
||||
if (!trimmed || seen.has(trimmed)) continue;
|
||||
seen.add(trimmed);
|
||||
result.push(trimmed);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function isMintUnreachableTopUpResult(result: TopUpResult): boolean {
|
||||
return !result.success &&
|
||||
(isMintUnreachableError(result.message) || isMintUnreachableError(result.error));
|
||||
}
|
||||
|
||||
async function getTopUpMintCandidates(
|
||||
initialMintUrl: string,
|
||||
walletClient: CocodClient,
|
||||
walletAdapter: WalletAdapterLike,
|
||||
): Promise<string[]> {
|
||||
const [configuredMints, balances] = await Promise.all([
|
||||
walletClient.listMints().catch(() => []),
|
||||
walletAdapter.getBalances().catch(() => ({})),
|
||||
]);
|
||||
|
||||
return uniqueMintUrls([
|
||||
initialMintUrl,
|
||||
...configuredMints,
|
||||
...Object.keys(balances),
|
||||
]);
|
||||
}
|
||||
|
||||
export function installMintFallbackTopUp(
|
||||
client: RoutstrClientLike,
|
||||
walletClient: CocodClient,
|
||||
walletAdapter: WalletAdapterLike,
|
||||
logger: LoggerLike,
|
||||
): void {
|
||||
const balanceManager = client.getBalanceManager() as BalanceManagerLike & {
|
||||
[PATCH_MARKER]?: boolean;
|
||||
};
|
||||
if (balanceManager[PATCH_MARKER]) return;
|
||||
|
||||
const originalTopUp = balanceManager.topUp.bind(balanceManager);
|
||||
balanceManager.topUp = async (options: TopUpOptions): Promise<TopUpResult> => {
|
||||
const candidates = await getTopUpMintCandidates(
|
||||
options.mintUrl,
|
||||
walletClient,
|
||||
walletAdapter,
|
||||
);
|
||||
|
||||
let lastMintUnreachableResult: TopUpResult | undefined;
|
||||
for (const [index, mintUrl] of candidates.entries()) {
|
||||
if (index > 0) {
|
||||
logger.log(
|
||||
`[wallet] Retrying provider top-up with fallback mint ${mintUrl} (${index + 1}/${candidates.length})...`,
|
||||
);
|
||||
}
|
||||
|
||||
const result = await originalTopUp({ ...options, mintUrl });
|
||||
if (!isMintUnreachableTopUpResult(result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
lastMintUnreachableResult = result;
|
||||
logger.warn(
|
||||
`[wallet] Provider top-up mint unreachable for ${mintUrl}.` +
|
||||
(index + 1 < candidates.length ? " Trying next configured mint." : " No fallback mints left."),
|
||||
);
|
||||
}
|
||||
|
||||
return lastMintUnreachableResult ?? originalTopUp(options);
|
||||
};
|
||||
|
||||
balanceManager[PATCH_MARKER] = true;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { CocodClient } from "../src/daemon/wallet/cocod-client";
|
||||
import {
|
||||
isMintUnreachableError,
|
||||
receiveBolt11WithMintFallback,
|
||||
} from "../src/daemon/wallet/mint-fallback";
|
||||
|
||||
function createClient(receiveBolt11: CocodClient["receiveBolt11"]): CocodClient {
|
||||
return {
|
||||
ping: async () => true,
|
||||
getStatus: async () => "UNLOCKED",
|
||||
unlock: async () => "ok",
|
||||
getBalances: async () => ({}),
|
||||
receiveCashu: async () => "ok",
|
||||
receiveBolt11,
|
||||
sendCashu: async () => "token",
|
||||
sendBolt11: async () => "ok",
|
||||
listMints: async () => [],
|
||||
addMint: async () => "ok",
|
||||
getMintInfo: async () => ({}),
|
||||
};
|
||||
}
|
||||
|
||||
describe("mint fallback", () => {
|
||||
test("detects mint_unreachable in nested error payloads", () => {
|
||||
expect(isMintUnreachableError(new Error("mint_unreachable"))).toBe(true);
|
||||
expect(
|
||||
isMintUnreachableError(
|
||||
new Error(
|
||||
"The mint that issued this Cashu token is unreachable; the token cannot be redeemed at another mint",
|
||||
),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isMintUnreachableError(
|
||||
JSON.stringify({ detail: { error: { type: "mint_unreachable" } } }),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(isMintUnreachableError(new Error("invoice expired"))).toBe(false);
|
||||
});
|
||||
|
||||
test("tries the next configured mint only for mint_unreachable", async () => {
|
||||
const attempts: string[] = [];
|
||||
const client = createClient(async (_amount, mintUrl) => {
|
||||
attempts.push(mintUrl || "");
|
||||
if (mintUrl === "https://mint-a.example") {
|
||||
throw new Error(
|
||||
JSON.stringify({ detail: { error: { type: "mint_unreachable" } } }),
|
||||
);
|
||||
}
|
||||
return "lnbc1fallback";
|
||||
});
|
||||
|
||||
const result = await receiveBolt11WithMintFallback(
|
||||
client,
|
||||
21,
|
||||
["https://mint-a.example", "https://mint-b.example"],
|
||||
"[test]",
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
invoice: "lnbc1fallback",
|
||||
mintUrl: "https://mint-b.example",
|
||||
});
|
||||
expect(attempts).toEqual(["https://mint-a.example", "https://mint-b.example"]);
|
||||
});
|
||||
|
||||
test("does not fallback for non-mint-unreachable errors", async () => {
|
||||
const attempts: string[] = [];
|
||||
const client = createClient(async (_amount, mintUrl) => {
|
||||
attempts.push(mintUrl || "");
|
||||
throw new Error("insufficient balance");
|
||||
});
|
||||
|
||||
await expect(
|
||||
receiveBolt11WithMintFallback(
|
||||
client,
|
||||
21,
|
||||
["https://mint-a.example", "https://mint-b.example"],
|
||||
"[test]",
|
||||
),
|
||||
).rejects.toThrow("insufficient balance");
|
||||
expect(attempts).toEqual(["https://mint-a.example"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { CocodClient } from "../src/daemon/wallet/cocod-client";
|
||||
import { installMintFallbackTopUp } from "../src/daemon/wallet/sdk-mint-fallback";
|
||||
|
||||
function createCocodClient(mints: string[]): CocodClient {
|
||||
return {
|
||||
ping: async () => true,
|
||||
getStatus: async () => "UNLOCKED",
|
||||
unlock: async () => "ok",
|
||||
getBalances: async () => ({}),
|
||||
receiveCashu: async () => "ok",
|
||||
receiveBolt11: async () => "invoice",
|
||||
sendCashu: async () => "token",
|
||||
sendBolt11: async () => "ok",
|
||||
listMints: async () => mints,
|
||||
addMint: async () => "ok",
|
||||
getMintInfo: async () => ({}),
|
||||
};
|
||||
}
|
||||
|
||||
describe("SDK top-up mint fallback", () => {
|
||||
test("retries provider top-up on mint_unreachable result", async () => {
|
||||
const attempts: string[] = [];
|
||||
const balanceManager = {
|
||||
topUp: async (options: { mintUrl: string }) => {
|
||||
attempts.push(options.mintUrl);
|
||||
if (options.mintUrl === "https://mint-a.example") {
|
||||
return {
|
||||
success: false,
|
||||
message: { detail: { error: { type: "mint_unreachable" } } },
|
||||
};
|
||||
}
|
||||
return { success: true, toppedUpAmount: 21 };
|
||||
},
|
||||
};
|
||||
const client = { getBalanceManager: () => balanceManager };
|
||||
const walletAdapter = {
|
||||
getBalances: async () => ({ "https://mint-c.example": 100 }),
|
||||
};
|
||||
|
||||
installMintFallbackTopUp(
|
||||
client,
|
||||
createCocodClient(["https://mint-a.example", "https://mint-b.example"]),
|
||||
walletAdapter,
|
||||
{ log: () => undefined, warn: () => undefined },
|
||||
);
|
||||
|
||||
const result = await balanceManager.topUp({
|
||||
mintUrl: "https://mint-a.example",
|
||||
baseUrl: "https://provider.example",
|
||||
amount: 21,
|
||||
token: "sk-test",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(attempts).toEqual(["https://mint-a.example", "https://mint-b.example"]);
|
||||
});
|
||||
|
||||
test("does not retry provider top-up on unrelated failure", async () => {
|
||||
const attempts: string[] = [];
|
||||
const balanceManager = {
|
||||
topUp: async (options: { mintUrl: string }) => {
|
||||
attempts.push(options.mintUrl);
|
||||
return { success: false, message: "insufficient balance" };
|
||||
},
|
||||
};
|
||||
const client = { getBalanceManager: () => balanceManager };
|
||||
const walletAdapter = { getBalances: async () => ({}) };
|
||||
|
||||
installMintFallbackTopUp(
|
||||
client,
|
||||
createCocodClient(["https://mint-a.example", "https://mint-b.example"]),
|
||||
walletAdapter,
|
||||
{ log: () => undefined, warn: () => undefined },
|
||||
);
|
||||
|
||||
const result = await balanceManager.topUp({
|
||||
mintUrl: "https://mint-a.example",
|
||||
baseUrl: "https://provider.example",
|
||||
amount: 21,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ success: false, message: "insufficient balance" });
|
||||
expect(attempts).toEqual(["https://mint-a.example"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user