routstrd 0.1.1 → 0.1.3

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.
@@ -23628,7 +23628,8 @@ async function resolveRouteRequestContext(options) {
23628
23628
  debugLevel,
23629
23629
  mode = "apikeys",
23630
23630
  usageTrackingDriver,
23631
- sdkStore
23631
+ sdkStore,
23632
+ providerManager: providedProviderManager
23632
23633
  } = options;
23633
23634
  let modelManager;
23634
23635
  let providers;
@@ -23648,7 +23649,7 @@ async function resolveRouteRequestContext(options) {
23648
23649
  }
23649
23650
  await modelManager.fetchModels(providers, forceRefresh);
23650
23651
  }
23651
- const providerManager = new ProviderManager(providerRegistry);
23652
+ const providerManager = providedProviderManager ?? new ProviderManager(providerRegistry, sdkStore);
23652
23653
  let baseUrl;
23653
23654
  let selectedModel;
23654
23655
  if (forcedProvider) {
@@ -23683,7 +23684,7 @@ async function resolveRouteRequestContext(options) {
23683
23684
  if (!mintUrl) {
23684
23685
  throw new Error("No mint configured in wallet");
23685
23686
  }
23686
- const client2 = new RoutstrClient(walletAdapter, storageAdapter, providerRegistry, "min", mode, { usageTrackingDriver, sdkStore });
23687
+ const client2 = new RoutstrClient(walletAdapter, storageAdapter, providerRegistry, "min", mode, { usageTrackingDriver, sdkStore, providerManager });
23687
23688
  if (debugLevel) {
23688
23689
  client2.setDebugLevel(debugLevel);
23689
23690
  }
@@ -24066,7 +24067,7 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
24066
24067
  }
24067
24068
  }
24068
24069
  const DEFAULT_RELAYS = [
24069
- "wss://relay.primal.net",
24070
+ "wss://relay.damus.io",
24070
24071
  "wss://nos.lol",
24071
24072
  "wss://relay.routstr.com"
24072
24073
  ];
@@ -24559,7 +24560,14 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
24559
24560
  continue;
24560
24561
  for (const xcashuToken of tokens) {
24561
24562
  try {
24562
- const receiveResult = await this.receiveToken(xcashuToken.token);
24563
+ if (!this.balanceManager) {
24564
+ throw new Error("BalanceManager not available for xcashu refund");
24565
+ }
24566
+ const fetchResult = await this.balanceManager.fetchRefundToken(baseUrl, xcashuToken.token, true);
24567
+ if (!fetchResult.success || !fetchResult.token) {
24568
+ throw new Error(fetchResult.error || "Failed to fetch refund token from provider");
24569
+ }
24570
+ const receiveResult = await this.receiveToken(fetchResult.token);
24563
24571
  if (receiveResult.success) {
24564
24572
  this.storageAdapter.removeXcashuToken(baseUrl, xcashuToken.token);
24565
24573
  results.push({
@@ -24578,7 +24586,7 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
24578
24586
  success: false,
24579
24587
  error: receiveResult.message ?? "Refund failed"
24580
24588
  });
24581
- this._log("DEBUG", `[CashuSpender] refundXcashuTokens: Failed to refund xcashu token for ${baseUrl}, incremented tryCount to ${newTryCount}`);
24589
+ this._log("DEBUG", `[CashuSpender] refundXcashuTokens: Failed to receive refund token for ${baseUrl}, incremented tryCount to ${newTryCount}`);
24582
24590
  }
24583
24591
  } catch (error) {
24584
24592
  const currentTryCount = xcashuToken.tryCount ?? 0;
@@ -24727,7 +24735,7 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
24727
24735
  }
24728
24736
  let fetchResult;
24729
24737
  try {
24730
- fetchResult = await this._fetchRefundTokenWithApiKey(baseUrl, apiKey);
24738
+ fetchResult = await this.fetchRefundToken(baseUrl, apiKey);
24731
24739
  if (!fetchResult.success) {
24732
24740
  return {
24733
24741
  success: false,
@@ -24760,7 +24768,7 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
24760
24768
  return this._handleRefundError(error, mintUrl, fetchResult?.requestId);
24761
24769
  }
24762
24770
  }
24763
- async _fetchRefundTokenWithApiKey(baseUrl, apiKey) {
24771
+ async fetchRefundToken(baseUrl, apiKeyOrToken, xCashu = false) {
24764
24772
  if (!baseUrl) {
24765
24773
  return {
24766
24774
  success: false,
@@ -24774,12 +24782,17 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
24774
24782
  controller.abort();
24775
24783
  }, 60000);
24776
24784
  try {
24785
+ const headers = {
24786
+ "Content-Type": "application/json"
24787
+ };
24788
+ if (xCashu) {
24789
+ headers["X-Cashu"] = apiKeyOrToken;
24790
+ } else {
24791
+ headers["Authorization"] = `Bearer ${apiKeyOrToken}`;
24792
+ }
24777
24793
  const response = await fetch(url2, {
24778
24794
  method: "POST",
24779
- headers: {
24780
- Authorization: `Bearer ${apiKey}`,
24781
- "Content-Type": "application/json"
24782
- },
24795
+ headers,
24783
24796
  signal: controller.signal
24784
24797
  });
24785
24798
  clearTimeout(timeoutId);
@@ -24800,7 +24813,7 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
24800
24813
  };
24801
24814
  } catch (error) {
24802
24815
  clearTimeout(timeoutId);
24803
- console.error("[BalanceManager._fetchRefundTokenWithApiKey] Fetch error", error);
24816
+ console.error("[BalanceManager.fetchRefundToken] Fetch error", error);
24804
24817
  if (error instanceof Error) {
24805
24818
  if (error.name === "AbortError") {
24806
24819
  return {
@@ -25141,7 +25154,7 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
25141
25154
  console.log(response.status);
25142
25155
  const data = await response.json();
25143
25156
  console.log("FAILED ", data);
25144
- const isInvalidApiKey = response.status === 401 && data?.code === "invalid_api_key" && data?.message?.includes("proofs already spent");
25157
+ const isInvalidApiKey = response.status === 401 && data?.detail?.error?.code === "invalid_api_key" && data?.detail?.error?.message?.includes("proofs already spent");
25145
25158
  return {
25146
25159
  amount: -1,
25147
25160
  reserved: data.reserved ?? 0,
@@ -25445,23 +25458,59 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
25445
25458
  const filtered = normalized.filter((value) => torMode ? true : !isOnionUrl(value));
25446
25459
  return dedupePreserveOrder(filtered.filter((value) => shouldAllowHttp(value, torMode)));
25447
25460
  }, ProviderManager = class _ProviderManager {
25448
- constructor(providerRegistry) {
25461
+ constructor(providerRegistry, store) {
25449
25462
  this.providerRegistry = providerRegistry;
25463
+ this.instanceId = `pm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
25464
+ if (store) {
25465
+ this.store = store;
25466
+ this.hydrateFromStore();
25467
+ }
25450
25468
  }
25451
25469
  failedProviders = /* @__PURE__ */ new Set;
25452
25470
  lastFailed = /* @__PURE__ */ new Map;
25453
25471
  providersOnCoolDown = [];
25454
25472
  static COOLDOWN_DURATION_MS = 300000;
25473
+ store = null;
25474
+ instanceId;
25475
+ hydrateFromStore() {
25476
+ if (!this.store)
25477
+ return;
25478
+ const state = this.store.getState();
25479
+ this.failedProviders = new Set(state.failedProviders);
25480
+ this.lastFailed = new Map(Object.entries(state.lastFailed));
25481
+ const now2 = Date.now();
25482
+ this.providersOnCoolDown = state.providersOnCooldown.filter((entry) => now2 - entry.timestamp < _ProviderManager.COOLDOWN_DURATION_MS).map((entry) => [entry.baseUrl, entry.timestamp]);
25483
+ console.log(`[ProviderManager:${this.instanceId}] Hydrated from store:`);
25484
+ console.log(` failedProviders: ${this.failedProviders.size}`);
25485
+ console.log(` lastFailed: ${this.lastFailed.size}`);
25486
+ console.log(` providersOnCooldown: ${this.providersOnCoolDown.length}`);
25487
+ }
25488
+ getInstanceId() {
25489
+ return this.instanceId;
25490
+ }
25455
25491
  cleanupExpiredCooldowns() {
25456
25492
  const now2 = Date.now();
25457
- this.providersOnCoolDown = this.providersOnCoolDown.filter(([, timestamp]) => now2 - timestamp < _ProviderManager.COOLDOWN_DURATION_MS);
25493
+ const before = this.providersOnCoolDown.length;
25494
+ this.providersOnCoolDown = this.providersOnCoolDown.filter(([url2, timestamp]) => {
25495
+ const age = now2 - timestamp;
25496
+ const isExpired = age >= _ProviderManager.COOLDOWN_DURATION_MS;
25497
+ if (isExpired) {
25498
+ console.log(`[cleanupExpiredCooldowns:${this.instanceId}] Removing expired cooldown for ${url2} (age: ${age}ms, cooldown: ${_ProviderManager.COOLDOWN_DURATION_MS}ms)`);
25499
+ }
25500
+ return !isExpired;
25501
+ });
25502
+ const after = this.providersOnCoolDown.length;
25503
+ if (before !== after) {
25504
+ console.log(`[cleanupExpiredCooldowns:${this.instanceId}] Cleaned up ${before - after} expired cooldown(s), ${after} remaining`);
25505
+ }
25458
25506
  }
25459
25507
  getCooldownDurationMs() {
25460
25508
  return _ProviderManager.COOLDOWN_DURATION_MS;
25461
25509
  }
25462
25510
  isOnCooldown(baseUrl) {
25463
25511
  this.cleanupExpiredCooldowns();
25464
- return this.providersOnCoolDown.some(([url2]) => url2 === baseUrl);
25512
+ const result = this.providersOnCoolDown.some(([url2]) => url2 === baseUrl);
25513
+ return result;
25465
25514
  }
25466
25515
  getProvidersOnCooldown() {
25467
25516
  this.cleanupExpiredCooldowns();
@@ -25469,6 +25518,9 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
25469
25518
  }
25470
25519
  resetFailedProviders() {
25471
25520
  this.failedProviders.clear();
25521
+ if (this.store) {
25522
+ this.store.getState().setFailedProviders([]);
25523
+ }
25472
25524
  }
25473
25525
  getLastFailed(baseUrl) {
25474
25526
  return this.lastFailed.get(baseUrl);
@@ -25479,23 +25531,59 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
25479
25531
  markFailed(baseUrl) {
25480
25532
  const now2 = Date.now();
25481
25533
  const lastFailure = this.lastFailed.get(baseUrl);
25534
+ console.log(`[markFailed:${this.instanceId}] baseUrl: ${baseUrl}`);
25535
+ console.log(`[markFailed:${this.instanceId}] lastFailure from map: ${lastFailure}`);
25536
+ console.log(`[markFailed:${this.instanceId}] current timestamp (now): ${now2}`);
25537
+ console.log(`[markFailed:${this.instanceId}] COOLDOWN_DURATION_MS: ${_ProviderManager.COOLDOWN_DURATION_MS}`);
25538
+ if (lastFailure !== undefined) {
25539
+ const timeSinceLastFailure = now2 - lastFailure;
25540
+ console.log(`[markFailed:${this.instanceId}] timeSinceLastFailure: ${timeSinceLastFailure}ms`);
25541
+ console.log(`[markFailed:${this.instanceId}] isWithinCooldownWindow: ${timeSinceLastFailure < _ProviderManager.COOLDOWN_DURATION_MS}`);
25542
+ }
25482
25543
  this.lastFailed.set(baseUrl, now2);
25483
25544
  this.failedProviders.add(baseUrl);
25545
+ if (this.store) {
25546
+ this.store.getState().setLastFailedTimestamp(baseUrl, now2);
25547
+ this.store.getState().addFailedProvider(baseUrl);
25548
+ }
25549
+ console.log(`[markFailed:${this.instanceId}] Updated lastFailed map for ${baseUrl} to ${now2}`);
25550
+ console.log(`[markFailed:${this.instanceId}] failedProviders set size: ${this.failedProviders.size}`);
25484
25551
  if (lastFailure !== undefined && now2 - lastFailure < _ProviderManager.COOLDOWN_DURATION_MS) {
25552
+ console.log(`[markFailed:${this.instanceId}] Second failure detected within cooldown window for ${baseUrl}`);
25485
25553
  if (!this.isOnCooldown(baseUrl)) {
25486
25554
  this.providersOnCoolDown.push([baseUrl, now2]);
25487
- console.log(`Provider ${baseUrl} added to cooldown after second failure within 5 minutes`);
25555
+ if (this.store) {
25556
+ this.store.getState().addProviderOnCooldown(baseUrl, now2);
25557
+ }
25558
+ console.log(`[markFailed:${this.instanceId}] Provider ${baseUrl} added to cooldown after second failure within 5 minutes`);
25559
+ } else {
25560
+ console.log(`[markFailed:${this.instanceId}] Provider ${baseUrl} is already on cooldown`);
25561
+ }
25562
+ } else {
25563
+ if (lastFailure === undefined) {
25564
+ console.log(`[markFailed:${this.instanceId}] First failure for ${baseUrl} - not adding to cooldown yet`);
25565
+ } else {
25566
+ console.log(`[markFailed:${this.instanceId}] Failure outside cooldown window for ${baseUrl} (timeSinceLastFailure: ${now2 - lastFailure}ms)`);
25488
25567
  }
25489
25568
  }
25490
25569
  }
25491
25570
  removeFromCooldown(baseUrl) {
25492
25571
  this.providersOnCoolDown = this.providersOnCoolDown.filter(([url2]) => url2 !== baseUrl);
25572
+ if (this.store) {
25573
+ this.store.getState().removeProviderFromCooldown(baseUrl);
25574
+ }
25493
25575
  }
25494
25576
  clearCooldowns() {
25495
25577
  this.providersOnCoolDown = [];
25578
+ if (this.store) {
25579
+ this.store.getState().clearProvidersOnCooldown();
25580
+ }
25496
25581
  }
25497
25582
  clearFailureHistory() {
25498
25583
  this.lastFailed.clear();
25584
+ if (this.store) {
25585
+ this.store.getState().setLastFailed({});
25586
+ }
25499
25587
  }
25500
25588
  hasFailed(baseUrl) {
25501
25589
  return this.failedProviders.has(baseUrl);
@@ -26432,6 +26520,9 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
26432
26520
  lastRoutstr21ModelsUpdate: null,
26433
26521
  cachedReceiveTokens: [],
26434
26522
  clientIds: [],
26523
+ failedProviders: [],
26524
+ lastFailed: {},
26525
+ providersOnCooldown: [],
26435
26526
  setModelsFromAllProviders: (value) => {
26436
26527
  const normalized = {};
26437
26528
  for (const [baseUrl, models] of Object.entries(value)) {
@@ -26561,6 +26652,70 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
26561
26652
  driver.setItem(SDK_STORAGE_KEYS.CLIENT_IDS, normalized);
26562
26653
  return { clientIds: normalized };
26563
26654
  });
26655
+ },
26656
+ setFailedProviders: (value) => {
26657
+ const normalized = value.map((url2) => normalizeBaseUrl5(url2));
26658
+ driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, normalized);
26659
+ set({ failedProviders: normalized });
26660
+ },
26661
+ addFailedProvider: (baseUrl) => {
26662
+ const normalized = normalizeBaseUrl5(baseUrl);
26663
+ const current = get().failedProviders;
26664
+ if (!current.includes(normalized)) {
26665
+ const updated = [...current, normalized];
26666
+ driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, updated);
26667
+ set({ failedProviders: updated });
26668
+ }
26669
+ },
26670
+ removeFailedProvider: (baseUrl) => {
26671
+ const normalized = normalizeBaseUrl5(baseUrl);
26672
+ const current = get().failedProviders;
26673
+ const updated = current.filter((url2) => url2 !== normalized);
26674
+ driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, updated);
26675
+ set({ failedProviders: updated });
26676
+ },
26677
+ setLastFailed: (value) => {
26678
+ const normalized = {};
26679
+ for (const [baseUrl, timestamp] of Object.entries(value)) {
26680
+ normalized[normalizeBaseUrl5(baseUrl)] = timestamp;
26681
+ }
26682
+ driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, normalized);
26683
+ set({ lastFailed: normalized });
26684
+ },
26685
+ setLastFailedTimestamp: (baseUrl, timestamp) => {
26686
+ const normalized = normalizeBaseUrl5(baseUrl);
26687
+ const current = get().lastFailed;
26688
+ const updated = { ...current, [normalized]: timestamp };
26689
+ driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, updated);
26690
+ set({ lastFailed: updated });
26691
+ },
26692
+ setProvidersOnCooldown: (value) => {
26693
+ const normalized = value.map((entry) => ({
26694
+ baseUrl: normalizeBaseUrl5(entry.baseUrl),
26695
+ timestamp: entry.timestamp
26696
+ }));
26697
+ driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, normalized);
26698
+ set({ providersOnCooldown: normalized });
26699
+ },
26700
+ addProviderOnCooldown: (baseUrl, timestamp) => {
26701
+ const normalized = normalizeBaseUrl5(baseUrl);
26702
+ const current = get().providersOnCooldown;
26703
+ if (!current.some((entry) => entry.baseUrl === normalized)) {
26704
+ const updated = [...current, { baseUrl: normalized, timestamp }];
26705
+ driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, updated);
26706
+ set({ providersOnCooldown: updated });
26707
+ }
26708
+ },
26709
+ removeProviderFromCooldown: (baseUrl) => {
26710
+ const normalized = normalizeBaseUrl5(baseUrl);
26711
+ const current = get().providersOnCooldown;
26712
+ const updated = current.filter((entry) => entry.baseUrl !== normalized);
26713
+ driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, updated);
26714
+ set({ providersOnCooldown: updated });
26715
+ },
26716
+ clearProvidersOnCooldown: () => {
26717
+ driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, []);
26718
+ set({ providersOnCooldown: [] });
26564
26719
  }
26565
26720
  })), hydrateStoreFromDriver = async (store, driver) => {
26566
26721
  const [
@@ -26578,7 +26733,10 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
26578
26733
  rawRoutstr21Models,
26579
26734
  rawLastRoutstr21ModelsUpdate,
26580
26735
  rawCachedReceiveTokens,
26581
- rawClientIds
26736
+ rawClientIds,
26737
+ rawFailedProviders,
26738
+ rawLastFailed,
26739
+ rawProvidersOnCooldown
26582
26740
  ] = await Promise.all([
26583
26741
  driver.getItem(SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS, {}),
26584
26742
  driver.getItem(SDK_STORAGE_KEYS.LAST_USED_MODEL, null),
@@ -26594,7 +26752,10 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
26594
26752
  driver.getItem(SDK_STORAGE_KEYS.ROUTSTR21_MODELS, []),
26595
26753
  driver.getItem(SDK_STORAGE_KEYS.LAST_ROUTSTR21_MODELS_UPDATE, null),
26596
26754
  driver.getItem(SDK_STORAGE_KEYS.CACHED_RECEIVE_TOKENS, []),
26597
- driver.getItem(SDK_STORAGE_KEYS.CLIENT_IDS, [])
26755
+ driver.getItem(SDK_STORAGE_KEYS.CLIENT_IDS, []),
26756
+ driver.getItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, []),
26757
+ driver.getItem(SDK_STORAGE_KEYS.LAST_FAILED, {}),
26758
+ driver.getItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, [])
26598
26759
  ]);
26599
26760
  const modelsFromAllProviders = Object.fromEntries(Object.entries(rawModels).map(([baseUrl, models]) => [
26600
26761
  normalizeBaseUrl5(baseUrl),
@@ -26650,6 +26811,15 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
26650
26811
  createdAt: entry.createdAt ?? Date.now(),
26651
26812
  lastUsed: entry.lastUsed ?? null
26652
26813
  }));
26814
+ const failedProviders = rawFailedProviders.map((url2) => normalizeBaseUrl5(url2));
26815
+ const lastFailed = Object.fromEntries(Object.entries(rawLastFailed).map(([baseUrl, timestamp]) => [
26816
+ normalizeBaseUrl5(baseUrl),
26817
+ timestamp
26818
+ ]));
26819
+ const providersOnCooldown = rawProvidersOnCooldown.map((entry) => ({
26820
+ baseUrl: normalizeBaseUrl5(entry.baseUrl),
26821
+ timestamp: entry.timestamp
26822
+ }));
26653
26823
  store.setState({
26654
26824
  modelsFromAllProviders,
26655
26825
  lastUsedModel,
@@ -26665,7 +26835,10 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
26665
26835
  routstr21Models,
26666
26836
  lastRoutstr21ModelsUpdate,
26667
26837
  cachedReceiveTokens,
26668
- clientIds
26838
+ clientIds,
26839
+ failedProviders,
26840
+ lastFailed,
26841
+ providersOnCooldown
26669
26842
  });
26670
26843
  }, createSdkStore = ({
26671
26844
  driver
@@ -26974,11 +27147,11 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
26974
27147
  this.balanceManager = new BalanceManager(walletAdapter, storageAdapter, providerRegistry);
26975
27148
  this.cashuSpender = new CashuSpender(walletAdapter, storageAdapter, providerRegistry, this.balanceManager);
26976
27149
  this.streamProcessor = new StreamProcessor;
26977
- this.providerManager = new ProviderManager(providerRegistry);
26978
27150
  this.alertLevel = alertLevel;
26979
27151
  this.mode = mode;
26980
27152
  this.usageTrackingDriver = options.usageTrackingDriver;
26981
27153
  this.sdkStore = options.sdkStore;
27154
+ this.providerManager = options.providerManager ?? new ProviderManager(providerRegistry, this.sdkStore);
26982
27155
  }
26983
27156
  cashuSpender;
26984
27157
  balanceManager;
@@ -27395,6 +27568,7 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
27395
27568
  const currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1000 : currentBalanceInfo.amount;
27396
27569
  const shortfall = Math.max(0, params.requiredSats - currentBalance);
27397
27570
  topupAmount = shortfall > 0 ? shortfall : params.requiredSats;
27571
+ this._log("DEBUG", `The shortfall is: ${shortfall}. requiredSats: ${params.requiredSats}. Current Balance: ${currentBalance} `);
27398
27572
  } catch (e) {
27399
27573
  this._log("WARN", "Could not get current token balance for topup calculation:", e);
27400
27574
  }
@@ -27477,12 +27651,20 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
27477
27651
  tryNextProvider = true;
27478
27652
  }
27479
27653
  }
27654
+ if (status === 401 && this.mode === "apikeys") {
27655
+ this._log("DEBUG", `[RoutstrClient] _handleErrorResponse: Checking balance for ${baseUrl}, key preview=${token}`);
27656
+ const latestBalanceInfo = await this.balanceManager.getTokenBalance(token, baseUrl);
27657
+ if (latestBalanceInfo.isInvalidApiKey) {
27658
+ this.storageAdapter.removeApiKey(baseUrl);
27659
+ tryNextProvider = true;
27660
+ }
27661
+ }
27480
27662
  if ((status === 401 || status === 403 || status === 413 || status === 400 || status === 500 || status === 502 || status === 503 || status === 504 || status === 521) && !tryNextProvider) {
27481
27663
  this._log("DEBUG", `[RoutstrClient] _handleErrorResponse: Status ${status} (auth/server error), attempting refund for ${baseUrl}, mode=${this.mode}`);
27482
27664
  if (this.mode === "apikeys") {
27483
27665
  this._log("DEBUG", `[RoutstrClient] _handleErrorResponse: Attempting API key refund for ${baseUrl}, key preview=${token}`);
27484
- const initialBalance = await this.balanceManager.getTokenBalance(token, baseUrl);
27485
- this._log("DEBUG", `[RoutstrClient] _handleErrorResponse: Initial API key balance: ${initialBalance.amount}`);
27666
+ const latestBalanceInfo = await this.balanceManager.getTokenBalance(token, baseUrl);
27667
+ this._log("DEBUG", `[RoutstrClient] _handleErrorResponse: Initial API key balance: ${latestBalanceInfo.amount}`);
27486
27668
  const refundResult = await this.balanceManager.refundApiKey({
27487
27669
  mintUrl,
27488
27670
  baseUrl,
@@ -27490,7 +27672,7 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
27490
27672
  forceRefund: true
27491
27673
  });
27492
27674
  this._log("DEBUG", `[RoutstrClient] _handleErrorResponse: API key refund result: success=${refundResult.success}, message=${refundResult.message}`);
27493
- if (!refundResult.success && initialBalance.amount > 0) {
27675
+ if (!refundResult.success && latestBalanceInfo.amount > 0) {
27494
27676
  throw new ProviderError(baseUrl, status, refundResult.message ?? "Unknown error");
27495
27677
  }
27496
27678
  }
@@ -27585,7 +27767,6 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
27585
27767
  try {
27586
27768
  const xcashuResults = await this.cashuSpender.refundXcashuTokens(mintUrl);
27587
27769
  this._log("DEBUG", "Refund xcashu tokens results:", xcashuResults);
27588
- const results = await this.cashuSpender.refundProviders(mintUrl);
27589
27770
  } catch (error) {
27590
27771
  this._log("ERROR", "Failed to refund providers:", error);
27591
27772
  }
@@ -27986,7 +28167,10 @@ var init_dist3 = __esm(() => {
27986
28167
  LAST_ROUTSTR21_MODELS_UPDATE: "lastRoutstr21ModelsUpdate",
27987
28168
  CACHED_RECEIVE_TOKENS: "cached_receive_tokens",
27988
28169
  USAGE_TRACKING: "usage_tracking",
27989
- CLIENT_IDS: "client_ids"
28170
+ CLIENT_IDS: "client_ids",
28171
+ FAILED_PROVIDERS: "failed_providers",
28172
+ LAST_FAILED: "last_failed",
28173
+ PROVIDERS_ON_COOLDOWN: "providers_on_cooldown"
27990
28174
  };
27991
28175
  isBrowser2 = typeof indexedDB !== "undefined";
27992
28176
  });
@@ -27994,7 +28178,7 @@ var init_dist3 = __esm(() => {
27994
28178
  // src/daemon/index.ts
27995
28179
  init_dist3();
27996
28180
  import { createServer } from "http";
27997
- import { existsSync as existsSync3 } from "fs";
28181
+ import { existsSync as existsSync6 } from "fs";
27998
28182
 
27999
28183
  // src/utils/config.ts
28000
28184
  var HOME = process.env.HOME || process.env.USERPROFILE || "";
@@ -28003,7 +28187,7 @@ var SOCKET_PATH = process.env.ROUTSTRD_SOCKET || `${CONFIG_DIR}/routstrd.sock`;
28003
28187
  var PID_FILE = process.env.ROUTSTRD_PID || `${CONFIG_DIR}/routstrd.pid`;
28004
28188
  var DB_PATH = `${CONFIG_DIR}/routstr.db`;
28005
28189
  var CONFIG_FILE = `${CONFIG_DIR}/config.json`;
28006
- var LOG_FILE = `${CONFIG_DIR}/routstrd.log`;
28190
+ var LOGS_DIR = `${CONFIG_DIR}/logs`;
28007
28191
  var DEFAULT_CONFIG = {
28008
28192
  port: 8008,
28009
28193
  provider: null,
@@ -28017,10 +28201,16 @@ import { existsSync } from "fs";
28017
28201
  import { join as join2 } from "path";
28018
28202
  var HOME2 = process.env.HOME || process.env.USERPROFILE || "";
28019
28203
  var LOG_DIR = process.env.ROUTSTRD_DIR || `${HOME2}/.routstrd`;
28020
- var LOG_FILE2 = join2(LOG_DIR, "routstrd.log");
28204
+ var LOGS_DIR2 = join2(LOG_DIR, "logs");
28205
+ function getLogFileForDate(date = new Date) {
28206
+ const year = date.getFullYear();
28207
+ const month = String(date.getMonth() + 1).padStart(2, "0");
28208
+ const day = String(date.getDate()).padStart(2, "0");
28209
+ return join2(LOGS_DIR2, `${year}-${month}-${day}.log`);
28210
+ }
28021
28211
  async function ensureLogDir() {
28022
- if (!existsSync(LOG_DIR)) {
28023
- await mkdir(LOG_DIR, { recursive: true });
28212
+ if (!existsSync(LOGS_DIR2)) {
28213
+ await mkdir(LOGS_DIR2, { recursive: true });
28024
28214
  }
28025
28215
  }
28026
28216
  async function writeLog(level, ...args) {
@@ -28042,8 +28232,9 @@ ${a.stack}` : ""}`;
28042
28232
  }).join(" ");
28043
28233
  const line = `[${timestamp}] [${level}] ${message}
28044
28234
  `;
28235
+ const logFile = getLogFileForDate(new Date(timestamp));
28045
28236
  try {
28046
- await appendFile(LOG_FILE2, line);
28237
+ await appendFile(logFile, line);
28047
28238
  } catch (error) {
28048
28239
  console.error("Failed to write log:", error);
28049
28240
  }
@@ -28053,6 +28244,9 @@ var logger3 = {
28053
28244
  console.log(...args);
28054
28245
  writeLog("INFO", ...args);
28055
28246
  },
28247
+ debug: (...args) => {
28248
+ writeLog("DEBUG", ...args);
28249
+ },
28056
28250
  error: (...args) => {
28057
28251
  console.error(...args);
28058
28252
  writeLog("ERROR", ...args);
@@ -28159,7 +28353,10 @@ var SDK_STORAGE_KEYS2 = {
28159
28353
  LAST_ROUTSTR21_MODELS_UPDATE: "lastRoutstr21ModelsUpdate",
28160
28354
  CACHED_RECEIVE_TOKENS: "cached_receive_tokens",
28161
28355
  USAGE_TRACKING: "usage_tracking",
28162
- CLIENT_IDS: "client_ids"
28356
+ CLIENT_IDS: "client_ids",
28357
+ FAILED_PROVIDERS: "failed_providers",
28358
+ LAST_FAILED: "last_failed",
28359
+ PROVIDERS_ON_COOLDOWN: "providers_on_cooldown"
28163
28360
  };
28164
28361
  var MIGRATION_MARKER_KEY32 = "usage_tracking_migration_v1";
28165
28362
  var normalizeBaseUrl32 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
@@ -28315,9 +28512,6 @@ var createBunSqliteUsageTrackingDriver2 = (options = {}) => {
28315
28512
  };
28316
28513
  };
28317
28514
 
28318
- // src/daemon/wallet/index.ts
28319
- import { spawn } from "child_process";
28320
-
28321
28515
  // node_modules/@scure/base/index.js
28322
28516
  /*! scure-base - MIT License (c) 2022 Paul Miller (paulmillr.com) */
28323
28517
  function isBytes3(a) {
@@ -33719,91 +33913,232 @@ function Ir(s) {
33719
33913
  return `authA${Re(t)}`;
33720
33914
  }
33721
33915
 
33722
- // src/daemon/wallet/index.ts
33723
- async function runWalletCommand(args) {
33724
- return new Promise((resolve, reject) => {
33725
- const child = spawn("cocod", args, {
33726
- stdio: ["ignore", "pipe", "pipe"]
33727
- });
33728
- let stdout = "";
33729
- let stderr = "";
33730
- child.stdout.on("data", (chunk) => {
33731
- stdout += chunk.toString();
33916
+ // src/daemon/wallet/cocod-client.ts
33917
+ import { createHash } from "crypto";
33918
+ var DEFAULT_CONFIG_DIR = `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`;
33919
+ var DEFAULT_SOCKET_PATH = process.env.COCOD_SOCKET || `${DEFAULT_CONFIG_DIR}/cocod.sock`;
33920
+
33921
+ class CocodHttpError extends Error {
33922
+ status;
33923
+ constructor(status, message) {
33924
+ super(message);
33925
+ this.name = "CocodHttpError";
33926
+ this.status = status;
33927
+ }
33928
+ }
33929
+ function resolveCocodExecutable(cocodPath) {
33930
+ const trimmed = cocodPath?.trim();
33931
+ return trimmed || "cocod";
33932
+ }
33933
+ function normalizeBalances(output4) {
33934
+ if (!output4)
33935
+ return {};
33936
+ return Object.fromEntries(Object.entries(output4).map(([mintUrl, value]) => {
33937
+ if (typeof value === "number") {
33938
+ return [mintUrl, value];
33939
+ }
33940
+ return [mintUrl, Number(value?.sats ?? 0)];
33941
+ }));
33942
+ }
33943
+ function parseMintList(output4) {
33944
+ return (output4 || "").split(`
33945
+ `).map((line) => line.trim()).filter(Boolean);
33946
+ }
33947
+ function delay(ms2) {
33948
+ return new Promise((resolve) => setTimeout(resolve, ms2));
33949
+ }
33950
+ function toErrorText(value) {
33951
+ if (typeof value === "string") {
33952
+ return value.trim();
33953
+ }
33954
+ if (value === null || value === undefined) {
33955
+ return "";
33956
+ }
33957
+ try {
33958
+ return JSON.stringify(value);
33959
+ } catch {
33960
+ return String(value);
33961
+ }
33962
+ }
33963
+ function tokenFingerprint(token) {
33964
+ return createHash("sha256").update(token).digest("hex").slice(0, 12);
33965
+ }
33966
+ function createCocodClient(options = {}) {
33967
+ const executable = resolveCocodExecutable(options.cocodPath);
33968
+ const socketPath = options.socketPath || DEFAULT_SOCKET_PATH;
33969
+ const fetchImpl = options.fetchImpl || fetch;
33970
+ const pollIntervalMs = options.pollIntervalMs ?? 100;
33971
+ const startupTimeoutMs = options.startupTimeoutMs ?? 5000;
33972
+ const spawnDaemon = options.spawnDaemon || ((args, env) => {
33973
+ const proc = Bun.spawn(args, {
33974
+ stdin: "ignore",
33975
+ stdout: "ignore",
33976
+ stderr: "ignore",
33977
+ detached: true,
33978
+ env
33732
33979
  });
33733
- child.stderr.on("data", (chunk) => {
33734
- stderr += chunk.toString();
33980
+ proc.unref();
33981
+ return proc;
33982
+ });
33983
+ let startPromise = null;
33984
+ async function fetchJson(path, init = {}) {
33985
+ const method = init.method || "GET";
33986
+ const requestInit = {
33987
+ ...init,
33988
+ unix: socketPath
33989
+ };
33990
+ const response = await fetchImpl(`http://localhost${path}`, requestInit);
33991
+ const rawText = await response.text();
33992
+ if (!rawText.trim()) {
33993
+ throw new CocodHttpError(response.ok ? 502 : response.status, `Empty response from cocod for ${method} ${path}`);
33994
+ }
33995
+ let data;
33996
+ try {
33997
+ data = JSON.parse(rawText);
33998
+ } catch {
33999
+ throw new CocodHttpError(response.ok ? 502 : response.status, `Invalid JSON response from cocod for ${method} ${path}`);
34000
+ }
34001
+ if (!data || typeof data !== "object") {
34002
+ throw new CocodHttpError(response.ok ? 502 : response.status, `Unexpected response shape from cocod for ${method} ${path}`);
34003
+ }
34004
+ const errorMessage = toErrorText(data.error);
34005
+ if (errorMessage) {
34006
+ throw new CocodHttpError(response.ok ? 400 : response.status, errorMessage);
34007
+ }
34008
+ if (!response.ok) {
34009
+ throw new CocodHttpError(response.status, data.error || response.statusText || `HTTP ${response.status}`);
34010
+ }
34011
+ return data;
34012
+ }
34013
+ async function pingInternal() {
34014
+ try {
34015
+ await fetchJson("/ping");
34016
+ return true;
34017
+ } catch {
34018
+ return false;
34019
+ }
34020
+ }
34021
+ async function startDaemon() {
34022
+ const env = { ...process.env, COCOD_SOCKET: socketPath };
34023
+ const proc = spawnDaemon([executable, "daemon"], env);
34024
+ const maxPolls = Math.ceil(startupTimeoutMs / pollIntervalMs);
34025
+ let exitCode = null;
34026
+ proc.exited.then((code) => {
34027
+ exitCode = code;
33735
34028
  });
33736
- child.on("error", (error) => reject(error));
33737
- child.on("close", (code) => {
33738
- if (code && code !== 0) {
33739
- reject(new Error(stderr.trim() || stdout.trim() || "Wallet CLI failed"));
34029
+ for (let i4 = 0;i4 < maxPolls; i4++) {
34030
+ await delay(pollIntervalMs);
34031
+ if (exitCode !== null) {
34032
+ throw new Error(`cocod daemon exited early with code ${exitCode}`);
34033
+ }
34034
+ if (await pingInternal()) {
34035
+ logger3.debug(`Connected to cocod daemon on ${socketPath}`);
33740
34036
  return;
33741
34037
  }
33742
- resolve(stdout.trim());
33743
- });
33744
- });
33745
- }
33746
- function parseBalances(output4) {
33747
- const trimmed = output4.trim();
33748
- if (!trimmed)
33749
- return {};
33750
- try {
33751
- const parsed = JSON.parse(trimmed);
33752
- if (parsed && typeof parsed === "object") {
33753
- return Object.fromEntries(Object.entries(parsed).map(([mintUrl, value]) => {
33754
- if (typeof value === "number") {
33755
- return [mintUrl, value];
33756
- }
33757
- if (value && typeof value === "object" && "sats" in value) {
33758
- return [mintUrl, Number(value.sats ?? 0)];
33759
- }
33760
- return [mintUrl, 0];
33761
- }));
33762
34038
  }
33763
- } catch {}
33764
- const balances = {};
33765
- trimmed.split(`
33766
- `).map((line) => line.trim()).forEach((line) => {
33767
- const match = line.match(/^(\S+):\s+(\d+)\s+s$/);
33768
- const mintUrl = match?.[1];
33769
- const amount = match?.[2];
33770
- if (mintUrl && amount) {
33771
- balances[mintUrl] = Number.parseInt(amount, 10);
34039
+ throw new Error(`cocod daemon failed to start within ${Math.round(startupTimeoutMs / 1000)} seconds`);
34040
+ }
34041
+ async function ensureDaemonRunning() {
34042
+ if (await pingInternal()) {
34043
+ return;
33772
34044
  }
33773
- });
33774
- return balances;
33775
- }
33776
- function parseMints(output4) {
33777
- return output4.split(`
33778
- `).map((line) => line.trim()).map((line) => {
33779
- const urlMatch = line.match(/https?:\/\/\S+/i);
33780
- if (!urlMatch)
33781
- return null;
33782
- const trustedMatch = line.match(/trusted:\s*(true|false)/i);
33783
- const trustedValue = trustedMatch?.[1];
33784
- return {
33785
- url: urlMatch[0],
33786
- trusted: trustedMatch ? trustedValue?.toLowerCase() === "true" : false
33787
- };
33788
- }).filter((entry) => Boolean(entry));
33789
- }
33790
- function pickTokenLine(output4) {
33791
- const lines = output4.split(`
33792
- `).map((line) => line.trim()).filter(Boolean);
33793
- return lines[lines.length - 1] || "";
34045
+ if (!startPromise) {
34046
+ logger3.debug(`Starting cocod daemon via ${executable}...`);
34047
+ startPromise = startDaemon().finally(() => {
34048
+ startPromise = null;
34049
+ });
34050
+ }
34051
+ await startPromise;
34052
+ }
34053
+ async function callDaemon(path, init = {}) {
34054
+ await ensureDaemonRunning();
34055
+ const response = await fetchJson(path, init);
34056
+ return response.output;
34057
+ }
34058
+ function post(path, body) {
34059
+ return callDaemon(path, {
34060
+ method: "POST",
34061
+ headers: { "Content-Type": "application/json" },
34062
+ body: JSON.stringify(body)
34063
+ });
34064
+ }
34065
+ return {
34066
+ async ping() {
34067
+ return pingInternal();
34068
+ },
34069
+ async getStatus() {
34070
+ return callDaemon("/status");
34071
+ },
34072
+ async unlock(passphrase) {
34073
+ return post("/unlock", { passphrase });
34074
+ },
34075
+ async getBalances() {
34076
+ const output4 = await callDaemon("/balance");
34077
+ return normalizeBalances(output4);
34078
+ },
34079
+ async receiveCashu(token) {
34080
+ logger3.debug(`[receiveCashu] Receiving Cashu token ${tokenFingerprint(token)}`);
34081
+ const message = await callDaemon("/receive/cashu", {
34082
+ method: "POST",
34083
+ headers: { "Content-Type": "application/json" },
34084
+ body: JSON.stringify({ token })
34085
+ });
34086
+ if (typeof message !== "string" || !message.trim()) {
34087
+ throw new CocodHttpError(502, "Unexpected response from cocod while receiving Cashu token.");
34088
+ }
34089
+ logger3.debug(`[receiveCashu] Full response.output:`, message);
34090
+ return message;
34091
+ },
34092
+ async receiveBolt11(amount, mintUrl) {
34093
+ return post("/receive/bolt11", { amount, mintUrl });
34094
+ },
34095
+ async sendCashu(amount, mintUrl) {
34096
+ return post("/send/cashu", { amount, mintUrl });
34097
+ },
34098
+ async sendBolt11(invoice, mintUrl) {
34099
+ return post("/send/bolt11", { invoice, mintUrl });
34100
+ },
34101
+ async listMints() {
34102
+ const output4 = await callDaemon("/mints/list");
34103
+ return parseMintList(output4);
34104
+ },
34105
+ async addMint(url2) {
34106
+ return post("/mints/add", { url: url2 });
34107
+ },
34108
+ async getMintInfo(url2) {
34109
+ return post("/mints/info", { url: url2 });
34110
+ }
34111
+ };
33794
34112
  }
33795
- async function createWalletAdapter() {
34113
+
34114
+ // src/daemon/wallet/index.ts
34115
+ function decodeCashuTokenAmount(token) {
34116
+ const decoded = ar(token);
34117
+ const amount = decoded?.proofs?.reduce((sum, proof) => sum + proof.amount, 0) ?? 0;
34118
+ const unit = decoded?.unit === "msat" ? "msat" : "sat";
34119
+ return { amount, unit };
34120
+ }
34121
+ async function createWalletAdapter(options = {}) {
34122
+ const client2 = options.walletClient || createCocodClient({ cocodPath: options.cocodPath });
33796
34123
  let activeMintUrl = null;
33797
34124
  let mintUnits = {};
33798
- const walletAdapter = {
33799
- async getBalances() {
33800
- const output4 = await runWalletCommand(["balance"]);
33801
- const balances = parseBalances(output4);
33802
- mintUnits = Object.fromEntries(Object.keys(balances).map((mintUrl) => [mintUrl, "sat"]));
34125
+ async function syncMintState(balances) {
34126
+ const nextBalances = balances || await client2.getBalances();
34127
+ mintUnits = Object.fromEntries(Object.keys(nextBalances).map((mintUrl) => [mintUrl, "sat"]));
34128
+ try {
34129
+ const mints = await client2.listMints();
34130
+ activeMintUrl = mints[0] || Object.keys(nextBalances)[0] || null;
34131
+ } catch (error) {
34132
+ logger3.error("Failed to list cocod mints:", error);
33803
34133
  if (!activeMintUrl) {
33804
- activeMintUrl = Object.keys(balances)[0] || null;
34134
+ activeMintUrl = Object.keys(nextBalances)[0] || null;
33805
34135
  }
33806
- return balances;
34136
+ }
34137
+ return nextBalances;
34138
+ }
34139
+ const walletAdapter = {
34140
+ async getBalances() {
34141
+ return syncMintState();
33807
34142
  },
33808
34143
  getMintUnits() {
33809
34144
  return mintUnits;
@@ -33817,18 +34152,7 @@ async function createWalletAdapter() {
33817
34152
  const retryErrorPattern = "Proof already reserved by operation";
33818
34153
  for (let attempt = 0;attempt <= maxRetries; attempt++) {
33819
34154
  try {
33820
- const output4 = await runWalletCommand([
33821
- "send",
33822
- "cashu",
33823
- String(amount),
33824
- "--mint-url",
33825
- mintUrl
33826
- ]);
33827
- const token = pickTokenLine(output4);
33828
- if (!token) {
33829
- throw new Error("Wallet CLI did not return a token.");
33830
- }
33831
- return token;
34155
+ return await client2.sendCashu(amount, mintUrl);
33832
34156
  } catch (error) {
33833
34157
  const errorMessage = error instanceof Error ? error.message : String(error);
33834
34158
  const shouldRetry = attempt < maxRetries && errorMessage.includes(retryErrorPattern);
@@ -33845,28 +34169,25 @@ async function createWalletAdapter() {
33845
34169
  },
33846
34170
  async receiveToken(token) {
33847
34171
  try {
33848
- await runWalletCommand(["receive", "cashu", token]);
33849
- const decoded = ar(token);
33850
- const amount = decoded?.proofs?.reduce((sum, proof) => sum + proof.amount, 0);
33851
- const unit = decoded?.unit === "msat" ? "msat" : "sat";
33852
- return { success: true, amount: amount ?? 0, unit };
34172
+ const message = await client2.receiveCashu(token);
34173
+ const { amount, unit } = decodeCashuTokenAmount(token);
34174
+ return { success: true, amount, unit, message };
33853
34175
  } catch (error) {
33854
- console.log("Eerro in receive", error);
33855
34176
  const errorMessage = error instanceof Error ? error.message : String(error);
33856
- const message = errorMessage.includes("Failed to fetch mint") ? errorMessage : undefined;
33857
- return { success: false, amount: 0, unit: "sat", message };
34177
+ logger3.error("Error in walletAdapter receiveToken:", error);
34178
+ return { success: false, amount: 0, unit: "sat", message: errorMessage };
33858
34179
  }
33859
- },
33860
- isUsingNip60() {
33861
- return false;
33862
34180
  }
33863
34181
  };
33864
34182
  try {
33865
- const mintsOutput = await runWalletCommand(["mints", "list"]);
33866
- const mints = parseMints(mintsOutput);
33867
- activeMintUrl = mints.find((mint) => mint.trusted)?.url || mints[0]?.url || null;
34183
+ const [balances, mints] = await Promise.all([
34184
+ client2.getBalances(),
34185
+ client2.listMints().catch(() => [])
34186
+ ]);
34187
+ mintUnits = Object.fromEntries(Object.keys(balances).map((mintUrl) => [mintUrl, "sat"]));
34188
+ activeMintUrl = mints[0] || Object.keys(balances)[0] || null;
33868
34189
  } catch (error) {
33869
- logger3.error("Failed to read mints from wallet:", error);
34190
+ logger3.error("Failed to initialize wallet adapter state:", error);
33870
34191
  }
33871
34192
  return walletAdapter;
33872
34193
  }
@@ -33908,6 +34229,11 @@ function createModelService(modelManager) {
33908
34229
 
33909
34230
  // src/daemon/http/index.ts
33910
34231
  init_dist3();
34232
+ import { randomBytes as randomBytes4 } from "crypto";
34233
+ function generateApiKey() {
34234
+ const bytes4 = randomBytes4(24);
34235
+ return `sk-${bytes4.toString("hex")}`;
34236
+ }
33911
34237
  async function readBody(req) {
33912
34238
  return new Promise((resolve, reject) => {
33913
34239
  let data = "";
@@ -33918,60 +34244,255 @@ async function readBody(req) {
33918
34244
  req.on("error", reject);
33919
34245
  });
33920
34246
  }
34247
+ async function readJsonBody(req) {
34248
+ const bodyText = await readBody(req);
34249
+ if (!bodyText) {
34250
+ return {};
34251
+ }
34252
+ try {
34253
+ return JSON.parse(bodyText);
34254
+ } catch {
34255
+ throw new CocodHttpError(400, "Invalid JSON body.");
34256
+ }
34257
+ }
33921
34258
  function parseLimit(value, fallback = 10) {
33922
34259
  const requested = Number.parseInt(value || String(fallback), 10);
33923
- return Number.isFinite(requested) && requested > 0 ? Math.min(requested, 1000) : fallback;
34260
+ return Number.isFinite(requested) && requested > 0 ? Math.min(requested, 1e5) : fallback;
34261
+ }
34262
+ function sendJson(res, status, payload) {
34263
+ res.writeHead(status, { "Content-Type": "application/json" });
34264
+ res.end(JSON.stringify(payload));
34265
+ }
34266
+ function toErrorMessage(error) {
34267
+ return error instanceof Error ? error.message : String(error);
34268
+ }
34269
+ function getWalletStateMessage(state) {
34270
+ switch (state) {
34271
+ case "LOCKED":
34272
+ return "Wallet is locked. Unlock it before performing wallet operations.";
34273
+ case "UNINITIALIZED":
34274
+ return "Wallet is not initialized. Run 'routstrd onboard' first.";
34275
+ case "ERROR":
34276
+ return "Wallet is in an error state.";
34277
+ default:
34278
+ return "Wallet is unavailable.";
34279
+ }
34280
+ }
34281
+ function respondWithError(res, error, fallbackStatus = 500) {
34282
+ if (error instanceof CocodHttpError) {
34283
+ sendJson(res, error.status, { error: error.message });
34284
+ return;
34285
+ }
34286
+ sendJson(res, fallbackStatus, { error: toErrorMessage(error) });
34287
+ }
34288
+ async function respond(res, getPayload) {
34289
+ try {
34290
+ sendJson(res, 200, await getPayload());
34291
+ } catch (error) {
34292
+ respondWithError(res, error);
34293
+ }
34294
+ }
34295
+ function requireStringField(body, field) {
34296
+ const value = body[field];
34297
+ return typeof value === "string" && value.trim() ? value.trim() : null;
34298
+ }
34299
+ function getRequiredStringField(body, field) {
34300
+ const value = requireStringField(body, field);
34301
+ if (!value) {
34302
+ throw new CocodHttpError(400, `Missing required '${field}' field.`);
34303
+ }
34304
+ return value;
34305
+ }
34306
+ function getRequiredPositiveNumberField(body, field) {
34307
+ const value = body[field];
34308
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
34309
+ return value;
34310
+ }
34311
+ if (typeof value === "string" && value.trim()) {
34312
+ const parsed = Number.parseInt(value.trim(), 10);
34313
+ if (Number.isFinite(parsed) && parsed > 0) {
34314
+ return parsed;
34315
+ }
34316
+ }
34317
+ throw new CocodHttpError(400, `Missing required '${field}' field.`);
34318
+ }
34319
+ function optionalStringField(body, field) {
34320
+ const value = body[field];
34321
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
34322
+ }
34323
+ function getCurrentMode(deps) {
34324
+ const stateMode = deps.store.getState()?.mode;
34325
+ return stateMode || deps.mode || "apikeys";
34326
+ }
34327
+ async function buildStatusOutput(deps) {
34328
+ const mode = getCurrentMode(deps);
34329
+ try {
34330
+ const walletState = await deps.walletClient.getStatus();
34331
+ if (walletState !== "UNLOCKED") {
34332
+ return {
34333
+ daemon: "running",
34334
+ wallet: "error",
34335
+ walletState,
34336
+ mode,
34337
+ error: getWalletStateMessage(walletState)
34338
+ };
34339
+ }
34340
+ const balances = await deps.walletAdapter.getBalances();
34341
+ return {
34342
+ daemon: "running",
34343
+ wallet: "connected",
34344
+ walletState,
34345
+ balances,
34346
+ mode
34347
+ };
34348
+ } catch (error) {
34349
+ return {
34350
+ daemon: "running",
34351
+ wallet: "error",
34352
+ walletState: "ERROR",
34353
+ mode,
34354
+ error: toErrorMessage(error)
34355
+ };
34356
+ }
34357
+ }
34358
+ async function buildWalletDetails(deps) {
34359
+ const state = await deps.walletClient.getStatus();
34360
+ if (state !== "UNLOCKED") {
34361
+ return { state, ready: false };
34362
+ }
34363
+ const balances = await deps.walletAdapter.getBalances();
34364
+ return {
34365
+ state,
34366
+ ready: true,
34367
+ balances,
34368
+ unit: "sat",
34369
+ activeMint: deps.walletAdapter.getActiveMintUrl()
34370
+ };
33924
34371
  }
33925
34372
  function createDaemonRequestHandler(deps) {
33926
34373
  return async function handler(req, res) {
33927
34374
  const host = req.headers.host || "localhost";
33928
34375
  const url2 = new URL(req.url || "/", `http://${host}`);
33929
34376
  if (req.method === "GET" && url2.pathname === "/health") {
33930
- res.writeHead(200, { "Content-Type": "application/json" });
33931
- res.end(JSON.stringify({ ok: true }));
34377
+ sendJson(res, 200, { ok: true });
33932
34378
  return;
33933
34379
  }
33934
34380
  if (req.method === "GET" && url2.pathname === "/ping") {
33935
- res.writeHead(200, { "Content-Type": "application/json" });
33936
- res.end(JSON.stringify({ output: "pong" }));
34381
+ sendJson(res, 200, { output: "pong" });
33937
34382
  return;
33938
34383
  }
33939
34384
  if (req.method === "GET" && url2.pathname === "/status") {
33940
- try {
33941
- const balancesOutput = await deps.runWalletCommand(["balance"]);
33942
- const balances = deps.parseBalances(balancesOutput);
33943
- const state = deps.store.getState();
33944
- const mode = state.mode || deps.mode || "apikeys";
33945
- res.writeHead(200, { "Content-Type": "application/json" });
33946
- res.end(JSON.stringify({
34385
+ const output4 = await buildStatusOutput(deps);
34386
+ sendJson(res, 200, { output: output4 });
34387
+ return;
34388
+ }
34389
+ if (req.method === "GET" && url2.pathname === "/wallet/status") {
34390
+ await respond(res, async () => ({
34391
+ output: await buildWalletDetails(deps)
34392
+ }));
34393
+ return;
34394
+ }
34395
+ if (req.method === "POST" && url2.pathname === "/wallet/unlock") {
34396
+ await respond(res, async () => {
34397
+ const body = await readJsonBody(req);
34398
+ const passphrase = getRequiredStringField(body, "passphrase");
34399
+ const message = await deps.walletClient.unlock(passphrase);
34400
+ const state = await deps.walletClient.getStatus();
34401
+ return { output: { message, state } };
34402
+ });
34403
+ return;
34404
+ }
34405
+ if (req.method === "GET" && url2.pathname === "/wallet/balance") {
34406
+ await respond(res, async () => {
34407
+ const balances = await deps.walletAdapter.getBalances();
34408
+ return {
33947
34409
  output: {
33948
- daemon: "running",
33949
- wallet: "connected",
33950
- mode,
33951
- balances
34410
+ balances,
34411
+ unit: "sat",
34412
+ activeMint: deps.walletAdapter.getActiveMintUrl(),
34413
+ walletState: "UNLOCKED"
33952
34414
  }
33953
- }));
33954
- } catch (error) {
33955
- res.writeHead(200, { "Content-Type": "application/json" });
33956
- res.end(JSON.stringify({
34415
+ };
34416
+ });
34417
+ return;
34418
+ }
34419
+ if (req.method === "POST" && url2.pathname === "/wallet/receive/cashu") {
34420
+ await respond(res, async () => {
34421
+ const body = await readJsonBody(req);
34422
+ const token = getRequiredStringField(body, "token");
34423
+ const message = await deps.walletClient.receiveCashu(token);
34424
+ const { amount, unit } = decodeCashuTokenAmount(token);
34425
+ return { output: { message, amount, unit } };
34426
+ });
34427
+ return;
34428
+ }
34429
+ if (req.method === "POST" && url2.pathname === "/wallet/receive/bolt11") {
34430
+ await respond(res, async () => {
34431
+ const body = await readJsonBody(req);
34432
+ const amount = getRequiredPositiveNumberField(body, "amount");
34433
+ const mintUrl = optionalStringField(body, "mintUrl");
34434
+ const invoice = await deps.walletClient.receiveBolt11(amount, mintUrl);
34435
+ return { output: { invoice, amount, mintUrl } };
34436
+ });
34437
+ return;
34438
+ }
34439
+ if (req.method === "POST" && url2.pathname === "/wallet/send/cashu") {
34440
+ await respond(res, async () => {
34441
+ const body = await readJsonBody(req);
34442
+ const amount = getRequiredPositiveNumberField(body, "amount");
34443
+ const mintUrl = optionalStringField(body, "mintUrl");
34444
+ const token = await deps.walletClient.sendCashu(amount, mintUrl);
34445
+ return { output: { token, amount, mintUrl } };
34446
+ });
34447
+ return;
34448
+ }
34449
+ if (req.method === "POST" && url2.pathname === "/wallet/send/bolt11") {
34450
+ await respond(res, async () => {
34451
+ const body = await readJsonBody(req);
34452
+ const invoice = getRequiredStringField(body, "invoice");
34453
+ const mintUrl = optionalStringField(body, "mintUrl");
34454
+ const message = await deps.walletClient.sendBolt11(invoice, mintUrl);
34455
+ return { output: { message, invoice, mintUrl } };
34456
+ });
34457
+ return;
34458
+ }
34459
+ if (req.method === "GET" && url2.pathname === "/wallet/mints") {
34460
+ await respond(res, async () => {
34461
+ const mints = await deps.walletClient.listMints();
34462
+ return {
33957
34463
  output: {
33958
- daemon: "running",
33959
- wallet: "error",
33960
- error: String(error)
34464
+ mints,
34465
+ activeMint: mints[0] || null
33961
34466
  }
33962
- }));
33963
- }
34467
+ };
34468
+ });
34469
+ return;
34470
+ }
34471
+ if (req.method === "POST" && url2.pathname === "/wallet/mints") {
34472
+ await respond(res, async () => {
34473
+ const body = await readJsonBody(req);
34474
+ const mintUrl = getRequiredStringField(body, "url");
34475
+ const message = await deps.walletClient.addMint(mintUrl);
34476
+ return { output: { message, url: mintUrl } };
34477
+ });
34478
+ return;
34479
+ }
34480
+ if (req.method === "POST" && url2.pathname === "/wallet/mints/info") {
34481
+ await respond(res, async () => {
34482
+ const body = await readJsonBody(req);
34483
+ const mintUrl = getRequiredStringField(body, "url");
34484
+ const info = await deps.walletClient.getMintInfo(mintUrl);
34485
+ return { output: { url: mintUrl, info } };
34486
+ });
33964
34487
  return;
33965
34488
  }
33966
34489
  if (req.method === "GET" && url2.pathname === "/models") {
33967
34490
  try {
33968
34491
  const forceRefresh = url2.searchParams.get("refresh")?.toLowerCase() === "true";
33969
34492
  const models = await deps.getRoutstr21Models(forceRefresh);
33970
- res.writeHead(200, { "Content-Type": "application/json" });
33971
- res.end(JSON.stringify({ output: { models } }));
34493
+ sendJson(res, 200, { output: { models } });
33972
34494
  } catch (error) {
33973
- res.writeHead(500, { "Content-Type": "application/json" });
33974
- res.end(JSON.stringify({ error: String(error) }));
34495
+ sendJson(res, 500, { error: toErrorMessage(error) });
33975
34496
  }
33976
34497
  return;
33977
34498
  }
@@ -33979,20 +34500,17 @@ function createDaemonRequestHandler(deps) {
33979
34500
  try {
33980
34501
  const forceRefresh = url2.searchParams.get("refresh")?.toLowerCase() === "true";
33981
34502
  const models = await deps.getRoutstr21Models(forceRefresh);
33982
- res.writeHead(200, { "Content-Type": "application/json" });
33983
- res.end(JSON.stringify({
34503
+ sendJson(res, 200, {
33984
34504
  object: "list",
33985
34505
  data: models.map((model2) => ({ ...model2, object: "model" }))
33986
- }));
34506
+ });
33987
34507
  } catch (error) {
33988
- res.writeHead(500, { "Content-Type": "application/json" });
33989
- res.end(JSON.stringify({ error: String(error) }));
34508
+ sendJson(res, 500, { error: toErrorMessage(error) });
33990
34509
  }
33991
34510
  return;
33992
34511
  }
33993
34512
  if (req.method === "POST" && url2.pathname === "/stop") {
33994
- res.writeHead(200, { "Content-Type": "application/json" });
33995
- res.end(JSON.stringify({ output: "stopping" }));
34513
+ sendJson(res, 200, { output: "stopping" });
33996
34514
  setTimeout(() => {
33997
34515
  deps.server.close(() => {
33998
34516
  process.exit(0);
@@ -34002,14 +34520,8 @@ function createDaemonRequestHandler(deps) {
34002
34520
  }
34003
34521
  if (req.method === "POST" && url2.pathname === "/refund") {
34004
34522
  try {
34005
- const bodyText = await readBody(req);
34006
- const body = bodyText ? JSON.parse(bodyText) : {};
34007
- const mintUrl = body.mintUrl;
34008
- if (!mintUrl) {
34009
- res.writeHead(400, { "Content-Type": "application/json" });
34010
- res.end(JSON.stringify({ error: "Missing required 'mintUrl' field." }));
34011
- return;
34012
- }
34523
+ const body = await readJsonBody(req);
34524
+ const mintUrl = getRequiredStringField(body, "mintUrl");
34013
34525
  const state = deps.store.getState();
34014
34526
  const pendingDistribution = (state.cachedTokens || []).map((t) => ({
34015
34527
  baseUrl: t.baseUrl,
@@ -34020,19 +34532,17 @@ function createDaemonRequestHandler(deps) {
34020
34532
  amount: k.balance || 0
34021
34533
  }));
34022
34534
  if (pendingDistribution.length === 0 && apiKeysStored.length === 0) {
34023
- res.writeHead(200, { "Content-Type": "application/json" });
34024
- res.end(JSON.stringify({
34535
+ sendJson(res, 200, {
34025
34536
  output: { message: "No pending tokens to refund", results: [] }
34026
- }));
34537
+ });
34027
34538
  return;
34028
34539
  }
34029
34540
  const refundBaseUrls = pendingDistribution.map((p) => p.baseUrl).concat(apiKeysStored.map((p) => p.baseUrl));
34030
34541
  const { RoutstrClient: RoutstrClient2 } = await Promise.resolve().then(() => (init_dist3(), exports_dist));
34031
34542
  const client2 = new RoutstrClient2(deps.walletAdapter, deps.storageAdapter, deps.providerRegistry, "min", "apikeys");
34032
34543
  const spender = client2.getCashuSpender();
34033
- const results = await spender.refundProviders(mintUrl);
34034
- res.writeHead(200, { "Content-Type": "application/json" });
34035
- res.end(JSON.stringify({
34544
+ const results = await spender.refundProviders(mintUrl, true);
34545
+ sendJson(res, 200, {
34036
34546
  output: {
34037
34547
  message: `Refunded to ${mintUrl}`,
34038
34548
  pendingTokens: pendingDistribution.length,
@@ -34042,29 +34552,25 @@ function createDaemonRequestHandler(deps) {
34042
34552
  success: r.success
34043
34553
  }))
34044
34554
  }
34045
- }));
34555
+ });
34046
34556
  } catch (error) {
34047
- const message = error instanceof Error ? error.message : String(error);
34048
- logger3.error(`Refund error: ${message}`);
34049
- res.writeHead(500, { "Content-Type": "application/json" });
34050
- res.end(JSON.stringify({ error: message }));
34557
+ logger3.error(`Refund error: ${toErrorMessage(error)}`);
34558
+ respondWithError(res, error);
34051
34559
  }
34052
34560
  return;
34053
34561
  }
34054
34562
  if (req.method === "GET" && url2.pathname === "/balance") {
34055
34563
  try {
34056
34564
  const balances = await deps.walletAdapter.getBalances();
34057
- res.writeHead(200, { "Content-Type": "application/json" });
34058
- res.end(JSON.stringify({
34565
+ sendJson(res, 200, {
34059
34566
  output: {
34060
34567
  balances,
34061
34568
  unit: "sat",
34062
34569
  activeMint: deps.walletAdapter.getActiveMintUrl()
34063
34570
  }
34064
- }));
34571
+ });
34065
34572
  } catch (error) {
34066
- res.writeHead(500, { "Content-Type": "application/json" });
34067
- res.end(JSON.stringify({ error: String(error) }));
34573
+ respondWithError(res, error);
34068
34574
  }
34069
34575
  return;
34070
34576
  }
@@ -34090,14 +34596,120 @@ function createDaemonRequestHandler(deps) {
34090
34596
  balance: k.balance || 0
34091
34597
  }))
34092
34598
  ];
34093
- res.writeHead(200, { "Content-Type": "application/json" });
34094
- res.end(JSON.stringify({
34599
+ sendJson(res, 200, {
34095
34600
  output: {
34096
34601
  keys: keys2,
34097
34602
  total: totalWallet + totalCached + totalApiKeys,
34098
34603
  unit: "sat",
34099
34604
  apikeysCalled: apiKeys.length
34100
34605
  }
34606
+ });
34607
+ } catch (error) {
34608
+ res.writeHead(500, { "Content-Type": "application/json" });
34609
+ res.end(JSON.stringify({ error: String(error) }));
34610
+ }
34611
+ return;
34612
+ }
34613
+ if (req.method === "POST" && url2.pathname === "/providers/disable") {
34614
+ try {
34615
+ const bodyText = await readBody(req);
34616
+ const body = bodyText ? JSON.parse(bodyText) : {};
34617
+ const indices = body.indices;
34618
+ if (!Array.isArray(indices)) {
34619
+ res.writeHead(400, { "Content-Type": "application/json" });
34620
+ res.end(JSON.stringify({
34621
+ error: "Missing or invalid 'indices' field (expected number[])."
34622
+ }));
34623
+ return;
34624
+ }
34625
+ const state = deps.store.getState();
34626
+ const baseUrlsList = state.baseUrlsList || [];
34627
+ const disabledProviders = [
34628
+ ...state.disabledProviders || []
34629
+ ];
34630
+ const toDisable = [];
34631
+ for (const idx of indices) {
34632
+ if (typeof idx === "number" && idx >= 0 && idx < baseUrlsList.length) {
34633
+ const baseUrl = baseUrlsList[idx];
34634
+ if (!disabledProviders.includes(baseUrl)) {
34635
+ disabledProviders.push(baseUrl);
34636
+ toDisable.push(baseUrl);
34637
+ }
34638
+ }
34639
+ }
34640
+ deps.store.getState().setDisabledProviders(disabledProviders);
34641
+ res.writeHead(200, { "Content-Type": "application/json" });
34642
+ res.end(JSON.stringify({
34643
+ output: {
34644
+ message: `Disabled ${toDisable.length} provider(s)`,
34645
+ disabled: toDisable
34646
+ }
34647
+ }));
34648
+ } catch (error) {
34649
+ res.writeHead(500, { "Content-Type": "application/json" });
34650
+ res.end(JSON.stringify({ error: String(error) }));
34651
+ }
34652
+ return;
34653
+ }
34654
+ if (req.method === "POST" && url2.pathname === "/providers/enable") {
34655
+ try {
34656
+ const bodyText = await readBody(req);
34657
+ const body = bodyText ? JSON.parse(bodyText) : {};
34658
+ const indices = body.indices;
34659
+ if (!Array.isArray(indices)) {
34660
+ res.writeHead(400, { "Content-Type": "application/json" });
34661
+ res.end(JSON.stringify({
34662
+ error: "Missing or invalid 'indices' field (expected number[])."
34663
+ }));
34664
+ return;
34665
+ }
34666
+ const state = deps.store.getState();
34667
+ const baseUrlsList = state.baseUrlsList || [];
34668
+ const disabledProviders = [
34669
+ ...state.disabledProviders || []
34670
+ ];
34671
+ const toEnable = [];
34672
+ for (const idx of indices) {
34673
+ if (typeof idx === "number" && idx >= 0 && idx < baseUrlsList.length) {
34674
+ const baseUrl = baseUrlsList[idx];
34675
+ const pos = disabledProviders.indexOf(baseUrl);
34676
+ if (pos !== -1) {
34677
+ disabledProviders.splice(pos, 1);
34678
+ toEnable.push(baseUrl);
34679
+ }
34680
+ }
34681
+ }
34682
+ deps.store.getState().setDisabledProviders(disabledProviders);
34683
+ res.writeHead(200, { "Content-Type": "application/json" });
34684
+ res.end(JSON.stringify({
34685
+ output: {
34686
+ message: `Enabled ${toEnable.length} provider(s)`,
34687
+ enabled: toEnable
34688
+ }
34689
+ }));
34690
+ } catch (error) {
34691
+ res.writeHead(500, { "Content-Type": "application/json" });
34692
+ res.end(JSON.stringify({ error: String(error) }));
34693
+ }
34694
+ return;
34695
+ }
34696
+ if (req.method === "GET" && url2.pathname === "/clients") {
34697
+ try {
34698
+ const state = deps.store.getState();
34699
+ const clientIds = state.clientIds || [];
34700
+ const clients = clientIds.map((c) => ({
34701
+ id: c.clientId,
34702
+ name: c.name,
34703
+ apiKey: c.apiKey,
34704
+ createdAt: c.createdAt,
34705
+ lastUsed: c.lastUsed
34706
+ }));
34707
+ res.writeHead(200, { "Content-Type": "application/json" });
34708
+ res.end(JSON.stringify({
34709
+ output: {
34710
+ clients,
34711
+ totalCount: clients.length
34712
+ }
34101
34713
  }));
34102
34714
  } catch (error) {
34103
34715
  res.writeHead(500, { "Content-Type": "application/json" });
@@ -34105,6 +34717,65 @@ function createDaemonRequestHandler(deps) {
34105
34717
  }
34106
34718
  return;
34107
34719
  }
34720
+ if (req.method === "POST" && url2.pathname === "/clients/add") {
34721
+ try {
34722
+ const bodyText = await readBody(req);
34723
+ const body = bodyText ? JSON.parse(bodyText) : {};
34724
+ const name = body.name;
34725
+ if (!name || typeof name !== "string" || name.trim() === "") {
34726
+ res.writeHead(400, { "Content-Type": "application/json" });
34727
+ res.end(JSON.stringify({
34728
+ error: "Missing required 'name' field (must be a non-empty string)."
34729
+ }));
34730
+ return;
34731
+ }
34732
+ const clientId = name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
34733
+ if (!clientId) {
34734
+ res.writeHead(400, { "Content-Type": "application/json" });
34735
+ res.end(JSON.stringify({
34736
+ error: "Invalid client name. Must contain alphanumeric characters."
34737
+ }));
34738
+ return;
34739
+ }
34740
+ const state = deps.store.getState();
34741
+ const existingClients = state.clientIds || [];
34742
+ const existingClient = existingClients.find((c) => c.clientId === clientId);
34743
+ if (existingClient) {
34744
+ res.writeHead(409, { "Content-Type": "application/json" });
34745
+ res.end(JSON.stringify({
34746
+ error: `Client with id '${clientId}' already exists.`
34747
+ }));
34748
+ return;
34749
+ }
34750
+ const apiKey = generateApiKey();
34751
+ const newClient = {
34752
+ clientId,
34753
+ name: name.trim(),
34754
+ apiKey,
34755
+ createdAt: Date.now()
34756
+ };
34757
+ deps.store.getState().setClientIds((prev) => [
34758
+ ...prev || [],
34759
+ newClient
34760
+ ]);
34761
+ logger3.log(`Added client '${name}' with id '${clientId}'`);
34762
+ res.writeHead(200, { "Content-Type": "application/json" });
34763
+ res.end(JSON.stringify({
34764
+ output: {
34765
+ message: `Client '${name}' added successfully`,
34766
+ client: {
34767
+ id: clientId,
34768
+ name: name.trim(),
34769
+ apiKey,
34770
+ createdAt: newClient.createdAt
34771
+ }
34772
+ }
34773
+ }));
34774
+ } catch (error) {
34775
+ respondWithError(res, error);
34776
+ }
34777
+ return;
34778
+ }
34108
34779
  if (req.method === "GET" && url2.pathname === "/providers") {
34109
34780
  try {
34110
34781
  const state = deps.store.getState();
@@ -34115,11 +34786,12 @@ function createDaemonRequestHandler(deps) {
34115
34786
  baseUrl,
34116
34787
  disabled: disabledProviders.includes(baseUrl)
34117
34788
  }));
34789
+ const activeDisabledCount = providers.filter((p) => p.disabled).length;
34118
34790
  res.writeHead(200, { "Content-Type": "application/json" });
34119
34791
  res.end(JSON.stringify({
34120
34792
  output: {
34121
34793
  providers,
34122
- disabledCount: disabledProviders.length,
34794
+ disabledCount: activeDisabledCount,
34123
34795
  totalCount: baseUrlsList.length
34124
34796
  }
34125
34797
  }));
@@ -34212,22 +34884,82 @@ function createDaemonRequestHandler(deps) {
34212
34884
  }
34213
34885
  return;
34214
34886
  }
34215
- if (req.method === "GET" && url2.pathname === "/usage") {
34887
+ if (req.method === "GET" && url2.pathname === "/clients") {
34216
34888
  try {
34217
- const usageDriver = deps.usageTrackingDriver;
34218
- const limit2 = parseLimit(url2.searchParams.get("limit"));
34219
- const entries = await usageDriver.list({ limit: limit2 });
34220
- const totalEntries = await usageDriver.count();
34221
- const totalSatsCost = (await usageDriver.list()).reduce((sum, entry) => sum + (entry.satsCost || 0), 0);
34222
- const recentSatsCost = entries.reduce((sum, entry) => sum + (entry.satsCost || 0), 0);
34889
+ const state = deps.store.getState();
34890
+ const clientIds = state.clientIds || [];
34891
+ const clients = clientIds.map((c) => ({
34892
+ id: c.clientId,
34893
+ name: c.name,
34894
+ apiKey: c.apiKey,
34895
+ createdAt: c.createdAt,
34896
+ lastUsed: c.lastUsed
34897
+ }));
34223
34898
  res.writeHead(200, { "Content-Type": "application/json" });
34224
34899
  res.end(JSON.stringify({
34225
34900
  output: {
34226
- entries,
34227
- totalEntries,
34228
- totalSatsCost,
34229
- recentSatsCost,
34230
- limit: limit2
34901
+ clients,
34902
+ totalCount: clients.length
34903
+ }
34904
+ }));
34905
+ } catch (error) {
34906
+ res.writeHead(500, { "Content-Type": "application/json" });
34907
+ res.end(JSON.stringify({ error: String(error) }));
34908
+ }
34909
+ return;
34910
+ }
34911
+ if (req.method === "POST" && url2.pathname === "/clients/add") {
34912
+ try {
34913
+ const bodyText = await readBody(req);
34914
+ const body = bodyText ? JSON.parse(bodyText) : {};
34915
+ const name = body.name;
34916
+ if (!name || typeof name !== "string" || name.trim() === "") {
34917
+ res.writeHead(400, { "Content-Type": "application/json" });
34918
+ res.end(JSON.stringify({
34919
+ error: "Missing required 'name' field (must be a non-empty string)."
34920
+ }));
34921
+ return;
34922
+ }
34923
+ const clientId = name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
34924
+ if (!clientId) {
34925
+ res.writeHead(400, { "Content-Type": "application/json" });
34926
+ res.end(JSON.stringify({
34927
+ error: "Invalid client name. Must contain alphanumeric characters."
34928
+ }));
34929
+ return;
34930
+ }
34931
+ const state = deps.store.getState();
34932
+ const existingClients = state.clientIds || [];
34933
+ const existingClient = existingClients.find((c) => c.clientId === clientId);
34934
+ if (existingClient) {
34935
+ res.writeHead(409, { "Content-Type": "application/json" });
34936
+ res.end(JSON.stringify({
34937
+ error: `Client with id '${clientId}' already exists.`
34938
+ }));
34939
+ return;
34940
+ }
34941
+ const apiKey = generateApiKey();
34942
+ const newClient = {
34943
+ clientId,
34944
+ name: name.trim(),
34945
+ apiKey,
34946
+ createdAt: Date.now()
34947
+ };
34948
+ deps.store.getState().setClientIds((prev) => [
34949
+ ...prev || [],
34950
+ newClient
34951
+ ]);
34952
+ logger3.log(`Added client '${name}' with id '${clientId}'`);
34953
+ res.writeHead(200, { "Content-Type": "application/json" });
34954
+ res.end(JSON.stringify({
34955
+ output: {
34956
+ message: `Client '${name}' added successfully`,
34957
+ client: {
34958
+ id: clientId,
34959
+ name: name.trim(),
34960
+ apiKey,
34961
+ createdAt: newClient.createdAt
34962
+ }
34231
34963
  }
34232
34964
  }));
34233
34965
  } catch (error) {
@@ -34236,14 +34968,25 @@ function createDaemonRequestHandler(deps) {
34236
34968
  }
34237
34969
  return;
34238
34970
  }
34971
+ if (req.method === "GET" && url2.pathname === "/usage") {
34972
+ try {
34973
+ const output4 = await deps.usageTrackingDriver.list({
34974
+ limit: parseLimit(url2.searchParams.get("limit"))
34975
+ });
34976
+ res.writeHead(200, { "Content-Type": "application/json" });
34977
+ res.end(JSON.stringify({ output: output4 }));
34978
+ } catch (error) {
34979
+ sendJson(res, 500, { error: toErrorMessage(error) });
34980
+ }
34981
+ return;
34982
+ }
34239
34983
  if (req.method === "GET" && url2.pathname === "/usagePi") {
34240
34984
  try {
34241
34985
  const timestamp = (url2.searchParams.get("timestamp") || "").trim();
34242
34986
  if (!timestamp) {
34243
- res.writeHead(400, { "Content-Type": "application/json" });
34244
- res.end(JSON.stringify({
34987
+ sendJson(res, 400, {
34245
34988
  error: "Missing required 'timestamp' query parameter."
34246
- }));
34989
+ });
34247
34990
  return;
34248
34991
  }
34249
34992
  const usageDriver = deps.usageTrackingDriver;
@@ -34267,33 +35010,29 @@ function createDaemonRequestHandler(deps) {
34267
35010
  }
34268
35011
  }));
34269
35012
  } catch (error) {
34270
- res.writeHead(500, { "Content-Type": "application/json" });
34271
- res.end(JSON.stringify({ error: String(error) }));
35013
+ sendJson(res, 500, { error: toErrorMessage(error) });
34272
35014
  }
34273
35015
  return;
34274
35016
  }
34275
- if (req.method !== "POST") {
35017
+ if (req.method !== "POST" && !url2.pathname.startsWith("/clients")) {
34276
35018
  res.writeHead(405, { "Content-Type": "application/json" });
34277
35019
  res.end(JSON.stringify({ error: "Only POST is supported." }));
34278
35020
  return;
34279
35021
  }
34280
35022
  let requestBody = {};
34281
35023
  try {
34282
- const bodyText = await readBody(req);
34283
- requestBody = bodyText ? JSON.parse(bodyText) : {};
35024
+ requestBody = await readJsonBody(req);
34284
35025
  } catch (error) {
34285
- res.writeHead(400, { "Content-Type": "application/json" });
34286
- res.end(JSON.stringify({
35026
+ sendJson(res, 400, {
34287
35027
  error: "Invalid JSON body.",
34288
- details: error instanceof Error ? error.message : String(error)
34289
- }));
35028
+ details: toErrorMessage(error)
35029
+ });
34290
35030
  return;
34291
35031
  }
34292
35032
  const bodyObj = requestBody;
34293
35033
  const modelId = typeof bodyObj.model === "string" ? bodyObj.model : "";
34294
35034
  if (!modelId) {
34295
- res.writeHead(400, { "Content-Type": "application/json" });
34296
- res.end(JSON.stringify({ error: "Missing required 'model' field." }));
35035
+ sendJson(res, 400, { error: "Missing required 'model' field." });
34297
35036
  return;
34298
35037
  }
34299
35038
  const forcedProvider = url2.searchParams.get("provider") || req.headers["x-routstr-provider"] || deps.provider || undefined;
@@ -34323,6 +35062,7 @@ function createDaemonRequestHandler(deps) {
34323
35062
  mode: deps.mode,
34324
35063
  usageTrackingDriver: deps.usageTrackingDriver,
34325
35064
  sdkStore: deps.store,
35065
+ providerManager: deps.providerManager,
34326
35066
  res
34327
35067
  });
34328
35068
  return;
@@ -34348,7 +35088,287 @@ function createDaemonRequestHandler(deps) {
34348
35088
  };
34349
35089
  }
34350
35090
 
35091
+ // src/integrations/opencode.ts
35092
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
35093
+ import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
35094
+ import { dirname as dirname3 } from "path";
35095
+
35096
+ // src/integrations/registry.ts
35097
+ import { randomBytes as randomBytes5 } from "crypto";
35098
+ import { join as join4 } from "path";
35099
+
35100
+ // src/integrations/pi.ts
35101
+ import { existsSync as existsSync3, mkdirSync } from "fs";
35102
+ import { readFile, writeFile } from "fs/promises";
35103
+ import { dirname } from "path";
35104
+ async function installPiIntegration(config, store, integrationConfig) {
35105
+ const { clientId, name, configPath } = integrationConfig;
35106
+ logger3.log(`
35107
+ Installing routstr models in pi models.json...`);
35108
+ const port = config.port || 8008;
35109
+ const baseUrl = `http://localhost:${port}/v1`;
35110
+ const state = store.getState();
35111
+ const existingClient = (state.clientIds || []).find((c) => c.clientId === clientId);
35112
+ let apiKey;
35113
+ if (existingClient) {
35114
+ apiKey = existingClient.apiKey;
35115
+ logger3.log(`Using existing API key for ${name}`);
35116
+ } else {
35117
+ apiKey = generateApiKey2();
35118
+ store.getState().setClientIds((prev) => [
35119
+ ...prev || [],
35120
+ {
35121
+ clientId,
35122
+ name,
35123
+ apiKey,
35124
+ createdAt: Date.now()
35125
+ }
35126
+ ]);
35127
+ logger3.log(`Created new API key for ${name}`);
35128
+ }
35129
+ let piConfig = {};
35130
+ try {
35131
+ if (existsSync3(configPath)) {
35132
+ const content2 = await readFile(configPath, "utf-8");
35133
+ piConfig = JSON.parse(content2);
35134
+ }
35135
+ } catch {
35136
+ piConfig = {};
35137
+ }
35138
+ if (!piConfig.providers) {
35139
+ piConfig.providers = {};
35140
+ }
35141
+ try {
35142
+ mkdirSync(dirname(configPath), { recursive: true });
35143
+ const response = await fetch(`http://localhost:${port}/models`);
35144
+ const data = await response.json();
35145
+ const models = data.output?.models || [];
35146
+ if (models.length === 0) {
35147
+ logger3.log("No models found from routstr daemon.");
35148
+ return;
35149
+ }
35150
+ const providerModels = models.map((model2) => ({
35151
+ id: model2.id
35152
+ }));
35153
+ piConfig.providers["routstr"] = {
35154
+ baseUrl,
35155
+ api: "openai-completions",
35156
+ apiKey,
35157
+ models: providerModels
35158
+ };
35159
+ await writeFile(configPath, JSON.stringify(piConfig, null, 2));
35160
+ logger3.log(`Added "routstr" provider with ${models.length} models to pi models.json`);
35161
+ } catch (error) {
35162
+ logger3.error("Failed to install models in pi models.json:", error);
35163
+ }
35164
+ }
35165
+
35166
+ // src/integrations/openclaw.ts
35167
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
35168
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
35169
+ import { dirname as dirname2 } from "path";
35170
+ var OPENCLAW_PROVIDER_ID = "routstr";
35171
+ var OPENCLAW_DEFAULT_PRIMARY_MODEL = "routstr/minimax-m2.5";
35172
+ var OPENCLAW_DEFAULT_FALLBACK_MODEL = "routstr/kimi-k2.5";
35173
+ async function installOpenClawIntegration(config, store, integrationConfig) {
35174
+ const { clientId, name, configPath } = integrationConfig;
35175
+ logger3.log(`
35176
+ Installing routstr models in openclaw.json...`);
35177
+ const port = config.port || 8008;
35178
+ const state = store.getState();
35179
+ const existingClient = (state.clientIds || []).find((c) => c.clientId === clientId);
35180
+ let apiKey;
35181
+ if (existingClient) {
35182
+ apiKey = existingClient.apiKey;
35183
+ logger3.log(`Using existing API key for ${name}`);
35184
+ } else {
35185
+ apiKey = generateApiKey2();
35186
+ store.getState().setClientIds((prev) => [
35187
+ ...prev || [],
35188
+ {
35189
+ clientId,
35190
+ name,
35191
+ apiKey,
35192
+ createdAt: Date.now()
35193
+ }
35194
+ ]);
35195
+ logger3.log(`Created new API key for ${name}`);
35196
+ }
35197
+ let openclawConfig = {};
35198
+ try {
35199
+ if (existsSync4(configPath)) {
35200
+ const content2 = await readFile2(configPath, "utf-8");
35201
+ openclawConfig = JSON.parse(content2);
35202
+ }
35203
+ } catch {
35204
+ openclawConfig = {};
35205
+ }
35206
+ if (!openclawConfig.models) {
35207
+ openclawConfig.models = {};
35208
+ }
35209
+ if (!openclawConfig.models.providers) {
35210
+ openclawConfig.models.providers = {};
35211
+ }
35212
+ if (!openclawConfig.agents) {
35213
+ openclawConfig.agents = {};
35214
+ }
35215
+ if (!openclawConfig.agents.defaults) {
35216
+ openclawConfig.agents.defaults = {};
35217
+ }
35218
+ try {
35219
+ mkdirSync2(dirname2(configPath), { recursive: true });
35220
+ const response = await fetch(`http://localhost:${port}/models`);
35221
+ const data = await response.json();
35222
+ const models = data.output?.models || [];
35223
+ if (models.length === 0) {
35224
+ logger3.log("No models found from routstr daemon.");
35225
+ return;
35226
+ }
35227
+ const providerModels = models.map((model2) => ({
35228
+ id: model2.id,
35229
+ name: model2.name || model2.id,
35230
+ reasoning: true
35231
+ }));
35232
+ openclawConfig.models.providers[OPENCLAW_PROVIDER_ID] = {
35233
+ baseUrl: `http://localhost:${port}/v1`,
35234
+ apiKey,
35235
+ api: "openai-completions",
35236
+ models: providerModels
35237
+ };
35238
+ const availableModelIds = new Set(providerModels.map((model2) => model2.id));
35239
+ const primaryId = availableModelIds.has("gpt-5.3-codex") ? "gpt-5.3-codex" : providerModels[0]?.id;
35240
+ const fallbackId = availableModelIds.has("minimax-m2.5") ? "minimax-m2.5" : providerModels.find((model2) => model2.id !== primaryId)?.id;
35241
+ if (primaryId) {
35242
+ openclawConfig.agents.defaults.model = {
35243
+ primary: `${OPENCLAW_PROVIDER_ID}/${primaryId}`,
35244
+ fallbacks: fallbackId ? [`${OPENCLAW_PROVIDER_ID}/${fallbackId}`] : []
35245
+ };
35246
+ } else {
35247
+ openclawConfig.agents.defaults.model = {
35248
+ primary: OPENCLAW_DEFAULT_PRIMARY_MODEL,
35249
+ fallbacks: [OPENCLAW_DEFAULT_FALLBACK_MODEL]
35250
+ };
35251
+ }
35252
+ await writeFile2(configPath, JSON.stringify(openclawConfig, null, 2));
35253
+ logger3.log(`Added "${OPENCLAW_PROVIDER_ID}" provider with ${models.length} models to openclaw.json`);
35254
+ } catch (error) {
35255
+ logger3.error("Failed to install models in openclaw.json:", error);
35256
+ }
35257
+ }
35258
+
35259
+ // src/integrations/registry.ts
35260
+ function generateApiKey2() {
35261
+ const bytes4 = randomBytes5(24);
35262
+ return `sk-${bytes4.toString("hex")}`;
35263
+ }
35264
+ var CLIENT_CONFIGS = {
35265
+ opencode: {
35266
+ clientId: "opencode",
35267
+ name: "OpenCode",
35268
+ configPath: join4(process.env.HOME || "", ".config/opencode/opencode.json")
35269
+ },
35270
+ "pi-agent": {
35271
+ clientId: "pi-agent",
35272
+ name: "Pi Agent",
35273
+ configPath: join4(process.env.HOME || "", ".pi/agent/models.json")
35274
+ },
35275
+ openclaw: {
35276
+ clientId: "openclaw",
35277
+ name: "OpenClaw",
35278
+ configPath: join4(process.env.HOME || "", ".openclaw/openclaw.json")
35279
+ }
35280
+ };
35281
+ var CLIENT_INTEGRATIONS = {
35282
+ opencode: installOpencodeIntegration,
35283
+ "pi-agent": installPiIntegration,
35284
+ openclaw: installOpenClawIntegration
35285
+ };
35286
+ async function runIntegrationsForClients(clientIds, config, store) {
35287
+ for (const client2 of clientIds) {
35288
+ const integrationFn = CLIENT_INTEGRATIONS[client2.clientId];
35289
+ const integrationConfig = CLIENT_CONFIGS[client2.clientId];
35290
+ if (integrationFn && integrationConfig) {
35291
+ try {
35292
+ await integrationFn(config, store, integrationConfig);
35293
+ } catch (error) {
35294
+ console.error(`Integration failed for ${client2.clientId}:`, error);
35295
+ }
35296
+ }
35297
+ }
35298
+ }
35299
+
35300
+ // src/integrations/opencode.ts
35301
+ var OPENCODE_SMALL_MODEL = "routstr/minimax-m2.5";
35302
+ async function installOpencodeIntegration(config, store, integrationConfig) {
35303
+ const { clientId, name, configPath } = integrationConfig;
35304
+ logger3.log(`
35305
+ Installing routstr models in opencode.json...`);
35306
+ const port = config.port || 8008;
35307
+ const state = store.getState();
35308
+ const existingClient = (state.clientIds || []).find((c) => c.clientId === clientId);
35309
+ let apiKey;
35310
+ if (existingClient) {
35311
+ apiKey = existingClient.apiKey;
35312
+ logger3.log(`Using existing API key for ${name}`);
35313
+ } else {
35314
+ apiKey = generateApiKey2();
35315
+ store.getState().setClientIds((prev) => [
35316
+ ...prev || [],
35317
+ {
35318
+ clientId,
35319
+ name,
35320
+ apiKey,
35321
+ createdAt: Date.now()
35322
+ }
35323
+ ]);
35324
+ logger3.log(`Created new API key for ${name}`);
35325
+ }
35326
+ let opencodeConfig;
35327
+ try {
35328
+ if (existsSync5(configPath)) {
35329
+ const content2 = await readFile3(configPath, "utf-8");
35330
+ opencodeConfig = JSON.parse(content2);
35331
+ } else {
35332
+ opencodeConfig = { provider: {} };
35333
+ }
35334
+ } catch {
35335
+ opencodeConfig = { provider: {} };
35336
+ }
35337
+ if (!opencodeConfig.provider) {
35338
+ opencodeConfig.provider = {};
35339
+ }
35340
+ try {
35341
+ mkdirSync3(dirname3(configPath), { recursive: true });
35342
+ const response = await fetch(`http://localhost:${port}/models`);
35343
+ const data = await response.json();
35344
+ const models = data.output?.models || [];
35345
+ if (models.length === 0) {
35346
+ logger3.log("No models found from routstr daemon.");
35347
+ return;
35348
+ }
35349
+ const modelsObj = {};
35350
+ for (const model2 of models) {
35351
+ modelsObj[model2.id] = { name: model2.name || model2.id };
35352
+ }
35353
+ opencodeConfig.provider["routstr"] = {
35354
+ npm: "@ai-sdk/openai-compatible",
35355
+ name: "routstr",
35356
+ options: {
35357
+ baseURL: `http://localhost:${port}/`,
35358
+ apiKey,
35359
+ includeUsage: true
35360
+ },
35361
+ models: modelsObj
35362
+ };
35363
+ opencodeConfig.small_model = OPENCODE_SMALL_MODEL;
35364
+ await writeFile3(configPath, JSON.stringify(opencodeConfig, null, 2));
35365
+ logger3.log(`Added "routstr" provider with ${models.length} models to opencode.json`);
35366
+ } catch (error) {
35367
+ logger3.error("Failed to install models in opencode.json:", error);
35368
+ }
35369
+ }
34351
35370
  // src/daemon/index.ts
35371
+ init_dist3();
34352
35372
  async function main() {
34353
35373
  const args = parseArgs(process.argv);
34354
35374
  const config = await loadDaemonConfig();
@@ -34369,13 +35389,19 @@ async function main() {
34369
35389
  const providerRegistry = createProviderRegistryFromStore(store);
34370
35390
  const storageAdapter = createStorageAdapterFromStore(store);
34371
35391
  const modelManager = new ModelManager(discoveryAdapter);
35392
+ const providerManager = new ProviderManager(providerRegistry, store);
34372
35393
  const { ensureProvidersBootstrapped, getRoutstr21Models } = createModelService(modelManager);
34373
- const walletAdapter = await createWalletAdapter();
35394
+ const walletClient = createCocodClient({ cocodPath: config.cocodPath });
35395
+ const walletAdapter = await createWalletAdapter({
35396
+ cocodPath: config.cocodPath,
35397
+ walletClient
35398
+ });
34374
35399
  const server = createServer();
34375
35400
  server.on("request", createDaemonRequestHandler({
34376
35401
  provider,
34377
35402
  server,
34378
35403
  store,
35404
+ walletClient,
34379
35405
  walletAdapter,
34380
35406
  storageAdapter,
34381
35407
  providerRegistry,
@@ -34383,18 +35409,17 @@ async function main() {
34383
35409
  modelManager,
34384
35410
  ensureProvidersBootstrapped,
34385
35411
  getRoutstr21Models,
34386
- runWalletCommand,
34387
- parseBalances,
34388
35412
  mode: config.mode || "apikeys",
34389
- usageTrackingDriver
35413
+ usageTrackingDriver,
35414
+ providerManager
34390
35415
  }));
34391
35416
  Bun.write(PID_FILE, String(process.pid));
34392
35417
  try {
34393
- if (existsSync3(SOCKET_PATH)) {
35418
+ if (existsSync6(SOCKET_PATH)) {
34394
35419
  Bun.spawn(["rm", SOCKET_PATH]);
34395
35420
  }
34396
35421
  } catch {}
34397
- const REFRESH_INTERVAL_MS = 12600000;
35422
+ const REFRESH_INTERVAL_MS = 1260000;
34398
35423
  let refreshInterval = null;
34399
35424
  const startModelRefreshJob = () => {
34400
35425
  logger3.log(`Starting recurring model refresh job (every ${REFRESH_INTERVAL_MS / 1000 / 60 / 60} hours)`);
@@ -34403,6 +35428,13 @@ async function main() {
34403
35428
  try {
34404
35429
  await getRoutstr21Models(true);
34405
35430
  logger3.log("Scheduled model refresh completed successfully.");
35431
+ const state = store.getState();
35432
+ const clientIds = state.clientIds || [];
35433
+ if (clientIds.length > 0) {
35434
+ logger3.log(`Refreshing ${clientIds.length} client integration(s)...`);
35435
+ await runIntegrationsForClients(clientIds, updatedConfig, store);
35436
+ logger3.log("Client integrations refreshed.");
35437
+ }
34406
35438
  } catch (error) {
34407
35439
  logger3.error("Scheduled model refresh failed:", error);
34408
35440
  }
@@ -34415,8 +35447,51 @@ async function main() {
34415
35447
  logger3.log("Stopped recurring model refresh job.");
34416
35448
  }
34417
35449
  };
35450
+ const REFUND_INTERVAL_MS = 600000;
35451
+ let refundInterval = null;
35452
+ const startRefundJob = async () => {
35453
+ logger3.log(`Starting recurring refund job (every ${REFUND_INTERVAL_MS / 1000 / 60} minutes)`);
35454
+ refundInterval = setInterval(async () => {
35455
+ logger3.log("Running scheduled refund...");
35456
+ try {
35457
+ const state = store.getState();
35458
+ const pendingDistribution = (state.cachedTokens || []).map((t) => ({
35459
+ baseUrl: t.baseUrl,
35460
+ amount: t.balance || 0
35461
+ }));
35462
+ const apiKeysStored = (state.apiKeys || []).map((k) => ({
35463
+ baseUrl: k.baseUrl,
35464
+ amount: k.balance || 0
35465
+ }));
35466
+ if (pendingDistribution.length === 0 && apiKeysStored.length === 0) {
35467
+ logger3.log("No pending tokens to refund.");
35468
+ return;
35469
+ }
35470
+ const mintUrl = walletAdapter.getActiveMintUrl();
35471
+ if (!mintUrl) {
35472
+ logger3.log("No active mint URL for refund.");
35473
+ return;
35474
+ }
35475
+ const client2 = new RoutstrClient(walletAdapter, storageAdapter, providerRegistry, "min", "apikeys");
35476
+ const spender = client2.getCashuSpender();
35477
+ const results = await spender.refundProviders(mintUrl);
35478
+ const successCount = results.filter((r) => r.success).length;
35479
+ logger3.log(`Scheduled refund completed: ${successCount}/${results.length} providers refunded.`);
35480
+ } catch (error) {
35481
+ logger3.error("Scheduled refund failed:", error);
35482
+ }
35483
+ }, REFUND_INTERVAL_MS);
35484
+ };
35485
+ const stopRefundJob = () => {
35486
+ if (refundInterval) {
35487
+ clearInterval(refundInterval);
35488
+ refundInterval = null;
35489
+ logger3.log("Stopped recurring refund job.");
35490
+ }
35491
+ };
34418
35492
  server.on("close", () => {
34419
35493
  stopModelRefreshJob();
35494
+ stopRefundJob();
34420
35495
  });
34421
35496
  server.listen(port, async () => {
34422
35497
  logger3.log(`Routstr daemon listening on http://localhost:${port}`);
@@ -34424,8 +35499,15 @@ async function main() {
34424
35499
  startModelRefreshJob();
34425
35500
  logger3.log("Running initial model refresh...");
34426
35501
  return getRoutstr21Models(true);
34427
- }).then(() => {
35502
+ }).then(async () => {
34428
35503
  logger3.log("Initial model refresh completed.");
35504
+ const state = store.getState();
35505
+ const clientIds = state.clientIds || [];
35506
+ if (clientIds.length > 0) {
35507
+ logger3.log(`Refreshing ${clientIds.length} client integration(s)...`);
35508
+ await runIntegrationsForClients(clientIds, updatedConfig, store);
35509
+ logger3.log("Client integrations refreshed.");
35510
+ }
34429
35511
  }).catch((error) => {
34430
35512
  logger3.error("Initial model refresh failed:", error);
34431
35513
  startModelRefreshJob();