routstrd 0.4.11 → 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 +22 -1
- package/SKILL.md +29 -4
- package/dist/daemon/index.js +93713 -72282
- package/dist/index.js +65080 -43216
- package/package.json +2 -2
- package/src/cli.ts +358 -23
- package/src/daemon/http/index.ts +318 -27
- 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 +921 -110
- 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 +3 -3
- package/src/daemon/wallet/wallet-client.ts +206 -0
- package/src/integrations/pi.ts +53 -3
- 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/daemon/wallet/cocod-client.ts +0 -505
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "routstrd",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.12",
|
|
4
4
|
"module": "src/index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@cashu/cashu-ts": "^4.3.0",
|
|
40
40
|
"@cashu/coco-core": "^1.0.1",
|
|
41
41
|
"@cashu/coco-sqlite-bun": "^1.0.1",
|
|
42
|
-
"@routstr/sdk": "^0.4.
|
|
42
|
+
"@routstr/sdk": "^0.4.8",
|
|
43
43
|
"@scure/bip39": "^2.2.0",
|
|
44
44
|
"applesauce-core": "^5.1.0",
|
|
45
45
|
"applesauce-relay": "^5.1.0",
|
package/src/cli.ts
CHANGED
|
@@ -10,7 +10,12 @@ import {
|
|
|
10
10
|
loadConfig,
|
|
11
11
|
getDaemonBaseUrl,
|
|
12
12
|
getUserNpub,
|
|
13
|
+
type CommandResponse,
|
|
13
14
|
} from "./utils/daemon-client";
|
|
15
|
+
import {
|
|
16
|
+
formatCooldowns,
|
|
17
|
+
type CooldownsOutput,
|
|
18
|
+
} from "./utils/cooldowns";
|
|
14
19
|
import { waitForDaemonToExit } from "./utils/daemon-stop";
|
|
15
20
|
import {
|
|
16
21
|
listClientsAction,
|
|
@@ -53,6 +58,10 @@ import {
|
|
|
53
58
|
} from "./daemon/wallet/paths";
|
|
54
59
|
import * as QRCode from "qrcode";
|
|
55
60
|
import { normalizeNostrPubkey, npubFromPubkey, npubFromSecretKey } from "./utils/nip98";
|
|
61
|
+
import {
|
|
62
|
+
HISTORY_ENTRY_TYPES,
|
|
63
|
+
isHistoryEntryType,
|
|
64
|
+
} from "./utils/history";
|
|
56
65
|
import { generateSecretKey, nip19 } from "nostr-tools";
|
|
57
66
|
import { generateMnemonic } from "@scure/bip39";
|
|
58
67
|
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
@@ -146,6 +155,36 @@ async function waitForMintQuote(operationId: string): Promise<void> {
|
|
|
146
155
|
}
|
|
147
156
|
}
|
|
148
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Warn that value-moving wallet operations will block until startup recovery
|
|
160
|
+
* finishes. Reads stay available, but the daemon gates send/receive behind the
|
|
161
|
+
* recovery gate, so without this notice the CLI would appear to hang silently
|
|
162
|
+
* after a restart.
|
|
163
|
+
*/
|
|
164
|
+
async function warnIfWalletRecovering(): Promise<void> {
|
|
165
|
+
try {
|
|
166
|
+
await ensureDaemonRunning();
|
|
167
|
+
const result = await callDaemon("/wallet/status");
|
|
168
|
+
const output = result.output as
|
|
169
|
+
| { state?: string; recovery?: { phase?: string } }
|
|
170
|
+
| undefined;
|
|
171
|
+
if (output?.state !== "RECOVERING") return;
|
|
172
|
+
|
|
173
|
+
const phase = output.recovery?.phase;
|
|
174
|
+
console.error(
|
|
175
|
+
"Wallet is recovering from a previous run (this only happens after a restart).",
|
|
176
|
+
);
|
|
177
|
+
if (phase && phase !== "done") {
|
|
178
|
+
console.error(`Recovery phase: ${phase}.`);
|
|
179
|
+
}
|
|
180
|
+
console.error(
|
|
181
|
+
"Please wait — send and receive resume as soon as recovery completes.",
|
|
182
|
+
);
|
|
183
|
+
} catch {
|
|
184
|
+
// Status is best-effort; never block the real command on it.
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
149
188
|
export function initializeWallet(walletDir = defaultWalletDir()): void {
|
|
150
189
|
const walletConfig = join(walletDir, "config.json");
|
|
151
190
|
|
|
@@ -296,10 +335,7 @@ async function initDaemon(integrationKey?: IntegrationKey): Promise<void> {
|
|
|
296
335
|
|
|
297
336
|
// Create initial config (0600, atomic write)
|
|
298
337
|
if (!existsSync(CONFIG_FILE)) {
|
|
299
|
-
const config: RoutstrdConfig = {
|
|
300
|
-
...DEFAULT_CONFIG,
|
|
301
|
-
cocodPath: null,
|
|
302
|
-
};
|
|
338
|
+
const config: RoutstrdConfig = { ...DEFAULT_CONFIG };
|
|
303
339
|
saveDaemonConfig(config);
|
|
304
340
|
console.log(`Created config file: ${CONFIG_FILE}`);
|
|
305
341
|
}
|
|
@@ -1132,6 +1168,24 @@ const providersCmd = program
|
|
|
1132
1168
|
.command("providers")
|
|
1133
1169
|
.description("List and manage providers");
|
|
1134
1170
|
|
|
1171
|
+
// Strict index parsing: parseInt accepts garbage like "3abc" (-> 3) or
|
|
1172
|
+
// "0x5" (-> 0), which would silently act on the wrong provider.
|
|
1173
|
+
const parseProviderIndices = (raw: string[]): number[] => {
|
|
1174
|
+
const valid: number[] = [];
|
|
1175
|
+
const invalid: string[] = [];
|
|
1176
|
+
for (const s of raw) {
|
|
1177
|
+
if (/^\d+$/.test(s)) {
|
|
1178
|
+
valid.push(parseInt(s, 10));
|
|
1179
|
+
} else {
|
|
1180
|
+
invalid.push(s);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
if (invalid.length > 0) {
|
|
1184
|
+
console.log(`Ignoring invalid index argument(s): ${invalid.join(", ")}`);
|
|
1185
|
+
}
|
|
1186
|
+
return valid;
|
|
1187
|
+
};
|
|
1188
|
+
|
|
1135
1189
|
providersCmd
|
|
1136
1190
|
.command("list")
|
|
1137
1191
|
.description("List all providers with their enabled/disabled status")
|
|
@@ -1152,6 +1206,8 @@ providersCmd
|
|
|
1152
1206
|
index: number;
|
|
1153
1207
|
baseUrl: string;
|
|
1154
1208
|
disabled: boolean;
|
|
1209
|
+
manuallyDisabled?: boolean;
|
|
1210
|
+
manuallyEnabled?: boolean;
|
|
1155
1211
|
}>;
|
|
1156
1212
|
disabledCount: number;
|
|
1157
1213
|
totalCount: number;
|
|
@@ -1167,7 +1223,13 @@ providersCmd
|
|
|
1167
1223
|
`Providers (${output.totalCount} total, ${output.disabledCount} disabled):\n`,
|
|
1168
1224
|
);
|
|
1169
1225
|
for (const provider of output.providers) {
|
|
1170
|
-
const status = provider.
|
|
1226
|
+
const status = provider.manuallyDisabled
|
|
1227
|
+
? "DISABLED (manual) "
|
|
1228
|
+
: provider.disabled
|
|
1229
|
+
? "DISABLED (nostr review) "
|
|
1230
|
+
: provider.manuallyEnabled
|
|
1231
|
+
? "enabled (manual override)"
|
|
1232
|
+
: "enabled ";
|
|
1171
1233
|
console.log(` [${provider.index}] ${status} ${provider.baseUrl}`);
|
|
1172
1234
|
}
|
|
1173
1235
|
});
|
|
@@ -1180,9 +1242,7 @@ providersCmd
|
|
|
1180
1242
|
.action(async (indices: string[]) => {
|
|
1181
1243
|
await ensureDaemonRunning();
|
|
1182
1244
|
|
|
1183
|
-
const indexNums = indices
|
|
1184
|
-
.map((s) => parseInt(s, 10))
|
|
1185
|
-
.filter((n) => Number.isFinite(n));
|
|
1245
|
+
const indexNums = parseProviderIndices(indices);
|
|
1186
1246
|
if (indexNums.length === 0) {
|
|
1187
1247
|
console.log("No valid indices provided.");
|
|
1188
1248
|
process.exit(1);
|
|
@@ -1217,9 +1277,7 @@ providersCmd
|
|
|
1217
1277
|
.action(async (indices: string[]) => {
|
|
1218
1278
|
await ensureDaemonRunning();
|
|
1219
1279
|
|
|
1220
|
-
const indexNums = indices
|
|
1221
|
-
.map((s) => parseInt(s, 10))
|
|
1222
|
-
.filter((n) => Number.isFinite(n));
|
|
1280
|
+
const indexNums = parseProviderIndices(indices);
|
|
1223
1281
|
if (indexNums.length === 0) {
|
|
1224
1282
|
console.log("No valid indices provided.");
|
|
1225
1283
|
process.exit(1);
|
|
@@ -1246,6 +1304,51 @@ providersCmd
|
|
|
1246
1304
|
}
|
|
1247
1305
|
});
|
|
1248
1306
|
|
|
1307
|
+
providersCmd
|
|
1308
|
+
.command("nostr-sync <indices...>")
|
|
1309
|
+
.description(
|
|
1310
|
+
"Remove manual enable/disable overrides and return providers to their Nostr review-based state (e.g., routstrd providers nostr-sync 0 2 5)",
|
|
1311
|
+
)
|
|
1312
|
+
.action(async (indices: string[]) => {
|
|
1313
|
+
await ensureDaemonRunning();
|
|
1314
|
+
|
|
1315
|
+
const indexNums = parseProviderIndices(indices);
|
|
1316
|
+
if (indexNums.length === 0) {
|
|
1317
|
+
console.log("No valid indices provided.");
|
|
1318
|
+
process.exit(1);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const result = await callDaemon("/providers/nostr-sync", {
|
|
1322
|
+
method: "POST",
|
|
1323
|
+
body: { indices: indexNums },
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
if (result.error) {
|
|
1327
|
+
console.log(result.error);
|
|
1328
|
+
process.exit(1);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
const output = result.output as
|
|
1332
|
+
| {
|
|
1333
|
+
message: string;
|
|
1334
|
+
providers: Array<{ baseUrl: string; disabled: boolean }>;
|
|
1335
|
+
skipped?: unknown[];
|
|
1336
|
+
}
|
|
1337
|
+
| undefined;
|
|
1338
|
+
if (output) {
|
|
1339
|
+
console.log(output.message);
|
|
1340
|
+
for (const provider of output.providers) {
|
|
1341
|
+
const status = provider.disabled
|
|
1342
|
+
? "DISABLED (nostr review)"
|
|
1343
|
+
: "enabled";
|
|
1344
|
+
console.log(` - ${provider.baseUrl} -> ${status}`);
|
|
1345
|
+
}
|
|
1346
|
+
if (output.skipped && output.skipped.length > 0) {
|
|
1347
|
+
console.log(`Skipped invalid indices: ${output.skipped.join(", ")}`);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
|
|
1249
1352
|
providersCmd
|
|
1250
1353
|
.command("reviews")
|
|
1251
1354
|
.description(
|
|
@@ -1334,6 +1437,52 @@ providersCmd
|
|
|
1334
1437
|
}
|
|
1335
1438
|
});
|
|
1336
1439
|
|
|
1440
|
+
// Cooldowns - providers/models the router is currently skipping
|
|
1441
|
+
program
|
|
1442
|
+
.command("cooldowns")
|
|
1443
|
+
.description(
|
|
1444
|
+
"List providers and models currently on cooldown (temporarily skipped by the router)",
|
|
1445
|
+
)
|
|
1446
|
+
.option("--json", "Print the raw daemon response as JSON", false)
|
|
1447
|
+
.action(async (options: { json: boolean }) => {
|
|
1448
|
+
await ensureDaemonRunning();
|
|
1449
|
+
|
|
1450
|
+
let result: CommandResponse;
|
|
1451
|
+
try {
|
|
1452
|
+
result = await callDaemon("/cooldowns");
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1455
|
+
// A daemon built before this command existed has no /cooldowns route:
|
|
1456
|
+
// it either 404s or falls through to the "POST only" catch-all.
|
|
1457
|
+
if (message.includes("404") || message === "Only POST is supported.") {
|
|
1458
|
+
console.error(
|
|
1459
|
+
"The running daemon has no /cooldowns endpoint, so it cannot report cooldowns. " +
|
|
1460
|
+
"Restart it on a newer build (routstrd service restart) and try again.",
|
|
1461
|
+
);
|
|
1462
|
+
process.exit(1);
|
|
1463
|
+
}
|
|
1464
|
+
throw error;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
if (result.error) {
|
|
1468
|
+
console.log(result.error);
|
|
1469
|
+
process.exit(1);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
const output = result.output as CooldownsOutput | undefined;
|
|
1473
|
+
if (options.json) {
|
|
1474
|
+
console.log(JSON.stringify(output ?? {}, null, 2));
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
if (!output) {
|
|
1479
|
+
console.log("No cooldown data returned by the daemon.");
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
console.log(formatCooldowns(output));
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1337
1486
|
// Clients - list and manage clients
|
|
1338
1487
|
const clientsCmd = program
|
|
1339
1488
|
.command("clients")
|
|
@@ -1671,17 +1820,50 @@ program
|
|
|
1671
1820
|
.description("Show wallet transaction history")
|
|
1672
1821
|
.option("-n, --limit <number>", "Number of entries to show", "50")
|
|
1673
1822
|
.option("--offset <number>", "Number of entries to skip", "0")
|
|
1674
|
-
.option(
|
|
1823
|
+
.option(
|
|
1824
|
+
"-t, --type <type...>",
|
|
1825
|
+
"Filter by transaction type (send, receive, mint, melt). Repeatable or comma-separated",
|
|
1826
|
+
)
|
|
1827
|
+
.option("-i, --id <id>", "Show a single transaction by its ID")
|
|
1828
|
+
.option("-v, --verbose", "Show full details instead of the summary line")
|
|
1675
1829
|
.option("--json", "Output raw JSON with token objects (no encoding)")
|
|
1676
|
-
.action(async (options: {
|
|
1830
|
+
.action(async (options: {
|
|
1831
|
+
limit: string;
|
|
1832
|
+
offset: string;
|
|
1833
|
+
type?: string[];
|
|
1834
|
+
id?: string;
|
|
1835
|
+
verbose: boolean;
|
|
1836
|
+
json: boolean;
|
|
1837
|
+
}) => {
|
|
1677
1838
|
await ensureDaemonRunning();
|
|
1678
1839
|
|
|
1679
1840
|
const limit = Math.min(parseInt(options.limit, 10) || 50, 1000);
|
|
1680
1841
|
const offset = parseInt(options.offset, 10) || 0;
|
|
1681
1842
|
|
|
1682
|
-
|
|
1683
|
-
|
|
1843
|
+
// Support both repeated flags (-t send -t mint) and comma-separated
|
|
1844
|
+
// values (-t send,mint).
|
|
1845
|
+
const requestedTypes = (options.type ?? [])
|
|
1846
|
+
.flatMap((value) => value.split(","))
|
|
1847
|
+
.map((value) => value.trim().toLowerCase())
|
|
1848
|
+
.filter((value) => value.length > 0);
|
|
1849
|
+
const unknownTypes = requestedTypes.filter(
|
|
1850
|
+
(value) => !isHistoryEntryType(value),
|
|
1684
1851
|
);
|
|
1852
|
+
if (unknownTypes.length > 0) {
|
|
1853
|
+
console.error(
|
|
1854
|
+
`Unknown transaction type: ${unknownTypes.join(", ")}. Valid types: ${HISTORY_ENTRY_TYPES.join(", ")}.`,
|
|
1855
|
+
);
|
|
1856
|
+
process.exit(1);
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
const query = new URLSearchParams({
|
|
1860
|
+
offset: String(offset),
|
|
1861
|
+
limit: String(limit),
|
|
1862
|
+
});
|
|
1863
|
+
if (requestedTypes.length > 0) query.set("type", requestedTypes.join(","));
|
|
1864
|
+
if (options.id) query.set("id", options.id);
|
|
1865
|
+
|
|
1866
|
+
const result = await callDaemon(`/wallet/history?${query.toString()}`);
|
|
1685
1867
|
|
|
1686
1868
|
if (result.error) {
|
|
1687
1869
|
console.log(result.error);
|
|
@@ -1694,7 +1876,15 @@ program
|
|
|
1694
1876
|
const entries = data?.entries || [];
|
|
1695
1877
|
|
|
1696
1878
|
if (entries.length === 0) {
|
|
1697
|
-
|
|
1879
|
+
if (options.id) {
|
|
1880
|
+
console.log(`No transaction found with ID ${options.id}.`);
|
|
1881
|
+
} else if (requestedTypes.length > 0) {
|
|
1882
|
+
console.log(
|
|
1883
|
+
`No ${requestedTypes.join(", ")} transactions found.`,
|
|
1884
|
+
);
|
|
1885
|
+
} else {
|
|
1886
|
+
console.log("No transaction history yet.");
|
|
1887
|
+
}
|
|
1698
1888
|
return;
|
|
1699
1889
|
}
|
|
1700
1890
|
|
|
@@ -1751,10 +1941,13 @@ program
|
|
|
1751
1941
|
const pad = (s: string, w: number) => s.padEnd(w);
|
|
1752
1942
|
const sep = Object.values(widths).map((w) => "-".repeat(w)).join(" | ");
|
|
1753
1943
|
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1944
|
+
// A single transaction lookup prints just its summary line.
|
|
1945
|
+
if (!options.id) {
|
|
1946
|
+
console.log(
|
|
1947
|
+
`${pad(idCol, widths.id)} | ${pad(timeCol, widths.time)} | ${pad(typeCol, widths.type)} | ${pad(mintCol, widths.mint)} | ${pad(amtCol, widths.amount)}`,
|
|
1948
|
+
);
|
|
1949
|
+
console.log(sep);
|
|
1950
|
+
}
|
|
1758
1951
|
|
|
1759
1952
|
for (const row of rows) {
|
|
1760
1953
|
console.log(
|
|
@@ -1787,6 +1980,7 @@ program
|
|
|
1787
1980
|
)
|
|
1788
1981
|
.option("--mint-url <url>", "Mint URL to use")
|
|
1789
1982
|
.action(async (target: string, options: { mintUrl?: string }) => {
|
|
1983
|
+
await warnIfWalletRecovering();
|
|
1790
1984
|
if (isPositiveIntegerString(target)) {
|
|
1791
1985
|
await handleDaemonCommand("/wallet/send/cashu", {
|
|
1792
1986
|
method: "POST",
|
|
@@ -1814,6 +2008,7 @@ program
|
|
|
1814
2008
|
)
|
|
1815
2009
|
.option("--mint-url <url>", "Mint URL to use for bolt11 receive")
|
|
1816
2010
|
.action(async (value: string, options: { mintUrl?: string }) => {
|
|
2011
|
+
await warnIfWalletRecovering();
|
|
1817
2012
|
if (isPositiveIntegerString(value)) {
|
|
1818
2013
|
try {
|
|
1819
2014
|
await ensureDaemonRunning();
|
|
@@ -1896,16 +2091,22 @@ walletCmd
|
|
|
1896
2091
|
.option("--mint-url <url>", "Only clean up operations for this mint URL")
|
|
1897
2092
|
.option(
|
|
1898
2093
|
"--min-age <hours>",
|
|
1899
|
-
"Minimum age for reclaiming sends/cancelling melts, in hours (default: 168, one week; expired mint quotes are
|
|
2094
|
+
"Minimum age for reclaiming sends/cancelling melts, in hours (default: 168, one week; expired mint quotes are checked with their mint)",
|
|
1900
2095
|
"168",
|
|
1901
2096
|
)
|
|
1902
2097
|
.option("--dry-run", "Report what would be cleaned without applying changes", false)
|
|
2098
|
+
.option(
|
|
2099
|
+
"--force",
|
|
2100
|
+
"Fail expired mint quotes without confirming UNPAID with the mint (may strand paid quotes)",
|
|
2101
|
+
false,
|
|
2102
|
+
)
|
|
1903
2103
|
.option("-y, --yes", "Skip confirmation prompt", false)
|
|
1904
2104
|
.action(
|
|
1905
2105
|
async (options: {
|
|
1906
2106
|
mintUrl?: string;
|
|
1907
2107
|
minAge: string;
|
|
1908
2108
|
dryRun: boolean;
|
|
2109
|
+
force: boolean;
|
|
1909
2110
|
yes: boolean;
|
|
1910
2111
|
}) => {
|
|
1911
2112
|
const minAgeHours = Number.parseFloat(options.minAge);
|
|
@@ -1921,7 +2122,9 @@ walletCmd
|
|
|
1921
2122
|
});
|
|
1922
2123
|
const answer = await new Promise<string>((resolve) => {
|
|
1923
2124
|
rl.question(
|
|
1924
|
-
|
|
2125
|
+
options.force
|
|
2126
|
+
? "WARNING: --force fails expired mint quotes WITHOUT checking the mint and may strand paid sats. It also reclaims old pending sends and cancels prepared melts. Continue? [y/N] "
|
|
2127
|
+
: "This will fail expired mint quotes confirmed unpaid, reclaim old pending sends, and cancel prepared melts. Continue? [y/N] ",
|
|
1925
2128
|
(value: string) => {
|
|
1926
2129
|
rl.close();
|
|
1927
2130
|
resolve(value.trim().toLowerCase());
|
|
@@ -1943,6 +2146,7 @@ walletCmd
|
|
|
1943
2146
|
mintUrl: options.mintUrl,
|
|
1944
2147
|
minAgeMs: Math.round(minAgeHours * 60 * 60 * 1000),
|
|
1945
2148
|
dryRun: options.dryRun === true,
|
|
2149
|
+
force: options.force === true,
|
|
1946
2150
|
},
|
|
1947
2151
|
});
|
|
1948
2152
|
|
|
@@ -1955,6 +2159,8 @@ walletCmd
|
|
|
1955
2159
|
| {
|
|
1956
2160
|
dryRun?: boolean;
|
|
1957
2161
|
failedMintQuotes?: number;
|
|
2162
|
+
mintQuoteCandidates?: number;
|
|
2163
|
+
leftForRecovery?: number;
|
|
1958
2164
|
reclaimedSends?: number;
|
|
1959
2165
|
cancelledMelts?: number;
|
|
1960
2166
|
skipped?: number;
|
|
@@ -1965,8 +2171,15 @@ walletCmd
|
|
|
1965
2171
|
if (output) {
|
|
1966
2172
|
const prefix = output.dryRun ? "Would clean up:" : "Cleaned up:";
|
|
1967
2173
|
console.log(prefix);
|
|
2174
|
+
if (output.dryRun) {
|
|
2175
|
+
console.log(
|
|
2176
|
+
` Expired mint quote candidates (not checked with mint): ${output.mintQuoteCandidates ?? 0}`,
|
|
2177
|
+
);
|
|
2178
|
+
} else {
|
|
2179
|
+
console.log(` Expired mint quotes failed: ${output.failedMintQuotes ?? 0}`);
|
|
2180
|
+
}
|
|
1968
2181
|
console.log(
|
|
1969
|
-
` Expired
|
|
2182
|
+
` Expired quotes kept for recovery (paid/issued/unverified): ${output.leftForRecovery ?? 0}`,
|
|
1970
2183
|
);
|
|
1971
2184
|
console.log(` Pending sends reclaimed: ${output.reclaimedSends ?? 0}`);
|
|
1972
2185
|
console.log(
|
|
@@ -1997,6 +2210,124 @@ walletCmd
|
|
|
1997
2210
|
},
|
|
1998
2211
|
);
|
|
1999
2212
|
|
|
2213
|
+
walletCmd
|
|
2214
|
+
.command("recover")
|
|
2215
|
+
.description(
|
|
2216
|
+
"Retry mint quotes using their stored outputs (does not replace rejected outputs)",
|
|
2217
|
+
)
|
|
2218
|
+
.option(
|
|
2219
|
+
"--op <id>",
|
|
2220
|
+
"Recover this operation id (repeatable; find IDs with routstrd history --json)",
|
|
2221
|
+
(value: string, previous: string[]) => [...previous, value],
|
|
2222
|
+
[] as string[],
|
|
2223
|
+
)
|
|
2224
|
+
.option(
|
|
2225
|
+
"--include-failed",
|
|
2226
|
+
"Also re-open operations coco already gave up on (requires --op)",
|
|
2227
|
+
false,
|
|
2228
|
+
)
|
|
2229
|
+
.option("-y, --yes", "Skip confirmation prompt", false)
|
|
2230
|
+
.action(
|
|
2231
|
+
async (options: {
|
|
2232
|
+
op: string[];
|
|
2233
|
+
includeFailed: boolean;
|
|
2234
|
+
yes: boolean;
|
|
2235
|
+
}) => {
|
|
2236
|
+
const operationIds = options.op ?? [];
|
|
2237
|
+
if (options.includeFailed && operationIds.length === 0) {
|
|
2238
|
+
console.error(
|
|
2239
|
+
"--include-failed can only target operations named with --op",
|
|
2240
|
+
);
|
|
2241
|
+
process.exit(1);
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
if (!options.yes) {
|
|
2245
|
+
const rl = require("readline").createInterface({
|
|
2246
|
+
input: process.stdin,
|
|
2247
|
+
output: process.stdout,
|
|
2248
|
+
});
|
|
2249
|
+
const prompt =
|
|
2250
|
+
operationIds.length > 0
|
|
2251
|
+
? `Recover ${operationIds.length} mint quote operation(s)? [y/N] `
|
|
2252
|
+
: "Check every pending mint quote with its mint and claim any paid sats? [y/N] ";
|
|
2253
|
+
const answer = await new Promise<string>((resolve) => {
|
|
2254
|
+
rl.question(prompt, (value: string) => {
|
|
2255
|
+
rl.close();
|
|
2256
|
+
resolve(value.trim().toLowerCase());
|
|
2257
|
+
});
|
|
2258
|
+
});
|
|
2259
|
+
if (answer !== "y" && answer !== "yes") {
|
|
2260
|
+
console.log("Aborted.");
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
try {
|
|
2266
|
+
await ensureDaemonRunning();
|
|
2267
|
+
|
|
2268
|
+
const result = await callDaemon("/wallet/recover", {
|
|
2269
|
+
method: "POST",
|
|
2270
|
+
body: {
|
|
2271
|
+
operationIds: operationIds.length > 0 ? operationIds : undefined,
|
|
2272
|
+
includeFailed: options.includeFailed === true,
|
|
2273
|
+
},
|
|
2274
|
+
});
|
|
2275
|
+
|
|
2276
|
+
if (result.error) {
|
|
2277
|
+
console.log(result.error);
|
|
2278
|
+
process.exit(1);
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
const output = result.output as
|
|
2282
|
+
| {
|
|
2283
|
+
checked?: number;
|
|
2284
|
+
recovered?: number;
|
|
2285
|
+
waiting?: number;
|
|
2286
|
+
terminal?: number;
|
|
2287
|
+
reopened?: number;
|
|
2288
|
+
retryable?: number;
|
|
2289
|
+
busy?: number;
|
|
2290
|
+
errors?: Array<{ operationId: string; error: string }>;
|
|
2291
|
+
}
|
|
2292
|
+
| undefined;
|
|
2293
|
+
|
|
2294
|
+
if (output) {
|
|
2295
|
+
console.log("Mint quote recovery:");
|
|
2296
|
+
console.log(` Checked with mint: ${output.checked ?? 0}`);
|
|
2297
|
+
console.log(
|
|
2298
|
+
` Recovered (paid sats claimed): ${output.recovered ?? 0}`,
|
|
2299
|
+
);
|
|
2300
|
+
console.log(` Still unpaid: ${output.waiting ?? 0}`);
|
|
2301
|
+
console.log(` No longer issuable: ${output.terminal ?? 0}`);
|
|
2302
|
+
console.log(
|
|
2303
|
+
` Re-opened failed operations: ${output.reopened ?? 0}`,
|
|
2304
|
+
);
|
|
2305
|
+
console.log(` Left for a later run: ${output.retryable ?? 0}`);
|
|
2306
|
+
console.log(
|
|
2307
|
+
` Skipped (recovery still running): ${output.busy ?? 0}`,
|
|
2308
|
+
);
|
|
2309
|
+
if (output.errors && output.errors.length > 0) {
|
|
2310
|
+
console.log("\nErrors:");
|
|
2311
|
+
for (const e of output.errors) {
|
|
2312
|
+
console.log(` - ${e.operationId}: ${e.error}`);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
} catch (error) {
|
|
2317
|
+
const message = (error as Error).message;
|
|
2318
|
+
if (
|
|
2319
|
+
message?.includes("fetch failed") ||
|
|
2320
|
+
message?.includes("Connection refused")
|
|
2321
|
+
) {
|
|
2322
|
+
console.error("Daemon is not running");
|
|
2323
|
+
process.exit(1);
|
|
2324
|
+
}
|
|
2325
|
+
console.error(message);
|
|
2326
|
+
process.exit(1);
|
|
2327
|
+
}
|
|
2328
|
+
},
|
|
2329
|
+
);
|
|
2330
|
+
|
|
2000
2331
|
const walletReceiveCmd = walletCmd
|
|
2001
2332
|
.command("receive")
|
|
2002
2333
|
.description("Wallet receive operations");
|
|
@@ -2005,6 +2336,7 @@ walletReceiveCmd
|
|
|
2005
2336
|
.command("cashu <token>")
|
|
2006
2337
|
.description("Receive a Cashu token")
|
|
2007
2338
|
.action(async (token: string) => {
|
|
2339
|
+
await warnIfWalletRecovering();
|
|
2008
2340
|
await handleDaemonCommand("/wallet/receive/cashu", {
|
|
2009
2341
|
method: "POST",
|
|
2010
2342
|
body: { token },
|
|
@@ -2016,6 +2348,7 @@ walletReceiveCmd
|
|
|
2016
2348
|
.description("Create a Lightning invoice")
|
|
2017
2349
|
.option("--mint-url <url>", "Mint URL to use")
|
|
2018
2350
|
.action(async (amount: string, options: { mintUrl?: string }) => {
|
|
2351
|
+
await warnIfWalletRecovering();
|
|
2019
2352
|
try {
|
|
2020
2353
|
await ensureDaemonRunning();
|
|
2021
2354
|
|
|
@@ -2054,6 +2387,7 @@ walletSendCmd
|
|
|
2054
2387
|
.description("Create a Cashu token to send")
|
|
2055
2388
|
.option("--mint-url <url>", "Mint URL to use")
|
|
2056
2389
|
.action(async (amount: string, options: { mintUrl?: string }) => {
|
|
2390
|
+
await warnIfWalletRecovering();
|
|
2057
2391
|
await handleDaemonCommand("/wallet/send/cashu", {
|
|
2058
2392
|
method: "POST",
|
|
2059
2393
|
body: {
|
|
@@ -2068,6 +2402,7 @@ walletSendCmd
|
|
|
2068
2402
|
.description("Pay a Lightning invoice")
|
|
2069
2403
|
.option("--mint-url <url>", "Mint URL to use")
|
|
2070
2404
|
.action(async (invoice: string, options: { mintUrl?: string }) => {
|
|
2405
|
+
await warnIfWalletRecovering();
|
|
2071
2406
|
await handleDaemonCommand("/wallet/send/bolt11", {
|
|
2072
2407
|
method: "POST",
|
|
2073
2408
|
body: {
|