routstrd 0.3.6 → 0.3.7

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 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.14",
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.14", "", { "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-4YY4ILWMMFTibTgH4TUCf7DNn/Uac5TbsVUgdUEUq8sLQHX25Y2cfYUP43LUThBsgI2mhqW7ZhjDD2zkP0ciug=="],
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
 
@@ -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
- throw new ProviderError(baseUrl, status, refundResult.message ?? "Unknown error");
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,
package/dist/index.js CHANGED
@@ -15596,7 +15596,7 @@ async function isCocodInstalled(cocodPath) {
15596
15596
  // package.json
15597
15597
  var package_default = {
15598
15598
  name: "routstrd",
15599
- version: "0.3.6",
15599
+ version: "0.3.7",
15600
15600
  module: "src/index.ts",
15601
15601
  type: "module",
15602
15602
  private: false,
@@ -15620,7 +15620,7 @@ var package_default = {
15620
15620
  },
15621
15621
  dependencies: {
15622
15622
  "@cashu/cashu-ts": "^4.3.0",
15623
- "@routstr/sdk": "^0.3.14",
15623
+ "@routstr/sdk": "^0.3.15",
15624
15624
  "applesauce-core": "^5.1.0",
15625
15625
  "applesauce-relay": "^5.1.0",
15626
15626
  "applesauce-wallet-connect": "^6.0.0",
@@ -15667,6 +15667,108 @@ async function installCocodOrExit() {
15667
15667
  }
15668
15668
  console.log("cocod installed successfully.");
15669
15669
  }
15670
+ async function restartDaemonsAfterUpdate() {
15671
+ const config = await loadConfig();
15672
+ const isRemote = !!config.daemonUrl;
15673
+ const failures = [];
15674
+ if (isRemote) {
15675
+ console.log(`
15676
+ Using remote daemon \u2014 skipping routstrd daemon restart.`);
15677
+ } else {
15678
+ try {
15679
+ const wasRunning = await isDaemonRunning();
15680
+ if (!wasRunning) {
15681
+ console.log(`
15682
+ routstrd daemon was not running \u2014 skipping restart.`);
15683
+ } else {
15684
+ console.log(`
15685
+ Restarting routstrd daemon...`);
15686
+ await callDaemon("/stop", { method: "POST" });
15687
+ for (let i2 = 0;i2 < 50; i2++) {
15688
+ await new Promise((resolve) => setTimeout(resolve, 100));
15689
+ if (!await isDaemonRunning())
15690
+ break;
15691
+ }
15692
+ if (await isDaemonRunning()) {
15693
+ throw new Error("routstrd did not stop within 5 seconds");
15694
+ }
15695
+ console.log("routstrd daemon stopped.");
15696
+ console.log("Starting routstrd daemon...");
15697
+ await startDaemon({
15698
+ port: String(config.port || 8008),
15699
+ provider: config.provider || undefined
15700
+ });
15701
+ console.log("routstrd daemon restarted.");
15702
+ }
15703
+ } catch (error) {
15704
+ const msg = error instanceof Error ? error.message : String(error);
15705
+ failures.push(`routstrd daemon: ${msg}`);
15706
+ }
15707
+ }
15708
+ try {
15709
+ const cocodExecutable = resolveCocodExecutable(config.cocodPath);
15710
+ const pingProc = Bun.spawn([cocodExecutable, "ping"], {
15711
+ stdout: "ignore",
15712
+ stderr: "ignore"
15713
+ });
15714
+ const cocodWasRunning = await pingProc.exited === 0;
15715
+ if (!cocodWasRunning) {
15716
+ console.log(`
15717
+ cocod daemon was not running \u2014 skipping restart.`);
15718
+ } else {
15719
+ console.log(`
15720
+ Restarting cocod daemon...`);
15721
+ const stopProc = Bun.spawn([cocodExecutable, "stop"], {
15722
+ stdout: "inherit",
15723
+ stderr: "inherit"
15724
+ });
15725
+ const stopCode = await stopProc.exited;
15726
+ if (stopCode !== 0) {
15727
+ throw new Error(`cocod stop exited with code ${stopCode}`);
15728
+ }
15729
+ console.log("cocod daemon stopped.");
15730
+ const env = { ...process.env };
15731
+ const startProc = Bun.spawn([cocodExecutable, "daemon"], {
15732
+ stdout: "ignore",
15733
+ stderr: "ignore",
15734
+ stdin: "ignore",
15735
+ detached: true,
15736
+ env
15737
+ });
15738
+ startProc.unref?.();
15739
+ let started = false;
15740
+ for (let i2 = 0;i2 < 100; i2++) {
15741
+ await new Promise((resolve) => setTimeout(resolve, 100));
15742
+ const poll = Bun.spawn([cocodExecutable, "ping"], {
15743
+ stdout: "ignore",
15744
+ stderr: "ignore"
15745
+ });
15746
+ if (await poll.exited === 0) {
15747
+ started = true;
15748
+ break;
15749
+ }
15750
+ }
15751
+ if (!started) {
15752
+ throw new Error("cocod did not come back up within 10 seconds");
15753
+ }
15754
+ console.log("cocod daemon restarted.");
15755
+ }
15756
+ } catch (error) {
15757
+ const msg = error instanceof Error ? error.message : String(error);
15758
+ failures.push(`cocod daemon: ${msg}`);
15759
+ }
15760
+ if (failures.length > 0) {
15761
+ console.error(`
15762
+ \u26A0 Some daemons failed to restart:`);
15763
+ for (const f of failures) {
15764
+ console.error(` - ${f}`);
15765
+ }
15766
+ console.error("The update was applied but may not take effect until daemons are manually restarted.");
15767
+ process.exit(1);
15768
+ }
15769
+ console.log(`
15770
+ \u2713 All daemons restarted successfully.`);
15771
+ }
15670
15772
  async function requireLocalDaemon() {
15671
15773
  const config = await loadConfig();
15672
15774
  if (config.daemonUrl) {
@@ -15787,6 +15889,7 @@ program.command("update").description("Update routstrd and cocod to the latest v
15787
15889
  console.log(`cocod updated successfully.
15788
15890
  `);
15789
15891
  console.log("Both routstrd and cocod have been updated!");
15892
+ await restartDaemonsAfterUpdate();
15790
15893
  });
15791
15894
  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
15895
  await ensureDaemonRunning();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "routstrd",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
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.14",
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