routstrd 0.2.22 → 0.3.0
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/.claude/settings.local.json +10 -0
- package/Dockerfile +8 -0
- package/bun.lock +75 -5
- package/dist/daemon/index.js +29829 -18929
- package/dist/index.js +309 -88
- package/docker-compose.yml +8 -7
- package/new-task.md +60 -0
- package/package.json +2 -1
- package/src/cli.ts +140 -1
- package/src/daemon/config-store.ts +13 -1
- package/src/daemon/wallet/auto-refill.ts +192 -0
- package/src/utils/config.ts +24 -0
package/docker-compose.yml
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
services:
|
|
2
|
-
|
|
2
|
+
routstrd:
|
|
3
3
|
build: .
|
|
4
|
-
container_name: routstrd
|
|
5
|
-
working_dir: /app
|
|
6
|
-
volumes:
|
|
7
|
-
- .:/app
|
|
8
|
-
- bun_home:/root/.bun
|
|
4
|
+
container_name: routstrd
|
|
9
5
|
stdin_open: true
|
|
10
6
|
tty: true
|
|
7
|
+
volumes:
|
|
8
|
+
- routstrd_data:/data
|
|
9
|
+
ports:
|
|
10
|
+
- "8008:8008"
|
|
11
|
+
restart: unless-stopped
|
|
11
12
|
command: /bin/bash
|
|
12
13
|
|
|
13
14
|
volumes:
|
|
14
|
-
|
|
15
|
+
routstrd_data:
|
package/new-task.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Hot-reload NWC connection (remove restart requirement)
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
`nwc connect` and `nwc disconnect` require a daemon restart to take effect. The
|
|
6
|
+
CLI writes the new connection string to disk, but the daemon never re-reads it.
|
|
7
|
+
|
|
8
|
+
## Current flow
|
|
9
|
+
|
|
10
|
+
1. **Startup** — `createWalletAdapter` reads `options.nwcConnectionString` and
|
|
11
|
+
creates a `WalletConnect` (relay pool + NWC wallet service) once:
|
|
12
|
+
```ts
|
|
13
|
+
// src/daemon/wallet/index.ts:70-78
|
|
14
|
+
if (options.nwcConnectionString) {
|
|
15
|
+
wallet = WalletConnect.fromConnectURI(options.nwcConnectionString, { pool });
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
2. **CLI `nwc connect`** — HTTP handler at `POST /nwc/connect` only calls
|
|
20
|
+
`saveDaemonConfig(config)`. The running `WalletConnect` instance is untouched.
|
|
21
|
+
|
|
22
|
+
3. **Result** — the old connection (or lack thereof) stays active until restart.
|
|
23
|
+
|
|
24
|
+
## What needs to happen
|
|
25
|
+
|
|
26
|
+
Provide a way for the daemon to tear down the old `WalletConnect` and create a
|
|
27
|
+
new one from the updated connection string, triggered by the HTTP handler,
|
|
28
|
+
**without restarting the process**.
|
|
29
|
+
|
|
30
|
+
### Key pieces
|
|
31
|
+
|
|
32
|
+
- **`src/daemon/wallet/index.ts`** — `createWalletAdapter` needs to expose a
|
|
33
|
+
`reconnect(connectionString: string)` method that:
|
|
34
|
+
- Closes the old relay pool / WalletConnect
|
|
35
|
+
- Creates a new `WalletConnect` from the new connection string
|
|
36
|
+
- Updates the adapter's internal `wallet` reference
|
|
37
|
+
- If connection string is empty/undefined, disconnects and sets `wallet` to
|
|
38
|
+
`undefined`
|
|
39
|
+
|
|
40
|
+
- **`src/daemon/http/index.ts`** — `POST /nwc/connect` and `POST /nwc/disconnect`
|
|
41
|
+
handlers should call `reconnect()` after saving config, and drop the "Restart
|
|
42
|
+
daemon" message.
|
|
43
|
+
|
|
44
|
+
- **`src/daemon/index.ts`** — may need to store the `reconnect` function and pass
|
|
45
|
+
it through `DaemonDeps` to the HTTP handler.
|
|
46
|
+
|
|
47
|
+
- **`src/cli.ts`** — remove `console.log("\nRun 'routstrd restart' to connect...")`
|
|
48
|
+
from the connect/disconnect commands.
|
|
49
|
+
|
|
50
|
+
### Considerations
|
|
51
|
+
|
|
52
|
+
- The `WalletConnect` instance is referenced in the auto-refill loop (passed to
|
|
53
|
+
`startAutoRefillLoop`). After a reconnect, the auto-refill loop's captured
|
|
54
|
+
`wallet` reference would be stale. The loop already reads config from disk
|
|
55
|
+
each cycle, so it should also reference the latest `wallet` instance.
|
|
56
|
+
- Could make the auto-refill loop accept a `getWallet: () => WalletConnect |
|
|
57
|
+
undefined` getter, same pattern as the config getter.
|
|
58
|
+
- Relay pool cleanup — `applesauce-wallet-connect` / `applesauce-relay` may need
|
|
59
|
+
explicit `pool.close()` or `pool.destroy()` to free connections.
|
|
60
|
+
- Edge case: a refill is in progress when reconnect is triggered.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "routstrd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"module": "src/index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"@routstr/sdk": "^0.3.8",
|
|
28
28
|
"applesauce-core": "^5.1.0",
|
|
29
29
|
"applesauce-relay": "^5.1.0",
|
|
30
|
+
"applesauce-wallet-connect": "^6.0.0",
|
|
30
31
|
"commander": "^14.0.2",
|
|
31
32
|
"nostr-tools": "^2.12.0",
|
|
32
33
|
"qrcode": "^1.5.4",
|
package/src/cli.ts
CHANGED
|
@@ -231,7 +231,8 @@ program
|
|
|
231
231
|
"Mint URL to refund to (defaults to first mint in wallet)",
|
|
232
232
|
)
|
|
233
233
|
.option("-y, --yes", "Skip confirmation prompt", false)
|
|
234
|
-
.
|
|
234
|
+
.option("--xcashu", "Refund xcashu tokens only (uses refundXcashuTokens)", false)
|
|
235
|
+
.action(async (options: { mintUrl?: string; yes: boolean; xcashu: boolean }) => {
|
|
235
236
|
await ensureDaemonRunning();
|
|
236
237
|
|
|
237
238
|
let mintUrl = options.mintUrl;
|
|
@@ -257,6 +258,36 @@ program
|
|
|
257
258
|
}
|
|
258
259
|
|
|
259
260
|
try {
|
|
261
|
+
if (options.xcashu) {
|
|
262
|
+
// xcashu path: only refund xcashu tokens
|
|
263
|
+
const result = await callDaemon("/refund/xcashu", {
|
|
264
|
+
method: "POST",
|
|
265
|
+
body: { mintUrl },
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
if (result.error) {
|
|
269
|
+
console.log(result.error);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const output = result.output as
|
|
274
|
+
| {
|
|
275
|
+
message: string;
|
|
276
|
+
results: Array<{ baseUrl: string; token: string; success: boolean; error?: string }>;
|
|
277
|
+
}
|
|
278
|
+
| undefined;
|
|
279
|
+
|
|
280
|
+
if (output) {
|
|
281
|
+
console.log(output.message);
|
|
282
|
+
console.log("\nResults:");
|
|
283
|
+
for (const r of output.results) {
|
|
284
|
+
const status = r.success ? "success" : `failed: ${r.error || "unknown"}`;
|
|
285
|
+
console.log(` - ${r.baseUrl}: ${status}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
260
291
|
const result = await callDaemon("/refund", {
|
|
261
292
|
method: "POST",
|
|
262
293
|
body: { mintUrl },
|
|
@@ -1253,6 +1284,114 @@ walletMintsCmd
|
|
|
1253
1284
|
});
|
|
1254
1285
|
});
|
|
1255
1286
|
|
|
1287
|
+
// ── NWC (Nostr Wallet Connect) commands ─────────────────────────
|
|
1288
|
+
|
|
1289
|
+
const nwcCmd = program
|
|
1290
|
+
.command("nwc")
|
|
1291
|
+
.description("Manage NWC (Nostr Wallet Connect) integration");
|
|
1292
|
+
|
|
1293
|
+
nwcCmd
|
|
1294
|
+
.command("connect")
|
|
1295
|
+
.description("Connect to a Lightning wallet via NWC")
|
|
1296
|
+
.argument("[connection-string]", "NWC connection string (nostr+walletconnect://...)")
|
|
1297
|
+
.action(async (connectionString?: string) => {
|
|
1298
|
+
if (!connectionString) {
|
|
1299
|
+
// Interactive mode: prompt for connection string
|
|
1300
|
+
const rl = require("readline").createInterface({
|
|
1301
|
+
input: process.stdin,
|
|
1302
|
+
output: process.stdout,
|
|
1303
|
+
});
|
|
1304
|
+
connectionString = await new Promise<string>((resolve) => {
|
|
1305
|
+
rl.question("Paste your NWC connection string: ", (answer: string) => {
|
|
1306
|
+
rl.close();
|
|
1307
|
+
resolve(answer.trim());
|
|
1308
|
+
});
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// Quick validation: must be nostr+walletconnect:// with a 64-char hex pubkey
|
|
1313
|
+
if (!/^nostr\+walletconnect:\/\/[0-9a-fA-F]{64}\?relay=/.test(connectionString)) {
|
|
1314
|
+
console.error("Invalid NWC connection string: expected nostr+walletconnect://<64-char-hex>?relay=...");
|
|
1315
|
+
process.exit(1);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
await handleDaemonCommand("/nwc/connect", {
|
|
1319
|
+
method: "POST",
|
|
1320
|
+
body: { connectionString },
|
|
1321
|
+
});
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
nwcCmd
|
|
1325
|
+
.command("disconnect")
|
|
1326
|
+
.description("Disconnect from NWC wallet")
|
|
1327
|
+
.action(async () => {
|
|
1328
|
+
await handleDaemonCommand("/nwc/disconnect", {
|
|
1329
|
+
method: "POST",
|
|
1330
|
+
});
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
nwcCmd
|
|
1334
|
+
.command("status")
|
|
1335
|
+
.description("Show NWC connection status and wallet info")
|
|
1336
|
+
.action(async () => {
|
|
1337
|
+
await handleDaemonCommand("/nwc/status");
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
nwcCmd
|
|
1341
|
+
.command("fund <amount>")
|
|
1342
|
+
.description("Manually fund the Cashu wallet from the connected NWC wallet")
|
|
1343
|
+
.action(async (amount: string) => {
|
|
1344
|
+
const parsedAmount = parsePositiveIntOrExit(amount, "amount");
|
|
1345
|
+
await handleDaemonCommand("/nwc/fund", {
|
|
1346
|
+
method: "POST",
|
|
1347
|
+
body: { amount: parsedAmount },
|
|
1348
|
+
});
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
const autoRefillCmd = nwcCmd
|
|
1352
|
+
.command("auto-refill")
|
|
1353
|
+
.description("Manage automatic wallet refill from NWC");
|
|
1354
|
+
|
|
1355
|
+
autoRefillCmd
|
|
1356
|
+
.command("on")
|
|
1357
|
+
.description("Enable auto-refill")
|
|
1358
|
+
.option(
|
|
1359
|
+
"--threshold <sats>",
|
|
1360
|
+
"Refill when Cashu balance drops below this many sats",
|
|
1361
|
+
"500",
|
|
1362
|
+
)
|
|
1363
|
+
.option("--amount <sats>", "Refill this many sats at a time", "1000")
|
|
1364
|
+
.option(
|
|
1365
|
+
"--cooldown <seconds>",
|
|
1366
|
+
"Minimum time between refills in seconds",
|
|
1367
|
+
"300",
|
|
1368
|
+
)
|
|
1369
|
+
.action(async (options: { threshold: string; amount: string; cooldown: string }) => {
|
|
1370
|
+
const threshold = parsePositiveIntOrExit(options.threshold, "threshold");
|
|
1371
|
+
const amount = parsePositiveIntOrExit(options.amount, "amount");
|
|
1372
|
+
const cooldownSec = parsePositiveIntOrExit(options.cooldown, "cooldown");
|
|
1373
|
+
|
|
1374
|
+
await handleDaemonCommand("/nwc/auto-refill", {
|
|
1375
|
+
method: "POST",
|
|
1376
|
+
body: {
|
|
1377
|
+
enabled: true,
|
|
1378
|
+
threshold,
|
|
1379
|
+
amount,
|
|
1380
|
+
cooldownMs: cooldownSec * 1000,
|
|
1381
|
+
},
|
|
1382
|
+
});
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
autoRefillCmd
|
|
1386
|
+
.command("off")
|
|
1387
|
+
.description("Disable auto-refill")
|
|
1388
|
+
.action(async () => {
|
|
1389
|
+
await handleDaemonCommand("/nwc/auto-refill", {
|
|
1390
|
+
method: "POST",
|
|
1391
|
+
body: { enabled: false },
|
|
1392
|
+
});
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1256
1395
|
// Stop
|
|
1257
1396
|
program
|
|
1258
1397
|
.command("stop")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mkdir } from "fs/promises";
|
|
2
|
-
import { existsSync } from "fs";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
3
|
import {
|
|
4
4
|
CONFIG_DIR,
|
|
5
5
|
CONFIG_FILE,
|
|
@@ -31,6 +31,18 @@ export async function loadDaemonConfig(): Promise<RoutstrdConfig> {
|
|
|
31
31
|
return DEFAULT_CONFIG;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
export function loadDaemonConfigSync(): RoutstrdConfig {
|
|
35
|
+
try {
|
|
36
|
+
if (existsSync(CONFIG_FILE)) {
|
|
37
|
+
const content = readFileSync(CONFIG_FILE, "utf-8");
|
|
38
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
logger.error("Failed to load config:", error);
|
|
42
|
+
}
|
|
43
|
+
return DEFAULT_CONFIG;
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
export function saveDaemonConfig(config: RoutstrdConfig): void {
|
|
35
47
|
Bun.write(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
36
48
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Auto-refill loop for routstrd
|
|
2
|
+
// Monitors Cocod Cashu balance and triggers NWC funding when below threshold.
|
|
3
|
+
// This runs server-side, so refills happen regardless of whether a frontend is open.
|
|
4
|
+
//
|
|
5
|
+
// Uses applesauce-wallet-connect (same approach as nwc_integration/pay_invoice.mts).
|
|
6
|
+
|
|
7
|
+
import type { CocodClient } from "./cocod-client";
|
|
8
|
+
import type { WalletConnect } from "applesauce-wallet-connect";
|
|
9
|
+
import { logger } from "../../utils/logger";
|
|
10
|
+
|
|
11
|
+
export interface AutoRefillConfig {
|
|
12
|
+
/** Minimum sats balance before triggering a refill */
|
|
13
|
+
threshold: number;
|
|
14
|
+
/** Amount of sats to refill each time */
|
|
15
|
+
amount: number;
|
|
16
|
+
/** Minimum time between refills (milliseconds) */
|
|
17
|
+
cooldownMs: number;
|
|
18
|
+
/** Maximum consecutive failures before stopping (0 = never stop on failures) */
|
|
19
|
+
maxConsecutiveFailures?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Errors that cannot be resolved by retrying — the user must intervene.
|
|
23
|
+
const FATAL_ERROR_PATTERNS = [
|
|
24
|
+
/insufficient (?:balance|funds)/i,
|
|
25
|
+
/invalid (?:credentials|auth)/i,
|
|
26
|
+
/forbidden/i,
|
|
27
|
+
/unauthorized/i,
|
|
28
|
+
/invoice.*expired/i,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function isFatalError(message: string): boolean {
|
|
32
|
+
return FATAL_ERROR_PATTERNS.some((pattern) => pattern.test(message));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function startAutoRefillLoop(
|
|
36
|
+
cocod: CocodClient,
|
|
37
|
+
getWallet: () => WalletConnect | undefined,
|
|
38
|
+
getConfig: () => AutoRefillConfig | undefined,
|
|
39
|
+
intervalMs: number = 5000,
|
|
40
|
+
): () => void {
|
|
41
|
+
let lastRefillAt = 0;
|
|
42
|
+
let lastAttemptAt = 0; // tracks last attempt (success or failure) for backoff
|
|
43
|
+
let running = true;
|
|
44
|
+
let timeout: ReturnType<typeof setInterval> | null = null;
|
|
45
|
+
let checkInProgress = false;
|
|
46
|
+
let consecutiveFailures = 0;
|
|
47
|
+
|
|
48
|
+
async function checkAndRefill(): Promise<void> {
|
|
49
|
+
if (!running) return;
|
|
50
|
+
if (checkInProgress) return;
|
|
51
|
+
const wallet = getWallet();
|
|
52
|
+
if (!wallet?.service) {
|
|
53
|
+
// NWC not connected — nothing to do
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Read config fresh each cycle so changes apply without restart
|
|
58
|
+
const config = getConfig();
|
|
59
|
+
if (!config) {
|
|
60
|
+
// Auto-refill disabled
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
if (now - lastRefillAt < config.cooldownMs) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
checkInProgress = true;
|
|
70
|
+
|
|
71
|
+
// If we've been failing too much, back off rather than retrying at full speed.
|
|
72
|
+
// This prevents tight loops on transient errors.
|
|
73
|
+
const backoffInterval = Math.min(
|
|
74
|
+
intervalMs * Math.pow(2, consecutiveFailures),
|
|
75
|
+
5 * 60 * 1000, // cap at 5 minutes
|
|
76
|
+
);
|
|
77
|
+
if (now - lastAttemptAt < backoffInterval) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const balances = await cocod.getBalances();
|
|
83
|
+
const totalBalance = Object.values(balances).reduce<number>(
|
|
84
|
+
(sum, b) => sum + (typeof b === "number" ? b : 0),
|
|
85
|
+
0,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (totalBalance >= config.threshold) {
|
|
89
|
+
// Balance is sufficient
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
logger.log(
|
|
94
|
+
`[auto-refill] Balance ${totalBalance} sats < threshold ${config.threshold}. Refilling ${config.amount} sats...`,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Get active mint
|
|
98
|
+
const mints = await cocod.listMints();
|
|
99
|
+
const mintUrl = mints[0];
|
|
100
|
+
if (!mintUrl) {
|
|
101
|
+
logger.error("[auto-refill] No active mint configured");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Step 1: Create a BOLT-11 invoice via cocod to fund the Cashu wallet
|
|
106
|
+
logger.log(
|
|
107
|
+
`[auto-refill] Creating BOLT-11 invoice for ${config.amount} sats via ${mintUrl}...`,
|
|
108
|
+
);
|
|
109
|
+
const invoice = await cocod.receiveBolt11(config.amount, mintUrl);
|
|
110
|
+
|
|
111
|
+
// Step 2: Pay the invoice via NWC (applesauce)
|
|
112
|
+
logger.log(`[auto-refill] Paying invoice via NWC...`);
|
|
113
|
+
const currentWallet = getWallet();
|
|
114
|
+
if (!currentWallet?.service) {
|
|
115
|
+
logger.log("[auto-refill] Wallet disconnected during refill check");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const payment = await currentWallet.payInvoice(invoice);
|
|
119
|
+
|
|
120
|
+
// Step 3: The Cashu mint should automatically detect the paid invoice
|
|
121
|
+
// and issue tokens. We don't need to explicitly mint here; cocod
|
|
122
|
+
// handles this on its end when the mint sees the payment.
|
|
123
|
+
const preimage = payment.preimage;
|
|
124
|
+
if (preimage) {
|
|
125
|
+
logger.log(
|
|
126
|
+
`[auto-refill] Successfully refilled ${config.amount} sats. Preimage: ${preimage.slice(0, 16)}...`,
|
|
127
|
+
);
|
|
128
|
+
} else {
|
|
129
|
+
logger.log(
|
|
130
|
+
`[auto-refill] Successfully refilled ${config.amount} sats (no preimage returned).`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (payment.fees_paid !== undefined) {
|
|
134
|
+
logger.log(`[auto-refill] Fees paid: ${payment.fees_paid} msats`);
|
|
135
|
+
}
|
|
136
|
+
lastRefillAt = now;
|
|
137
|
+
lastAttemptAt = now;
|
|
138
|
+
consecutiveFailures = 0; // reset on success
|
|
139
|
+
} catch (error) {
|
|
140
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
141
|
+
lastAttemptAt = now; // track for backoff regardless of success/failure
|
|
142
|
+
consecutiveFailures++;
|
|
143
|
+
|
|
144
|
+
if (isFatalError(message)) {
|
|
145
|
+
// Cooldown for 10 minutes — the user likely needs to fund their NWC wallet.
|
|
146
|
+
// We don't stop permanently so it auto-recovers without a restart.
|
|
147
|
+
// Set lastRefillAt into the future so all time-gate checks block.
|
|
148
|
+
const FATAL_COOLDOWN_MS = 10 * 60 * 1000;
|
|
149
|
+
logger.error(
|
|
150
|
+
`[auto-refill] FATAL: ${message}. The funding wallet (NWC) cannot pay. ` +
|
|
151
|
+
`Cooling down for ${FATAL_COOLDOWN_MS / 60000} minutes. ` +
|
|
152
|
+
`Fund your NWC wallet and auto-refill will retry automatically.`,
|
|
153
|
+
);
|
|
154
|
+
lastRefillAt = now + FATAL_COOLDOWN_MS;
|
|
155
|
+
consecutiveFailures = 0; // reset — no point counting these as transient backoffs
|
|
156
|
+
checkInProgress = false;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const maxFailures = config.maxConsecutiveFailures ?? 0;
|
|
161
|
+
if (maxFailures > 0 && consecutiveFailures >= maxFailures) {
|
|
162
|
+
logger.error(
|
|
163
|
+
`[auto-refill] Stopping after ${consecutiveFailures} consecutive failures (maxConsecutiveFailures=${maxFailures}). ` +
|
|
164
|
+
`Last error: ${message}`,
|
|
165
|
+
);
|
|
166
|
+
running = false;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
logger.error(
|
|
171
|
+
`[auto-refill] Error (attempt ${consecutiveFailures}, next in ${Math.round(backoffInterval / 1000)}s): ${message}`,
|
|
172
|
+
);
|
|
173
|
+
} finally {
|
|
174
|
+
checkInProgress = false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check immediately on start
|
|
179
|
+
checkAndRefill();
|
|
180
|
+
|
|
181
|
+
// Then poll on interval
|
|
182
|
+
timeout = setInterval(checkAndRefill, intervalMs);
|
|
183
|
+
|
|
184
|
+
return () => {
|
|
185
|
+
running = false;
|
|
186
|
+
if (timeout) {
|
|
187
|
+
clearInterval(timeout);
|
|
188
|
+
timeout = null;
|
|
189
|
+
}
|
|
190
|
+
logger.log("[auto-refill] Stopped");
|
|
191
|
+
};
|
|
192
|
+
}
|
package/src/utils/config.ts
CHANGED
|
@@ -7,6 +7,28 @@ export const DB_PATH = `${CONFIG_DIR}/routstr.db`;
|
|
|
7
7
|
export const CONFIG_FILE = `${CONFIG_DIR}/config.json`;
|
|
8
8
|
export const LOGS_DIR = `${CONFIG_DIR}/logs`;
|
|
9
9
|
|
|
10
|
+
/** NWC auto-refill configuration */
|
|
11
|
+
export interface NwcAutoRefillConfig {
|
|
12
|
+
/** Whether auto-refill is enabled */
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
/** Refill when Cashu balance drops below this many sats */
|
|
15
|
+
threshold: number;
|
|
16
|
+
/** Refill this many sats at a time */
|
|
17
|
+
amount: number;
|
|
18
|
+
/** Minimum time between refills in milliseconds */
|
|
19
|
+
cooldownMs: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** NWC configuration section */
|
|
23
|
+
export interface NwcConfig {
|
|
24
|
+
/** NWC mode: "funding_source" = NWC funds the cocod Cashu wallet */
|
|
25
|
+
mode: "funding_source" | "standalone";
|
|
26
|
+
/** NWC connection string (nostr+walletconnect://...) */
|
|
27
|
+
connectionString?: string;
|
|
28
|
+
/** Auto-refill settings */
|
|
29
|
+
autoRefill?: NwcAutoRefillConfig;
|
|
30
|
+
}
|
|
31
|
+
|
|
10
32
|
export interface RoutstrdConfig {
|
|
11
33
|
port: number;
|
|
12
34
|
provider: string | null;
|
|
@@ -16,6 +38,8 @@ export interface RoutstrdConfig {
|
|
|
16
38
|
nsec?: string;
|
|
17
39
|
/** Nostr hex pubkey for routstr review/model events (kind 38425/38423). */
|
|
18
40
|
routstrPubkey?: string;
|
|
41
|
+
/** NWC integration configuration */
|
|
42
|
+
nwc?: NwcConfig;
|
|
19
43
|
}
|
|
20
44
|
|
|
21
45
|
export const DEFAULT_CONFIG: RoutstrdConfig = {
|