mirror of
https://github.com/Routstr/routstrd.git
synced 2026-10-05 12:28:23 +00:00
fix(wallet): give post-finalize diagnostic read its own budget
The diagnostic ops.mint.get that fetches coco's persisted mint error was bound to remaining() — often ~1ms after a slow mint consumed the per-op budget, so the actionable error silently fell back to the generic message. It is a local DB read, not a mint round-trip, so it now gets its own 250ms bound (still hang-safe). Adds a regression test for the nearly-spent-budget case.
This commit is contained in:
@@ -1355,6 +1355,32 @@ describe("runMintQuoteRecovery", () => {
|
|||||||
expect(result.errors).toEqual([{ operationId: "op-1", error: "original failure" }]);
|
expect(result.errors).toEqual([{ operationId: "op-1", error: "original failure" }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("surfaces the persisted mint error even when the mint budget is nearly spent", async () => {
|
||||||
|
const { source } = fakeSource([mintOp()], {
|
||||||
|
observe: async () => ({ category: "ready" }),
|
||||||
|
finalize: async () => {
|
||||||
|
// Consume almost the whole per-op mint budget before throwing: exactly
|
||||||
|
// the slow-mint case where a diagnostic bound to remaining() would
|
||||||
|
// starve and silently fall back to the generic message.
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||||
|
throw new Error("remains pending");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
source.ops.mint.get = async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
return {
|
||||||
|
...mintOp(),
|
||||||
|
state: "pending",
|
||||||
|
error: "keyset id inactive.",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const result = await runMintQuoteRecovery(source, { timeoutMs: 30 });
|
||||||
|
expect(result).toMatchObject({ retryable: 1, recovered: 0 });
|
||||||
|
expect(result.errors).toEqual([
|
||||||
|
{ operationId: "op-1", error: "keyset id inactive." },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("bounds finalize so one hung mint cannot block recovery", async () => {
|
it("bounds finalize so one hung mint cannot block recovery", async () => {
|
||||||
const { source } = fakeSource([mintOp()], {
|
const { source } = fakeSource([mintOp()], {
|
||||||
observe: async () => ({ category: "ready" }),
|
observe: async () => ({ category: "ready" }),
|
||||||
|
|||||||
@@ -1002,6 +1002,14 @@ export function createRunQueue(): <T>(run: () => Promise<T>) => Promise<T> {
|
|||||||
/** Per-quote budget for the mint round-trip during explicit recovery. */
|
/** Per-quote budget for the mint round-trip during explicit recovery. */
|
||||||
const MINT_QUOTE_RECOVERY_TIMEOUT_MS = 20_000;
|
const MINT_QUOTE_RECOVERY_TIMEOUT_MS = 20_000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bound for the local post-finalize diagnostic read. This is a database
|
||||||
|
* lookup, not a mint round-trip, so it gets its own small budget: the per-op
|
||||||
|
* mint budget is often already spent when finalize throws, and a starved
|
||||||
|
* diagnostic would silently fall back to the generic error message.
|
||||||
|
*/
|
||||||
|
const DIAGNOSTIC_LOOKUP_TIMEOUT_MS = 250;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recover mint quotes whose sats are PAID at the mint but were never claimed.
|
* Recover mint quotes whose sats are PAID at the mint but were never claimed.
|
||||||
*
|
*
|
||||||
@@ -1142,7 +1150,8 @@ export async function runMintQuoteRecovery(
|
|||||||
}
|
}
|
||||||
result.checked++;
|
result.checked++;
|
||||||
// One budget per operation, shared by the mint check and the finalize, so
|
// One budget per operation, shared by the mint check and the finalize, so
|
||||||
// a slow mint cannot silently double the documented per-quote wait.
|
// a slow mint cannot silently double the documented per-quote wait. The
|
||||||
|
// local post-finalize diagnostic read is exempt (DIAGNOSTIC_LOOKUP_TIMEOUT_MS).
|
||||||
const deadlineAt = Date.now() + timeoutMs;
|
const deadlineAt = Date.now() + timeoutMs;
|
||||||
const remaining = () => Math.max(1, deadlineAt - Date.now());
|
const remaining = () => Math.max(1, deadlineAt - Date.now());
|
||||||
|
|
||||||
@@ -1225,7 +1234,7 @@ export async function runMintQuoteRecovery(
|
|||||||
// persisted the actionable mint rejection (for example inactive keyset).
|
// persisted the actionable mint rejection (for example inactive keyset).
|
||||||
const current = await withTimeout(
|
const current = await withTimeout(
|
||||||
source.ops.mint.get(operationId),
|
source.ops.mint.get(operationId),
|
||||||
remaining(),
|
Math.max(remaining(), DIAGNOSTIC_LOOKUP_TIMEOUT_MS),
|
||||||
).catch(() => null);
|
).catch(() => null);
|
||||||
const detail = current?.state === "pending" && current.error
|
const detail = current?.state === "pending" && current.error
|
||||||
? current.error
|
? current.error
|
||||||
|
|||||||
Reference in New Issue
Block a user