routstrd 0.3.6 → 0.3.8
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/bun.lock +2 -2
- package/dist/daemon/index.js +122 -42
- package/dist/index.js +111 -5
- package/package.json +2 -2
- package/src/cli.ts +127 -0
- package/src/start-daemon.ts +6 -2
package/bun.lock
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"name": "routstrd",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@cashu/cashu-ts": "^4.3.0",
|
|
8
|
-
"@routstr/sdk": "^0.3.
|
|
8
|
+
"@routstr/sdk": "^0.3.15",
|
|
9
9
|
"applesauce-core": "^5.1.0",
|
|
10
10
|
"applesauce-relay": "^5.1.0",
|
|
11
11
|
"applesauce-wallet-connect": "^6.0.0",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
|
|
88
88
|
"@panva/hpke-noble": ["@panva/hpke-noble@1.1.2", "", { "dependencies": { "@noble/ciphers": "^2.2.0", "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@noble/post-quantum": "^0.6.1" }, "peerDependencies": { "hpke": "^1.0.0" } }, "sha512-Gr/jKr/ErpRow8aHK+SAp0sAaWy7iJGHlFsXsdTjD8ULcN4vvsVVWR3fiD6Ogf+VoSB28XY/xWRDUEIGXw+9aA=="],
|
|
89
89
|
|
|
90
|
-
"@routstr/sdk": ["@routstr/sdk@0.3.
|
|
90
|
+
"@routstr/sdk": ["@routstr/sdk@0.3.15", "", { "dependencies": { "@cashu/cashu-ts": "^3.1.1", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-sqlite": "^6.0.0", "ehbp": "^0.2.3", "rxjs": "^7.8.1", "tinfoil": "^1.1.6", "zustand": "^5.0.5" }, "optionalDependencies": { "better-sqlite3": "^12.10.0" }, "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-Cz1SAoeyEE8rYeivw3MxybkN/i9Ye4/EKAkbjoWbLMZKX+ECFuGB1hHuDzda9Id5Y7n0Rn4PixVvIlxAWsk1IA=="],
|
|
91
91
|
|
|
92
92
|
"@scure/base": ["@scure/base@2.2.0", "", {}, "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg=="],
|
|
93
93
|
|
package/dist/daemon/index.js
CHANGED
|
@@ -85237,7 +85237,7 @@ function selectMintWithBalance(balances, units, amount, excludeMints = []) {
|
|
|
85237
85237
|
}
|
|
85238
85238
|
return { selectedMintUrl: null, selectedMintBalance: 0 };
|
|
85239
85239
|
}
|
|
85240
|
-
var CashuSpender = class {
|
|
85240
|
+
var CashuSpender = class _CashuSpender {
|
|
85241
85241
|
constructor(walletAdapter, storageAdapter, _discoveryAdapter, balanceManager, logger3) {
|
|
85242
85242
|
this.walletAdapter = walletAdapter;
|
|
85243
85243
|
this.storageAdapter = storageAdapter;
|
|
@@ -85252,6 +85252,9 @@ var CashuSpender = class {
|
|
|
85252
85252
|
_isBusy = false;
|
|
85253
85253
|
debugLevel = "WARN";
|
|
85254
85254
|
logger;
|
|
85255
|
+
static MAX_REFUND_RETRIES = 3;
|
|
85256
|
+
static REFUND_RETRY_INTERVAL_MS = 120000;
|
|
85257
|
+
_refundRetryInterval = null;
|
|
85255
85258
|
async receiveToken(token) {
|
|
85256
85259
|
try {
|
|
85257
85260
|
const result = await this.walletAdapter.receiveToken(token);
|
|
@@ -85550,6 +85553,35 @@ var CashuSpender = class {
|
|
|
85550
85553
|
throw new Error("BalanceManager not available for xcashu refund");
|
|
85551
85554
|
}
|
|
85552
85555
|
const fetchResult = await this.balanceManager.fetchRefundToken(baseUrl, xcashuToken.token, true);
|
|
85556
|
+
if (!fetchResult.success && fetchResult.status === 425) {
|
|
85557
|
+
results.push({
|
|
85558
|
+
baseUrl,
|
|
85559
|
+
token: xcashuToken.token,
|
|
85560
|
+
success: false,
|
|
85561
|
+
error: fetchResult.error
|
|
85562
|
+
});
|
|
85563
|
+
this._log("WARN", `[CashuSpender] refundXcashuTokens: 425 "Refund is pending" for ${baseUrl}; leaving token in store, tryCount untouched`);
|
|
85564
|
+
continue;
|
|
85565
|
+
}
|
|
85566
|
+
if (!fetchResult.success && fetchResult.status === 404 && (fetchResult.error || "").includes("Refund not found")) {
|
|
85567
|
+
const currentTryCount = xcashuToken.tryCount ?? 0;
|
|
85568
|
+
const newTryCount = currentTryCount + 1;
|
|
85569
|
+
if (newTryCount >= _CashuSpender.MAX_REFUND_RETRIES) {
|
|
85570
|
+
this.storageAdapter.removeXcashuToken(baseUrl, xcashuToken.token);
|
|
85571
|
+
this._log("WARN", `[CashuSpender] refundXcashuTokens: 404 "Refund not found" for ${baseUrl} after ${newTryCount} retries; removing unrefundable xcashu token from store`);
|
|
85572
|
+
} else {
|
|
85573
|
+
this.storageAdapter.updateXcashuTokenTryCount(xcashuToken.token, newTryCount);
|
|
85574
|
+
this._log("WARN", `[CashuSpender] refundXcashuTokens: 404 "Refund not found" for ${baseUrl}; tryCount=${newTryCount}/${_CashuSpender.MAX_REFUND_RETRIES}, will retry in ${_CashuSpender.REFUND_RETRY_INTERVAL_MS / 1000}s`);
|
|
85575
|
+
this._startRefundRetryInterval(mintUrl);
|
|
85576
|
+
}
|
|
85577
|
+
results.push({
|
|
85578
|
+
baseUrl,
|
|
85579
|
+
token: xcashuToken.token,
|
|
85580
|
+
success: false,
|
|
85581
|
+
error: fetchResult.error
|
|
85582
|
+
});
|
|
85583
|
+
continue;
|
|
85584
|
+
}
|
|
85553
85585
|
if (!fetchResult.success || !fetchResult.token) {
|
|
85554
85586
|
throw new Error(fetchResult.error || "Failed to fetch refund token from provider");
|
|
85555
85587
|
}
|
|
@@ -85591,6 +85623,31 @@ var CashuSpender = class {
|
|
|
85591
85623
|
}
|
|
85592
85624
|
return results;
|
|
85593
85625
|
}
|
|
85626
|
+
_startRefundRetryInterval(mintUrl) {
|
|
85627
|
+
if (this._refundRetryInterval)
|
|
85628
|
+
return;
|
|
85629
|
+
this._log("DEBUG", `[CashuSpender] Starting refund retry interval (every ${_CashuSpender.REFUND_RETRY_INTERVAL_MS / 1000}s)`);
|
|
85630
|
+
this._refundRetryInterval = setInterval(async () => {
|
|
85631
|
+
const remainingTokens = this.storageAdapter.getXcashuTokens();
|
|
85632
|
+
const hasTokens = Object.values(remainingTokens).some((tokens) => tokens.length > 0);
|
|
85633
|
+
if (!hasTokens) {
|
|
85634
|
+
this._stopRefundRetryInterval();
|
|
85635
|
+
return;
|
|
85636
|
+
}
|
|
85637
|
+
try {
|
|
85638
|
+
await this.refundXcashuTokens(mintUrl);
|
|
85639
|
+
} catch (error53) {
|
|
85640
|
+
this._log("ERROR", `[CashuSpender] Refund retry interval error: ${error53 instanceof Error ? error53.message : String(error53)}`);
|
|
85641
|
+
}
|
|
85642
|
+
}, _CashuSpender.REFUND_RETRY_INTERVAL_MS);
|
|
85643
|
+
}
|
|
85644
|
+
_stopRefundRetryInterval() {
|
|
85645
|
+
if (this._refundRetryInterval) {
|
|
85646
|
+
clearInterval(this._refundRetryInterval);
|
|
85647
|
+
this._refundRetryInterval = null;
|
|
85648
|
+
this._log("DEBUG", `[CashuSpender] Stopped refund retry interval`);
|
|
85649
|
+
}
|
|
85650
|
+
}
|
|
85594
85651
|
async refundProviders(mintUrl, forceRefund) {
|
|
85595
85652
|
const results = [];
|
|
85596
85653
|
const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
|
|
@@ -85898,6 +85955,7 @@ var BalanceManager = class _BalanceManager {
|
|
|
85898
85955
|
return {
|
|
85899
85956
|
success: false,
|
|
85900
85957
|
requestId,
|
|
85958
|
+
status: response.status,
|
|
85901
85959
|
error: `API key refund failed: ${errorData?.detail || responseBody || response.statusText}`
|
|
85902
85960
|
};
|
|
85903
85961
|
}
|
|
@@ -88755,7 +88813,12 @@ var RoutstrClient = class {
|
|
|
88755
88813
|
});
|
|
88756
88814
|
this._log("DEBUG", `[RoutstrClient] _handleErrorResponse: API key refund result: success=${refundResult.success}, message=${refundResult.message}`);
|
|
88757
88815
|
if (!refundResult.success && latestBalanceInfo.amount > 0 && !latestBalanceInfo.balanceUnknown) {
|
|
88758
|
-
|
|
88816
|
+
if (this._isTransientRefundError(refundResult.message)) {
|
|
88817
|
+
this._log("WARN", `[RoutstrClient] _handleErrorResponse: Refund skipped for ${baseUrl} (transient: ${refundResult.message}); failing over to next provider`);
|
|
88818
|
+
tryNextProvider = true;
|
|
88819
|
+
} else {
|
|
88820
|
+
throw new ProviderError(baseUrl, status, refundResult.message ?? "Unknown error");
|
|
88821
|
+
}
|
|
88759
88822
|
}
|
|
88760
88823
|
}
|
|
88761
88824
|
}
|
|
@@ -88803,6 +88866,12 @@ var RoutstrClient = class {
|
|
|
88803
88866
|
}
|
|
88804
88867
|
throw new FailoverError(baseUrl, Array.from(this.providerManager.getFailedProviders()));
|
|
88805
88868
|
}
|
|
88869
|
+
_isTransientRefundError(message) {
|
|
88870
|
+
if (!message)
|
|
88871
|
+
return false;
|
|
88872
|
+
const lower = message.toLowerCase();
|
|
88873
|
+
return lower.includes("ongoing requests for this api key") || lower.includes("cannot refund key");
|
|
88874
|
+
}
|
|
88806
88875
|
async _handlePostResponseBalanceUpdate(params) {
|
|
88807
88876
|
const {
|
|
88808
88877
|
token,
|
|
@@ -105381,6 +105450,7 @@ import { existsSync as existsSync3 } from "fs";
|
|
|
105381
105450
|
|
|
105382
105451
|
// src/start-daemon.ts
|
|
105383
105452
|
var DAEMON_STARTUP_LOCK_PATH = `${CONFIG_DIR}/routstrd-startup.lock`;
|
|
105453
|
+
var DEBUG_LOG_PATH = `${CONFIG_DIR}/debug.log`;
|
|
105384
105454
|
|
|
105385
105455
|
// node_modules/nostr-tools/node_modules/@noble/hashes/utils.js
|
|
105386
105456
|
/*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
|
@@ -114403,6 +114473,12 @@ function makeSdkLogger2(prefix) {
|
|
|
114403
114473
|
};
|
|
114404
114474
|
}
|
|
114405
114475
|
var daemonSdkLogger = makeSdkLogger2();
|
|
114476
|
+
process.on("uncaughtException", (error53) => {
|
|
114477
|
+
logger5.error("UNCAUGHT EXCEPTION:", error53);
|
|
114478
|
+
});
|
|
114479
|
+
process.on("unhandledRejection", (reason) => {
|
|
114480
|
+
logger5.error("UNHANDLED REJECTION:", reason);
|
|
114481
|
+
});
|
|
114406
114482
|
async function main() {
|
|
114407
114483
|
const args = parseArgs(process.argv);
|
|
114408
114484
|
const config2 = await loadDaemonConfig();
|
|
@@ -114483,19 +114559,21 @@ async function main() {
|
|
|
114483
114559
|
let refreshInterval = null;
|
|
114484
114560
|
const startModelRefreshJob = () => {
|
|
114485
114561
|
logger5.log(`Starting recurring model refresh job (every ${REFRESH_INTERVAL_MS / 1000 / 60 / 60} hours)`);
|
|
114486
|
-
refreshInterval = setInterval(
|
|
114487
|
-
|
|
114488
|
-
|
|
114489
|
-
|
|
114490
|
-
|
|
114491
|
-
|
|
114492
|
-
|
|
114493
|
-
|
|
114494
|
-
|
|
114495
|
-
|
|
114496
|
-
|
|
114497
|
-
|
|
114498
|
-
|
|
114562
|
+
refreshInterval = setInterval(() => {
|
|
114563
|
+
(async () => {
|
|
114564
|
+
logger5.log("Running scheduled Nostr event refresh...");
|
|
114565
|
+
try {
|
|
114566
|
+
await modelManager.refreshNostrEvents();
|
|
114567
|
+
} catch (error53) {
|
|
114568
|
+
logger5.error("Scheduled Nostr event refresh failed:", error53);
|
|
114569
|
+
}
|
|
114570
|
+
logger5.log("Running scheduled model refresh...");
|
|
114571
|
+
try {
|
|
114572
|
+
await refreshModelsAndIntegrations(getRoutstr21Models, updatedConfig, "Scheduled");
|
|
114573
|
+
} catch (error53) {
|
|
114574
|
+
logger5.error("Scheduled model refresh failed:", error53);
|
|
114575
|
+
}
|
|
114576
|
+
})().catch((error53) => logger5.error("Model refresh interval escaped:", error53));
|
|
114499
114577
|
}, REFRESH_INTERVAL_MS);
|
|
114500
114578
|
};
|
|
114501
114579
|
const stopModelRefreshJob = () => {
|
|
@@ -114509,34 +114587,36 @@ async function main() {
|
|
|
114509
114587
|
let refundInterval = null;
|
|
114510
114588
|
const startRefundJob = async () => {
|
|
114511
114589
|
logger5.log(`Starting recurring refund job (every ${REFUND_INTERVAL_MS / 1000 / 60} minutes)`);
|
|
114512
|
-
refundInterval = setInterval(
|
|
114513
|
-
|
|
114514
|
-
|
|
114515
|
-
|
|
114516
|
-
|
|
114517
|
-
|
|
114518
|
-
|
|
114519
|
-
|
|
114520
|
-
|
|
114521
|
-
|
|
114522
|
-
|
|
114523
|
-
|
|
114524
|
-
|
|
114525
|
-
|
|
114526
|
-
|
|
114527
|
-
|
|
114528
|
-
|
|
114529
|
-
|
|
114530
|
-
|
|
114531
|
-
|
|
114590
|
+
refundInterval = setInterval(() => {
|
|
114591
|
+
(async () => {
|
|
114592
|
+
logger5.log("Running scheduled refund...");
|
|
114593
|
+
try {
|
|
114594
|
+
const state = store.getState();
|
|
114595
|
+
const pendingDistribution = (state.cachedTokens || []).map((t) => ({
|
|
114596
|
+
baseUrl: t.baseUrl,
|
|
114597
|
+
amount: t.balance || 0
|
|
114598
|
+
}));
|
|
114599
|
+
const apiKeysStored = (state.apiKeys || []).map((k3) => ({
|
|
114600
|
+
baseUrl: k3.baseUrl,
|
|
114601
|
+
amount: k3.balance || 0
|
|
114602
|
+
}));
|
|
114603
|
+
if (pendingDistribution.length === 0 && apiKeysStored.length === 0) {
|
|
114604
|
+
logger5.log("No pending tokens to refund.");
|
|
114605
|
+
return;
|
|
114606
|
+
}
|
|
114607
|
+
const mintUrl = walletAdapter.getActiveMintUrl();
|
|
114608
|
+
if (!mintUrl) {
|
|
114609
|
+
logger5.log("No active mint URL for refund.");
|
|
114610
|
+
return;
|
|
114611
|
+
}
|
|
114612
|
+
const spender = refundClient.getCashuSpender();
|
|
114613
|
+
const results = await spender.refundProviders(mintUrl);
|
|
114614
|
+
const successCount = results.filter((r) => r.success).length;
|
|
114615
|
+
logger5.log(`Scheduled refund completed: ${successCount}/${results.length} providers refunded.`);
|
|
114616
|
+
} catch (error53) {
|
|
114617
|
+
logger5.error("Scheduled refund failed:", error53);
|
|
114532
114618
|
}
|
|
114533
|
-
|
|
114534
|
-
const results = await spender.refundProviders(mintUrl);
|
|
114535
|
-
const successCount = results.filter((r) => r.success).length;
|
|
114536
|
-
logger5.log(`Scheduled refund completed: ${successCount}/${results.length} providers refunded.`);
|
|
114537
|
-
} catch (error53) {
|
|
114538
|
-
logger5.error("Scheduled refund failed:", error53);
|
|
114539
|
-
}
|
|
114619
|
+
})().catch((error53) => logger5.error("Refund interval escaped:", error53));
|
|
114540
114620
|
}, REFUND_INTERVAL_MS);
|
|
114541
114621
|
};
|
|
114542
114622
|
const stopRefundJob = () => {
|
package/dist/index.js
CHANGED
|
@@ -2301,6 +2301,7 @@ async function withCrossProcessLock(lockDir, fn, options = {}) {
|
|
|
2301
2301
|
var init_process_lock = () => {};
|
|
2302
2302
|
|
|
2303
2303
|
// src/start-daemon.ts
|
|
2304
|
+
import { openSync } from "fs";
|
|
2304
2305
|
async function isDaemonHealthy(port) {
|
|
2305
2306
|
const controller = new AbortController;
|
|
2306
2307
|
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
|
@@ -2332,9 +2333,10 @@ async function startDaemonUnlocked(options) {
|
|
|
2332
2333
|
}
|
|
2333
2334
|
const daemonScript = new URL("./daemon/index.js", import.meta.url).pathname;
|
|
2334
2335
|
const shellCmd = `bun run "${daemonScript}" ${args.map((a) => `'${a}'`).join(" ")}`;
|
|
2336
|
+
const debugLogFd = openSync(DEBUG_LOG_PATH, "a");
|
|
2335
2337
|
const proc = Bun.spawn(["sh", "-c", shellCmd], {
|
|
2336
|
-
stdout:
|
|
2337
|
-
stderr:
|
|
2338
|
+
stdout: debugLogFd,
|
|
2339
|
+
stderr: debugLogFd,
|
|
2338
2340
|
stdin: "ignore",
|
|
2339
2341
|
detached: true
|
|
2340
2342
|
});
|
|
@@ -2371,12 +2373,13 @@ async function startDaemon(options = {}) {
|
|
|
2371
2373
|
log: (message) => logger.debug(message)
|
|
2372
2374
|
});
|
|
2373
2375
|
}
|
|
2374
|
-
var DAEMON_STARTUP_LOCK_PATH;
|
|
2376
|
+
var DAEMON_STARTUP_LOCK_PATH, DEBUG_LOG_PATH;
|
|
2375
2377
|
var init_start_daemon = __esm(() => {
|
|
2376
2378
|
init_logger();
|
|
2377
2379
|
init_config();
|
|
2378
2380
|
init_process_lock();
|
|
2379
2381
|
DAEMON_STARTUP_LOCK_PATH = `${CONFIG_DIR}/routstrd-startup.lock`;
|
|
2382
|
+
DEBUG_LOG_PATH = `${CONFIG_DIR}/debug.log`;
|
|
2380
2383
|
});
|
|
2381
2384
|
|
|
2382
2385
|
// node_modules/nostr-tools/node_modules/@noble/hashes/utils.js
|
|
@@ -15596,7 +15599,7 @@ async function isCocodInstalled(cocodPath) {
|
|
|
15596
15599
|
// package.json
|
|
15597
15600
|
var package_default = {
|
|
15598
15601
|
name: "routstrd",
|
|
15599
|
-
version: "0.3.
|
|
15602
|
+
version: "0.3.8",
|
|
15600
15603
|
module: "src/index.ts",
|
|
15601
15604
|
type: "module",
|
|
15602
15605
|
private: false,
|
|
@@ -15620,7 +15623,7 @@ var package_default = {
|
|
|
15620
15623
|
},
|
|
15621
15624
|
dependencies: {
|
|
15622
15625
|
"@cashu/cashu-ts": "^4.3.0",
|
|
15623
|
-
"@routstr/sdk": "^0.3.
|
|
15626
|
+
"@routstr/sdk": "^0.3.15",
|
|
15624
15627
|
"applesauce-core": "^5.1.0",
|
|
15625
15628
|
"applesauce-relay": "^5.1.0",
|
|
15626
15629
|
"applesauce-wallet-connect": "^6.0.0",
|
|
@@ -15667,6 +15670,108 @@ async function installCocodOrExit() {
|
|
|
15667
15670
|
}
|
|
15668
15671
|
console.log("cocod installed successfully.");
|
|
15669
15672
|
}
|
|
15673
|
+
async function restartDaemonsAfterUpdate() {
|
|
15674
|
+
const config = await loadConfig();
|
|
15675
|
+
const isRemote = !!config.daemonUrl;
|
|
15676
|
+
const failures = [];
|
|
15677
|
+
if (isRemote) {
|
|
15678
|
+
console.log(`
|
|
15679
|
+
Using remote daemon \u2014 skipping routstrd daemon restart.`);
|
|
15680
|
+
} else {
|
|
15681
|
+
try {
|
|
15682
|
+
const wasRunning = await isDaemonRunning();
|
|
15683
|
+
if (!wasRunning) {
|
|
15684
|
+
console.log(`
|
|
15685
|
+
routstrd daemon was not running \u2014 skipping restart.`);
|
|
15686
|
+
} else {
|
|
15687
|
+
console.log(`
|
|
15688
|
+
Restarting routstrd daemon...`);
|
|
15689
|
+
await callDaemon("/stop", { method: "POST" });
|
|
15690
|
+
for (let i2 = 0;i2 < 50; i2++) {
|
|
15691
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
15692
|
+
if (!await isDaemonRunning())
|
|
15693
|
+
break;
|
|
15694
|
+
}
|
|
15695
|
+
if (await isDaemonRunning()) {
|
|
15696
|
+
throw new Error("routstrd did not stop within 5 seconds");
|
|
15697
|
+
}
|
|
15698
|
+
console.log("routstrd daemon stopped.");
|
|
15699
|
+
console.log("Starting routstrd daemon...");
|
|
15700
|
+
await startDaemon({
|
|
15701
|
+
port: String(config.port || 8008),
|
|
15702
|
+
provider: config.provider || undefined
|
|
15703
|
+
});
|
|
15704
|
+
console.log("routstrd daemon restarted.");
|
|
15705
|
+
}
|
|
15706
|
+
} catch (error) {
|
|
15707
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
15708
|
+
failures.push(`routstrd daemon: ${msg}`);
|
|
15709
|
+
}
|
|
15710
|
+
}
|
|
15711
|
+
try {
|
|
15712
|
+
const cocodExecutable = resolveCocodExecutable(config.cocodPath);
|
|
15713
|
+
const pingProc = Bun.spawn([cocodExecutable, "ping"], {
|
|
15714
|
+
stdout: "ignore",
|
|
15715
|
+
stderr: "ignore"
|
|
15716
|
+
});
|
|
15717
|
+
const cocodWasRunning = await pingProc.exited === 0;
|
|
15718
|
+
if (!cocodWasRunning) {
|
|
15719
|
+
console.log(`
|
|
15720
|
+
cocod daemon was not running \u2014 skipping restart.`);
|
|
15721
|
+
} else {
|
|
15722
|
+
console.log(`
|
|
15723
|
+
Restarting cocod daemon...`);
|
|
15724
|
+
const stopProc = Bun.spawn([cocodExecutable, "stop"], {
|
|
15725
|
+
stdout: "inherit",
|
|
15726
|
+
stderr: "inherit"
|
|
15727
|
+
});
|
|
15728
|
+
const stopCode = await stopProc.exited;
|
|
15729
|
+
if (stopCode !== 0) {
|
|
15730
|
+
throw new Error(`cocod stop exited with code ${stopCode}`);
|
|
15731
|
+
}
|
|
15732
|
+
console.log("cocod daemon stopped.");
|
|
15733
|
+
const env = { ...process.env };
|
|
15734
|
+
const startProc = Bun.spawn([cocodExecutable, "daemon"], {
|
|
15735
|
+
stdout: "ignore",
|
|
15736
|
+
stderr: "ignore",
|
|
15737
|
+
stdin: "ignore",
|
|
15738
|
+
detached: true,
|
|
15739
|
+
env
|
|
15740
|
+
});
|
|
15741
|
+
startProc.unref?.();
|
|
15742
|
+
let started = false;
|
|
15743
|
+
for (let i2 = 0;i2 < 100; i2++) {
|
|
15744
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
15745
|
+
const poll = Bun.spawn([cocodExecutable, "ping"], {
|
|
15746
|
+
stdout: "ignore",
|
|
15747
|
+
stderr: "ignore"
|
|
15748
|
+
});
|
|
15749
|
+
if (await poll.exited === 0) {
|
|
15750
|
+
started = true;
|
|
15751
|
+
break;
|
|
15752
|
+
}
|
|
15753
|
+
}
|
|
15754
|
+
if (!started) {
|
|
15755
|
+
throw new Error("cocod did not come back up within 10 seconds");
|
|
15756
|
+
}
|
|
15757
|
+
console.log("cocod daemon restarted.");
|
|
15758
|
+
}
|
|
15759
|
+
} catch (error) {
|
|
15760
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
15761
|
+
failures.push(`cocod daemon: ${msg}`);
|
|
15762
|
+
}
|
|
15763
|
+
if (failures.length > 0) {
|
|
15764
|
+
console.error(`
|
|
15765
|
+
\u26A0 Some daemons failed to restart:`);
|
|
15766
|
+
for (const f of failures) {
|
|
15767
|
+
console.error(` - ${f}`);
|
|
15768
|
+
}
|
|
15769
|
+
console.error("The update was applied but may not take effect until daemons are manually restarted.");
|
|
15770
|
+
process.exit(1);
|
|
15771
|
+
}
|
|
15772
|
+
console.log(`
|
|
15773
|
+
\u2713 All daemons restarted successfully.`);
|
|
15774
|
+
}
|
|
15670
15775
|
async function requireLocalDaemon() {
|
|
15671
15776
|
const config = await loadConfig();
|
|
15672
15777
|
if (config.daemonUrl) {
|
|
@@ -15787,6 +15892,7 @@ program.command("update").description("Update routstrd and cocod to the latest v
|
|
|
15787
15892
|
console.log(`cocod updated successfully.
|
|
15788
15893
|
`);
|
|
15789
15894
|
console.log("Both routstrd and cocod have been updated!");
|
|
15895
|
+
await restartDaemonsAfterUpdate();
|
|
15790
15896
|
});
|
|
15791
15897
|
program.command("refund").description("Refund pending tokens and API keys to a specified mint").option("-m, --mint-url <mintUrl>", "Mint URL to refund to (defaults to first mint in wallet)").option("-y, --yes", "Skip confirmation prompt", false).option("--xcashu", "Refund xcashu tokens only (uses refundXcashuTokens)", false).action(async (options) => {
|
|
15792
15898
|
await ensureDaemonRunning();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "routstrd",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"module": "src/index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@cashu/cashu-ts": "^4.3.0",
|
|
27
|
-
"@routstr/sdk": "^0.3.
|
|
27
|
+
"@routstr/sdk": "^0.3.15",
|
|
28
28
|
"applesauce-core": "^5.1.0",
|
|
29
29
|
"applesauce-relay": "^5.1.0",
|
|
30
30
|
"applesauce-wallet-connect": "^6.0.0",
|
package/src/cli.ts
CHANGED
|
@@ -103,6 +103,130 @@ async function installCocodOrExit(): Promise<void> {
|
|
|
103
103
|
console.log("cocod installed successfully.");
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Restart the routstrd and cocod daemons after an update so the new
|
|
108
|
+
* binaries take effect immediately. Failures are collected and reported
|
|
109
|
+
* but never roll back the update itself.
|
|
110
|
+
*/
|
|
111
|
+
async function restartDaemonsAfterUpdate(): Promise<void> {
|
|
112
|
+
const config = await loadConfig();
|
|
113
|
+
const isRemote = !!config.daemonUrl;
|
|
114
|
+
const failures: string[] = [];
|
|
115
|
+
|
|
116
|
+
// --- routstrd daemon ---
|
|
117
|
+
if (isRemote) {
|
|
118
|
+
console.log("\nUsing remote daemon — skipping routstrd daemon restart.");
|
|
119
|
+
} else {
|
|
120
|
+
try {
|
|
121
|
+
const wasRunning = await isDaemonRunning();
|
|
122
|
+
if (!wasRunning) {
|
|
123
|
+
console.log("\nroutstrd daemon was not running — skipping restart.");
|
|
124
|
+
} else {
|
|
125
|
+
console.log("\nRestarting routstrd daemon...");
|
|
126
|
+
|
|
127
|
+
// Graceful stop — the /stop endpoint closes the HTTP server
|
|
128
|
+
// (draining active connections) then exits.
|
|
129
|
+
await callDaemon("/stop", { method: "POST" });
|
|
130
|
+
|
|
131
|
+
for (let i = 0; i < 50; i++) {
|
|
132
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
133
|
+
if (!(await isDaemonRunning())) break;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (await isDaemonRunning()) {
|
|
137
|
+
throw new Error("routstrd did not stop within 5 seconds");
|
|
138
|
+
}
|
|
139
|
+
console.log("routstrd daemon stopped.");
|
|
140
|
+
|
|
141
|
+
console.log("Starting routstrd daemon...");
|
|
142
|
+
await startDaemon({
|
|
143
|
+
port: String(config.port || 8008),
|
|
144
|
+
provider: config.provider || undefined,
|
|
145
|
+
});
|
|
146
|
+
console.log("routstrd daemon restarted.");
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
150
|
+
failures.push(`routstrd daemon: ${msg}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// --- cocod daemon ---
|
|
155
|
+
try {
|
|
156
|
+
const cocodExecutable = resolveCocodExecutable(config.cocodPath);
|
|
157
|
+
|
|
158
|
+
// Check whether cocod is currently running.
|
|
159
|
+
const pingProc = Bun.spawn([cocodExecutable, "ping"], {
|
|
160
|
+
stdout: "ignore",
|
|
161
|
+
stderr: "ignore",
|
|
162
|
+
});
|
|
163
|
+
const cocodWasRunning = (await pingProc.exited) === 0;
|
|
164
|
+
|
|
165
|
+
if (!cocodWasRunning) {
|
|
166
|
+
console.log("\ncocod daemon was not running — skipping restart.");
|
|
167
|
+
} else {
|
|
168
|
+
console.log("\nRestarting cocod daemon...");
|
|
169
|
+
|
|
170
|
+
const stopProc = Bun.spawn([cocodExecutable, "stop"], {
|
|
171
|
+
stdout: "inherit",
|
|
172
|
+
stderr: "inherit",
|
|
173
|
+
});
|
|
174
|
+
const stopCode = await stopProc.exited;
|
|
175
|
+
if (stopCode !== 0) {
|
|
176
|
+
throw new Error(`cocod stop exited with code ${stopCode}`);
|
|
177
|
+
}
|
|
178
|
+
console.log("cocod daemon stopped.");
|
|
179
|
+
|
|
180
|
+
// Start cocod in the background (detached, like the wallet client does).
|
|
181
|
+
const env = { ...process.env };
|
|
182
|
+
const startProc = Bun.spawn([cocodExecutable, "daemon"], {
|
|
183
|
+
stdout: "ignore",
|
|
184
|
+
stderr: "ignore",
|
|
185
|
+
stdin: "ignore",
|
|
186
|
+
detached: true,
|
|
187
|
+
env,
|
|
188
|
+
});
|
|
189
|
+
startProc.unref?.();
|
|
190
|
+
|
|
191
|
+
// Poll until cocod responds to ping (max ~10 s).
|
|
192
|
+
let started = false;
|
|
193
|
+
for (let i = 0; i < 100; i++) {
|
|
194
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
195
|
+
const poll = Bun.spawn([cocodExecutable, "ping"], {
|
|
196
|
+
stdout: "ignore",
|
|
197
|
+
stderr: "ignore",
|
|
198
|
+
});
|
|
199
|
+
if ((await poll.exited) === 0) {
|
|
200
|
+
started = true;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!started) {
|
|
206
|
+
throw new Error("cocod did not come back up within 10 seconds");
|
|
207
|
+
}
|
|
208
|
+
console.log("cocod daemon restarted.");
|
|
209
|
+
}
|
|
210
|
+
} catch (error) {
|
|
211
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
212
|
+
failures.push(`cocod daemon: ${msg}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// --- report ---
|
|
216
|
+
if (failures.length > 0) {
|
|
217
|
+
console.error("\n⚠ Some daemons failed to restart:");
|
|
218
|
+
for (const f of failures) {
|
|
219
|
+
console.error(` - ${f}`);
|
|
220
|
+
}
|
|
221
|
+
console.error(
|
|
222
|
+
"The update was applied but may not take effect until daemons are manually restarted.",
|
|
223
|
+
);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log("\n✓ All daemons restarted successfully.");
|
|
228
|
+
}
|
|
229
|
+
|
|
106
230
|
async function requireLocalDaemon(): Promise<void> {
|
|
107
231
|
const config = await loadConfig();
|
|
108
232
|
if (config.daemonUrl) {
|
|
@@ -264,6 +388,9 @@ program
|
|
|
264
388
|
console.log("cocod updated successfully.\n");
|
|
265
389
|
|
|
266
390
|
console.log("Both routstrd and cocod have been updated!");
|
|
391
|
+
|
|
392
|
+
// Restart daemons so the new binaries take effect immediately.
|
|
393
|
+
await restartDaemonsAfterUpdate();
|
|
267
394
|
});
|
|
268
395
|
|
|
269
396
|
program
|
package/src/start-daemon.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { openSync } from "fs";
|
|
1
2
|
import { logger } from "./utils/logger";
|
|
2
3
|
import { CONFIG_DIR, LOGS_DIR } from "./utils/config";
|
|
3
4
|
import { withCrossProcessLock } from "./utils/process-lock";
|
|
4
5
|
|
|
5
6
|
const DAEMON_STARTUP_LOCK_PATH = `${CONFIG_DIR}/routstrd-startup.lock`;
|
|
7
|
+
const DEBUG_LOG_PATH = `${CONFIG_DIR}/debug.log`;
|
|
6
8
|
|
|
7
9
|
async function isDaemonHealthy(port: string): Promise<boolean> {
|
|
8
10
|
const controller = new AbortController();
|
|
@@ -42,9 +44,11 @@ async function startDaemonUnlocked(
|
|
|
42
44
|
const daemonScript = new URL("./daemon/index.js", import.meta.url).pathname;
|
|
43
45
|
const shellCmd = `bun run "${daemonScript}" ${args.map((a) => `'${a}'`).join(" ")}`;
|
|
44
46
|
|
|
47
|
+
const debugLogFd = openSync(DEBUG_LOG_PATH, "a");
|
|
48
|
+
|
|
45
49
|
const proc = Bun.spawn(["sh", "-c", shellCmd], {
|
|
46
|
-
stdout:
|
|
47
|
-
stderr:
|
|
50
|
+
stdout: debugLogFd,
|
|
51
|
+
stderr: debugLogFd,
|
|
48
52
|
stdin: "ignore",
|
|
49
53
|
detached: true,
|
|
50
54
|
});
|