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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "routstrd",
3
- "version": "0.4.11",
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.6",
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.disabled ? "DISABLED" : "enabled ";
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("-v, --verbose", "Show full details including encoded Cashu tokens")
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: { limit: string; offset: string; verbose: boolean; json: boolean }) => {
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
- const result = await callDaemon(
1683
- `/wallet/history?offset=${offset}&limit=${limit}`,
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
- console.log("No transaction history yet.");
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
- console.log(
1755
- `${pad(idCol, widths.id)} | ${pad(timeCol, widths.time)} | ${pad(typeCol, widths.type)} | ${pad(mintCol, widths.mint)} | ${pad(amtCol, widths.amount)}`,
1756
- );
1757
- console.log(sep);
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 always failed)",
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
- "This will fail expired mint quotes, reclaim old pending sends, and cancel prepared melts. Continue? [y/N] ",
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 mint quotes failed: ${output.failedMintQuotes ?? 0}`,
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: {