routstrd 0.1.1 → 0.1.4
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/SKILL.md +260 -0
- package/bun.lock +49 -189
- package/dist/daemon/index.js +1316 -234
- package/dist/index.js +4967 -381
- package/package.json +5 -4
- package/refund.js +33 -0
- package/refund_new.js +20 -0
- package/src/cli-shared.ts +8 -5
- package/src/cli.ts +462 -74
- package/src/daemon/http/index.ts +768 -140
- package/src/daemon/index.ts +106 -16
- package/src/daemon/wallet/cocod-client.ts +340 -0
- package/src/daemon/wallet/index.ts +56 -141
- package/src/integrations/index.ts +5 -3
- package/src/integrations/openclaw.ts +16 -26
- package/src/integrations/opencode.ts +15 -24
- package/src/integrations/pi.ts +15 -25
- package/src/integrations/registry.ts +71 -0
- package/src/start-daemon.ts +17 -12
- package/src/tui/usage/app.ts +1 -1
- package/src/tui/usage/data.ts +24 -14
- package/src/tui/usage/render.ts +10 -7
- package/src/utils/config.ts +1 -1
- package/src/utils/logger.ts +15 -4
- package/test_chat.sh +29 -0
- package/src/daemon/sse.ts +0 -98
package/dist/daemon/index.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
27485
|
-
this._log("DEBUG", `[RoutstrClient] _handleErrorResponse: Initial API key balance: ${
|
|
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 &&
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
28023
|
-
await mkdir(
|
|
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(
|
|
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/
|
|
33723
|
-
|
|
33724
|
-
|
|
33725
|
-
|
|
33726
|
-
|
|
33727
|
-
|
|
33728
|
-
|
|
33729
|
-
|
|
33730
|
-
|
|
33731
|
-
|
|
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
|
-
|
|
33734
|
-
|
|
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
|
-
|
|
33737
|
-
|
|
33738
|
-
if (
|
|
33739
|
-
|
|
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
|
-
|
|
33764
|
-
|
|
33765
|
-
|
|
33766
|
-
|
|
33767
|
-
|
|
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
|
-
|
|
33775
|
-
|
|
33776
|
-
|
|
33777
|
-
|
|
33778
|
-
|
|
33779
|
-
|
|
33780
|
-
|
|
33781
|
-
|
|
33782
|
-
|
|
33783
|
-
const
|
|
33784
|
-
return
|
|
33785
|
-
|
|
33786
|
-
|
|
33787
|
-
|
|
33788
|
-
|
|
33789
|
-
}
|
|
33790
|
-
|
|
33791
|
-
|
|
33792
|
-
|
|
33793
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
33799
|
-
|
|
33800
|
-
|
|
33801
|
-
|
|
33802
|
-
|
|
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(
|
|
34134
|
+
activeMintUrl = Object.keys(nextBalances)[0] || null;
|
|
33805
34135
|
}
|
|
33806
|
-
|
|
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
|
-
|
|
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
|
|
33849
|
-
const
|
|
33850
|
-
|
|
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
|
-
|
|
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
|
|
33866
|
-
|
|
33867
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
33941
|
-
|
|
33942
|
-
|
|
33943
|
-
|
|
33944
|
-
|
|
33945
|
-
|
|
33946
|
-
|
|
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
|
-
|
|
33949
|
-
|
|
33950
|
-
|
|
33951
|
-
|
|
34410
|
+
balances,
|
|
34411
|
+
unit: "sat",
|
|
34412
|
+
activeMint: deps.walletAdapter.getActiveMintUrl(),
|
|
34413
|
+
walletState: "UNLOCKED"
|
|
33952
34414
|
}
|
|
33953
|
-
}
|
|
33954
|
-
}
|
|
33955
|
-
|
|
33956
|
-
|
|
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
|
-
|
|
33959
|
-
|
|
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
|
|
33971
|
-
res.end(JSON.stringify({ output: { models } }));
|
|
34493
|
+
sendJson(res, 200, { output: { models } });
|
|
33972
34494
|
} catch (error) {
|
|
33973
|
-
res
|
|
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
|
|
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
|
|
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
|
|
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
|
|
34006
|
-
const
|
|
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
|
|
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
|
|
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
|
-
|
|
34048
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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 === "/
|
|
34887
|
+
if (req.method === "GET" && url2.pathname === "/clients") {
|
|
34216
34888
|
try {
|
|
34217
|
-
const
|
|
34218
|
-
const
|
|
34219
|
-
const
|
|
34220
|
-
|
|
34221
|
-
|
|
34222
|
-
|
|
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
|
-
|
|
34227
|
-
|
|
34228
|
-
|
|
34229
|
-
|
|
34230
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
34283
|
-
requestBody = bodyText ? JSON.parse(bodyText) : {};
|
|
35024
|
+
requestBody = await readJsonBody(req);
|
|
34284
35025
|
} catch (error) {
|
|
34285
|
-
res
|
|
34286
|
-
res.end(JSON.stringify({
|
|
35026
|
+
sendJson(res, 400, {
|
|
34287
35027
|
error: "Invalid JSON body.",
|
|
34288
|
-
details:
|
|
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
|
|
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
|
|
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 (
|
|
35418
|
+
if (existsSync6(SOCKET_PATH)) {
|
|
34394
35419
|
Bun.spawn(["rm", SOCKET_PATH]);
|
|
34395
35420
|
}
|
|
34396
35421
|
} catch {}
|
|
34397
|
-
const REFRESH_INTERVAL_MS =
|
|
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();
|