routstrd 0.4.10 → 0.4.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -15
- package/SECURITY.md +0 -1
- package/SKILL.md +267 -52
- package/dist/daemon/index.js +93747 -70861
- package/dist/index.js +65958 -42561
- package/package.json +3 -2
- package/src/cli.ts +358 -23
- package/src/daemon/http/index.ts +323 -38
- package/src/daemon/http/request-body.ts +67 -0
- package/src/daemon/http/request-path.ts +39 -0
- package/src/daemon/index.ts +1 -1
- package/src/daemon/models.ts +46 -11
- package/src/daemon/wallet/auto-refill.ts +20 -8
- package/src/daemon/wallet/cleanup.ts +16 -2
- package/src/daemon/wallet/coco-client.ts +941 -115
- package/src/daemon/wallet/index.ts +168 -50
- package/src/daemon/wallet/mint-quote-recovery.ts +119 -0
- package/src/daemon/wallet/recovery-probe.ts +312 -0
- package/src/daemon/wallet/recovery-work.ts +45 -0
- package/src/daemon/wallet/testing/fake-mint.ts +347 -0
- package/src/daemon/wallet/trusted-mints.ts +87 -0
- package/src/daemon/wallet/wallet-client.ts +206 -0
- package/src/integrations/pi.ts +189 -34
- package/src/integrations/registry.ts +14 -0
- package/src/tui/usage/render.ts +161 -22
- package/src/utils/config.ts +3 -2
- package/src/utils/cooldowns.ts +134 -0
- package/src/utils/daemon-client.ts +53 -9
- package/src/utils/history.ts +9 -0
- package/src/utils/with-timeout.ts +21 -0
- package/src/TUI refactor.md +0 -113
- package/src/daemon/wallet/cocod-client.ts +0 -505
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { getTokenMetadata } from "@cashu/cashu-ts";
|
|
2
2
|
import { InsufficientBalanceError } from "@routstr/sdk";
|
|
3
3
|
import { WalletConnect } from "applesauce-wallet-connect";
|
|
4
|
+
import { WalletBaseError } from "applesauce-wallet-connect/helpers/error";
|
|
4
5
|
import { RelayPool } from "applesauce-relay";
|
|
5
6
|
import { logger } from "../../utils/logger";
|
|
6
|
-
import {
|
|
7
|
+
import { withTimeout } from "../../utils/with-timeout";
|
|
8
|
+
import type { WalletClient } from "./wallet-client";
|
|
7
9
|
import { startAutoRefillLoop, type AutoRefillConfig } from "./auto-refill";
|
|
8
10
|
|
|
11
|
+
/**
|
|
12
|
+
* NWC reads (get_info/get_balance) should answer in a couple of seconds. If
|
|
13
|
+
* they don't, the long-lived relay subscription is presumed stale and the
|
|
14
|
+
* connection is rebuilt before one retry.
|
|
15
|
+
*/
|
|
16
|
+
const NWC_READ_TIMEOUT_MS = 15_000;
|
|
17
|
+
/** Overall bound including encryption negotiation; replies have a library 30s timeout. */
|
|
18
|
+
const NWC_PAY_TIMEOUT_MS = 45_000;
|
|
19
|
+
|
|
20
|
+
type NwcPayment = { preimage?: string; fees_paid?: number };
|
|
21
|
+
|
|
9
22
|
export function decodeCashuTokenAmount(token: string): {
|
|
10
23
|
amount: number;
|
|
11
24
|
unit: "sat" | "msat";
|
|
@@ -21,7 +34,7 @@ export function decodeCashuTokenAmount(token: string): {
|
|
|
21
34
|
}
|
|
22
35
|
|
|
23
36
|
export async function receiveCashuToken(
|
|
24
|
-
client: Pick<
|
|
37
|
+
client: Pick<WalletClient, "receiveCashu">,
|
|
25
38
|
token: string,
|
|
26
39
|
): Promise<{ message: string; amount: number; unit: "sat" | "msat" }> {
|
|
27
40
|
// Validate the token before handing it to a state-changing wallet call. This
|
|
@@ -33,10 +46,14 @@ export async function receiveCashuToken(
|
|
|
33
46
|
}
|
|
34
47
|
|
|
35
48
|
export interface WalletAdapterOptions {
|
|
36
|
-
|
|
37
|
-
walletClient
|
|
49
|
+
/** The in-process wallet engine. Required — construct it with `createCocoClient()`. */
|
|
50
|
+
walletClient: WalletClient;
|
|
38
51
|
/** NWC connection string for Lightning funding (uses applesauce-wallet-connect) */
|
|
39
52
|
nwcConnectionString?: string;
|
|
53
|
+
/** Override the NWC read timeout in milliseconds (test hook). */
|
|
54
|
+
nwcReadTimeoutMs?: number;
|
|
55
|
+
/** Override the NWC payment timeout in milliseconds (test hook). */
|
|
56
|
+
nwcPayTimeoutMs?: number;
|
|
40
57
|
/** Auto-refill configuration (static, for startup only) */
|
|
41
58
|
autoRefill?: AutoRefillConfig;
|
|
42
59
|
/**
|
|
@@ -48,10 +65,9 @@ export interface WalletAdapterOptions {
|
|
|
48
65
|
}
|
|
49
66
|
|
|
50
67
|
export async function createWalletAdapter(
|
|
51
|
-
options: WalletAdapterOptions
|
|
68
|
+
options: WalletAdapterOptions,
|
|
52
69
|
) {
|
|
53
|
-
const client =
|
|
54
|
-
options.walletClient || createCocodClient({ cocodPath: options.cocodPath });
|
|
70
|
+
const client = options.walletClient;
|
|
55
71
|
let activeMintUrl: string | null = null;
|
|
56
72
|
let mintUnits: Record<string, "sat" | "msat"> = {};
|
|
57
73
|
|
|
@@ -82,19 +98,44 @@ export async function createWalletAdapter(
|
|
|
82
98
|
|
|
83
99
|
let wallet: WalletConnect | undefined;
|
|
84
100
|
let pool: RelayPool | undefined;
|
|
101
|
+
let nwcConnectionString = options.nwcConnectionString;
|
|
102
|
+
const nwcReadTimeoutMs = options.nwcReadTimeoutMs ?? NWC_READ_TIMEOUT_MS;
|
|
103
|
+
const nwcPayTimeoutMs = options.nwcPayTimeoutMs ?? NWC_PAY_TIMEOUT_MS;
|
|
85
104
|
|
|
86
105
|
// Getter for the current wallet instance (used by auto-refill loop)
|
|
87
106
|
const getWallet = (): WalletConnect | undefined => wallet;
|
|
88
107
|
|
|
89
|
-
if
|
|
90
|
-
|
|
91
|
-
|
|
108
|
+
/** Close the active relay pool, if any. */
|
|
109
|
+
function closeNwcPool(): void {
|
|
110
|
+
if (pool) {
|
|
111
|
+
for (const [url] of pool.relays) {
|
|
112
|
+
pool.remove(url, true);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
pool = undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* (Re)create the relay pool + WalletConnect client for a connection string.
|
|
120
|
+
* Shared by interactive connects and self-healing recovery so both paths use
|
|
121
|
+
* identical setup.
|
|
122
|
+
*/
|
|
123
|
+
function connectNwc(connectionString: string, reason: string): void {
|
|
124
|
+
const nextPool = new RelayPool();
|
|
125
|
+
const nextWallet = WalletConnect.fromConnectURI(connectionString, {
|
|
126
|
+
pool: nextPool,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
pool = nextPool;
|
|
130
|
+
wallet = nextWallet;
|
|
131
|
+
nwcConnectionString = connectionString;
|
|
92
132
|
|
|
93
133
|
// Connect in background (non-blocking)
|
|
94
|
-
|
|
134
|
+
nextWallet
|
|
135
|
+
.waitForService()
|
|
95
136
|
.then(() => {
|
|
96
137
|
logger.log(
|
|
97
|
-
`[nwc] NWC wallet
|
|
138
|
+
`[nwc] NWC wallet ${reason}. Relay: ${nextWallet.relays[0]}, Service: ${nextWallet.service}`,
|
|
98
139
|
);
|
|
99
140
|
})
|
|
100
141
|
.catch((err) => {
|
|
@@ -102,39 +143,97 @@ export async function createWalletAdapter(
|
|
|
102
143
|
});
|
|
103
144
|
}
|
|
104
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Rebuild the NWC connection in place after a request stalled. applesauce's
|
|
148
|
+
* request timeout only covers the response stream, so a stale relay
|
|
149
|
+
* subscription can leave `getInfo`/`getBalance` pending forever while it
|
|
150
|
+
* negotiates encryption. Recreating the pool gives the next call a fresh
|
|
151
|
+
* subscription.
|
|
152
|
+
*/
|
|
153
|
+
function rebuildNwcConnection(reason: string): void {
|
|
154
|
+
if (!nwcConnectionString) return;
|
|
155
|
+
closeNwcPool();
|
|
156
|
+
wallet = undefined;
|
|
157
|
+
connectNwc(nwcConnectionString, reason);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Run an idempotent NWC read, bounding the wait and rebuilding the connection
|
|
162
|
+
* once if it stalls.
|
|
163
|
+
*/
|
|
164
|
+
async function nwcRead<T>(
|
|
165
|
+
label: string,
|
|
166
|
+
operation: (w: WalletConnect) => Promise<T>,
|
|
167
|
+
): Promise<T> {
|
|
168
|
+
const first = wallet;
|
|
169
|
+
if (!first?.service) {
|
|
170
|
+
throw new Error("NWC not connected");
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
return await withTimeout(
|
|
174
|
+
operation(first),
|
|
175
|
+
nwcReadTimeoutMs,
|
|
176
|
+
`${label} timed out`,
|
|
177
|
+
);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
// A normal NIP-47 error proves the wallet answered. Keep other calls alive.
|
|
180
|
+
if (error instanceof WalletBaseError || !nwcConnectionString) throw error;
|
|
181
|
+
logger.warn(
|
|
182
|
+
`[nwc] ${label} failed (${(error as Error).message}); rebuilding NWC connection and retrying`,
|
|
183
|
+
);
|
|
184
|
+
rebuildNwcConnection("reconnected after stall");
|
|
185
|
+
const retry = wallet;
|
|
186
|
+
if (!retry?.service) throw error;
|
|
187
|
+
return await withTimeout(
|
|
188
|
+
operation(retry),
|
|
189
|
+
nwcReadTimeoutMs,
|
|
190
|
+
`${label} timed out after reconnect`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Pay a BOLT-11 invoice over NWC with a bounded wait. A timeout rebuilds the
|
|
197
|
+
* relay connection so later calls recover without a daemon restart. The
|
|
198
|
+
* payment is not retried here. A timeout is an unknown payment outcome,
|
|
199
|
+
* not proof of failure; callers must reconcile before trying a fresh invoice.
|
|
200
|
+
*/
|
|
201
|
+
async function payNwcInvoice(invoice: string): Promise<NwcPayment> {
|
|
202
|
+
const payer = wallet;
|
|
203
|
+
if (!payer?.service) throw new Error("NWC not connected");
|
|
204
|
+
try {
|
|
205
|
+
return await withTimeout(
|
|
206
|
+
payer.payInvoice(invoice),
|
|
207
|
+
nwcPayTimeoutMs,
|
|
208
|
+
"NWC payment timed out",
|
|
209
|
+
);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
// Include the library's own timeout, but not normal wallet error replies.
|
|
212
|
+
if (!(error instanceof WalletBaseError)) {
|
|
213
|
+
rebuildNwcConnection("reconnected after payment stall");
|
|
214
|
+
}
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (options.nwcConnectionString) {
|
|
220
|
+
connectNwc(options.nwcConnectionString, "connected");
|
|
221
|
+
}
|
|
222
|
+
|
|
105
223
|
const walletAdapter = {
|
|
106
224
|
async reconnect(connectionString?: string): Promise<void> {
|
|
107
225
|
logger.log(
|
|
108
226
|
`[nwc] Reconnecting NWC wallet... ${connectionString ? "new connection string provided" : "disconnecting"}`,
|
|
109
227
|
);
|
|
110
228
|
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
for (const [url] of pool.relays) {
|
|
114
|
-
pool.remove(url, true);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// 2. Update wallet reference
|
|
229
|
+
// Close existing relay pool connections and update the wallet reference
|
|
230
|
+
closeNwcPool();
|
|
119
231
|
wallet = undefined;
|
|
120
|
-
pool = undefined;
|
|
121
232
|
|
|
122
|
-
// 3. Create new wallet if connection string provided
|
|
123
233
|
if (connectionString) {
|
|
124
|
-
|
|
125
|
-
wallet = WalletConnect.fromConnectURI(connectionString, { pool });
|
|
126
|
-
|
|
127
|
-
// Connect in background (non-blocking)
|
|
128
|
-
wallet.waitForService()
|
|
129
|
-
.then(() => {
|
|
130
|
-
logger.log(
|
|
131
|
-
`[nwc] NWC wallet reconnected. Relay: ${wallet!.relays[0]}, Service: ${wallet!.service}`,
|
|
132
|
-
);
|
|
133
|
-
})
|
|
134
|
-
.catch((err) => {
|
|
135
|
-
logger.error(`[nwc] NWC reconnection failed: ${err.message}`);
|
|
136
|
-
});
|
|
234
|
+
connectNwc(connectionString, "reconnected");
|
|
137
235
|
} else {
|
|
236
|
+
nwcConnectionString = undefined;
|
|
138
237
|
logger.log("[nwc] NWC wallet disconnected.");
|
|
139
238
|
}
|
|
140
239
|
},
|
|
@@ -192,9 +291,9 @@ export async function createWalletAdapter(
|
|
|
192
291
|
const { invoice } = await client.receiveBolt11(amount, mintUrl);
|
|
193
292
|
logger.log(`[nwc] Invoice: ${invoice}`);
|
|
194
293
|
|
|
195
|
-
// Step 3: Pay it via NWC
|
|
294
|
+
// Step 3: Pay it via NWC (bounded — a stale relay must not hang the CLI)
|
|
196
295
|
logger.log("[nwc] Paying invoice via NWC...");
|
|
197
|
-
const { preimage, fees_paid } = await
|
|
296
|
+
const { preimage, fees_paid } = await payNwcInvoice(invoice);
|
|
198
297
|
logger.log(`[nwc] ✅ Payment successful!`);
|
|
199
298
|
logger.log(`[nwc] Preimage: ${preimage}`);
|
|
200
299
|
if (fees_paid !== undefined) {
|
|
@@ -244,10 +343,10 @@ export async function createWalletAdapter(
|
|
|
244
343
|
}
|
|
245
344
|
|
|
246
345
|
try {
|
|
247
|
-
const info = await
|
|
346
|
+
const info = await nwcRead("get_info", (w) => w.getInfo());
|
|
248
347
|
let balance: number | undefined;
|
|
249
348
|
try {
|
|
250
|
-
const bal = await
|
|
349
|
+
const bal = await nwcRead("get_balance", (w) => w.getBalance());
|
|
251
350
|
balance = Math.floor(bal.balance / 1000); // msats → sats
|
|
252
351
|
} catch {
|
|
253
352
|
// Balance might not be available
|
|
@@ -326,21 +425,40 @@ export async function createWalletAdapter(
|
|
|
326
425
|
|
|
327
426
|
let stopAutoRefill: (() => void) | undefined;
|
|
328
427
|
|
|
428
|
+
/**
|
|
429
|
+
* Start the auto-refill loop if it is not already running. Safe to call more
|
|
430
|
+
* than once; it becomes a no-op after the first call.
|
|
431
|
+
*/
|
|
432
|
+
function ensureAutoRefillLoop(): void {
|
|
433
|
+
if (stopAutoRefill) return;
|
|
434
|
+
const getConfig = options.getAutoRefillConfig ?? (() => options.autoRefill);
|
|
435
|
+
stopAutoRefill = startAutoRefillLoop(
|
|
436
|
+
client,
|
|
437
|
+
getWallet,
|
|
438
|
+
getConfig,
|
|
439
|
+
5000,
|
|
440
|
+
payNwcInvoice,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
329
444
|
const autoRefillConfig = options.getAutoRefillConfig
|
|
330
445
|
? options.getAutoRefillConfig()
|
|
331
446
|
: options.autoRefill;
|
|
332
447
|
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
448
|
+
if (options.getAutoRefillConfig || options.autoRefill) {
|
|
449
|
+
// Start the loop even when no wallet is connected yet: it reads the wallet
|
|
450
|
+
// and config fresh each cycle, so a later `nwc connect` activates refills
|
|
451
|
+
// without a daemon restart.
|
|
452
|
+
ensureAutoRefillLoop();
|
|
453
|
+
if (autoRefillConfig) {
|
|
454
|
+
logger.log(
|
|
455
|
+
`[wallet] Auto-refill enabled: threshold=${autoRefillConfig.threshold} sats, amount=${autoRefillConfig.amount} sats, cooldown=${autoRefillConfig.cooldownMs / 60000} minutes`,
|
|
456
|
+
);
|
|
457
|
+
} else {
|
|
458
|
+
logger.log(
|
|
459
|
+
"[wallet] Auto-refill loop started (currently disabled — enable via CLI to activate)",
|
|
460
|
+
);
|
|
461
|
+
}
|
|
344
462
|
}
|
|
345
463
|
|
|
346
464
|
try {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for PAID mint-quote recovery.
|
|
3
|
+
*
|
|
4
|
+
* A mint quote can be PAID at the mint while its local operation is still
|
|
5
|
+
* `pending` (the Lightning payment landed before expiry while the daemon was
|
|
6
|
+
* down, so no local observation was ever recorded) or even terminally
|
|
7
|
+
* `failed` (coco gives up when the mint refuses to sign, for example after the
|
|
8
|
+
* invoice expiry). Claimability still depends on the mint accepting issuance.
|
|
9
|
+
* This feature retries the stored outputs or restores their signatures; it
|
|
10
|
+
* does not regenerate outputs rejected by the mint (for example an inactive
|
|
11
|
+
* keyset). coco already reconciles pending paid quotes at startup and in the
|
|
12
|
+
* periodic sweep. The new capability is operator-targeted recovery, including
|
|
13
|
+
* explicitly reopening failed operations, alongside safer cleanup.
|
|
14
|
+
*
|
|
15
|
+
* Recovery asks the mint what it thinks, then retries issuance or restore. These helpers decide *what* to do from a remote observation; the
|
|
16
|
+
* actual state transitions are applied by the in-process coco wallet client
|
|
17
|
+
* so coco-core's operation services emit their normal events and release
|
|
18
|
+
* proof reservations. Keeping the decisions here makes them unit testable
|
|
19
|
+
* without a wallet database or network access.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Subset of coco's mint operation rows that recovery needs. */
|
|
23
|
+
export interface MintQuoteRecoveryCandidate {
|
|
24
|
+
id: string;
|
|
25
|
+
mintUrl: string;
|
|
26
|
+
quoteId?: string;
|
|
27
|
+
state: string;
|
|
28
|
+
/** Quote amount in sats. */
|
|
29
|
+
amount: number;
|
|
30
|
+
/** Quote expiry in epoch seconds. `0` means unknown/not applicable. */
|
|
31
|
+
expiry: number;
|
|
32
|
+
/** Last quote state observed from the mint (UNPAID, PAID, ISSUED). */
|
|
33
|
+
lastObservedRemoteState?: string;
|
|
34
|
+
error?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Coco's classification of a fresh remote quote check. */
|
|
38
|
+
export type PendingMintCheckCategory =
|
|
39
|
+
| "waiting"
|
|
40
|
+
| "ready"
|
|
41
|
+
| "completed"
|
|
42
|
+
| "terminal";
|
|
43
|
+
|
|
44
|
+
/** What recovery should do with a quote after checking it with the mint. */
|
|
45
|
+
export type MintQuoteRecoveryDecision =
|
|
46
|
+
| { action: "finalize"; observedRemoteState: "PAID" | "ISSUED" }
|
|
47
|
+
| { action: "waiting" }
|
|
48
|
+
| { action: "terminal" };
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Map a remote quote check onto a recovery action.
|
|
52
|
+
*
|
|
53
|
+
* - `ready` means the mint reports the quote PAID but never issued: submit the
|
|
54
|
+
* operation's stored outputs to claim the sats.
|
|
55
|
+
* - `completed` means the mint already issued it: recover the signatures
|
|
56
|
+
* (NUT-09) instead of minting again.
|
|
57
|
+
* - `waiting` means the mint still reports the quote UNPAID: nothing is
|
|
58
|
+
* claimable, so leave the operation alone.
|
|
59
|
+
* - `terminal` means the quote can no longer be issued (for example the mint
|
|
60
|
+
* refused an expired quote). coco persists that verdict as a failed
|
|
61
|
+
* operation, so recovery must report it rather than treat it as progress.
|
|
62
|
+
*/
|
|
63
|
+
export function classifyMintQuoteObservation(
|
|
64
|
+
category: PendingMintCheckCategory,
|
|
65
|
+
): MintQuoteRecoveryDecision {
|
|
66
|
+
switch (category) {
|
|
67
|
+
case "ready":
|
|
68
|
+
return { action: "finalize", observedRemoteState: "PAID" };
|
|
69
|
+
case "completed":
|
|
70
|
+
return { action: "finalize", observedRemoteState: "ISSUED" };
|
|
71
|
+
case "waiting":
|
|
72
|
+
return { action: "waiting" };
|
|
73
|
+
case "terminal":
|
|
74
|
+
return { action: "terminal" };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface MintQuoteRecoverySelectionOptions<
|
|
79
|
+
T extends MintQuoteRecoveryCandidate,
|
|
80
|
+
> {
|
|
81
|
+
mints: T[];
|
|
82
|
+
/**
|
|
83
|
+
* Also consider terminally failed operations. Off by default: re-opening a
|
|
84
|
+
* failed operation is a mutation, so only an explicit user-invoked recovery
|
|
85
|
+
* may do it. Startup recovery must never resurrect quotes on its own.
|
|
86
|
+
*/
|
|
87
|
+
includeFailed?: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface MintQuoteRecoverySelection<
|
|
91
|
+
T extends MintQuoteRecoveryCandidate,
|
|
92
|
+
> {
|
|
93
|
+
/** Pending (or executing) operations that need a fresh mint observation. */
|
|
94
|
+
pending: T[];
|
|
95
|
+
/** Failed operations the caller may re-open and retry. */
|
|
96
|
+
failed: T[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Split operations into those recovery should check and those that were
|
|
101
|
+
* already given up on.
|
|
102
|
+
*
|
|
103
|
+
* Every `pending` operation is selected: only the mint knows whether an
|
|
104
|
+
* expired quote was paid before the local invoice ran out. `executing`
|
|
105
|
+
* operations are recovered too, since a crash mid-mint leaves outputs that
|
|
106
|
+
* may already be signed.
|
|
107
|
+
*/
|
|
108
|
+
export function selectMintQuotesForRecovery<
|
|
109
|
+
T extends MintQuoteRecoveryCandidate,
|
|
110
|
+
>(options: MintQuoteRecoverySelectionOptions<T>): MintQuoteRecoverySelection<T> {
|
|
111
|
+
const { mints, includeFailed = false } = options;
|
|
112
|
+
const pending = mints.filter(
|
|
113
|
+
(op) => op.state === "pending" || op.state === "executing",
|
|
114
|
+
);
|
|
115
|
+
const failed = includeFailed
|
|
116
|
+
? mints.filter((op) => op.state === "failed")
|
|
117
|
+
: [];
|
|
118
|
+
return { pending, failed };
|
|
119
|
+
}
|