routstrd 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +260 -0
- package/bun.lock +94 -38
- package/dist/daemon/index.js +35522 -0
- package/dist/index.js +4968 -382
- package/package.json +6 -5
- 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 +18 -13
- 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/src/daemon/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createServer } from "http";
|
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import {
|
|
4
4
|
ModelManager,
|
|
5
|
+
ProviderManager,
|
|
5
6
|
createDiscoveryAdapterFromStore,
|
|
6
7
|
createProviderRegistryFromStore,
|
|
7
8
|
createStorageAdapterFromStore,
|
|
@@ -10,22 +11,17 @@ import {
|
|
|
10
11
|
import { DB_PATH, SOCKET_PATH, PID_FILE } from "../utils/config";
|
|
11
12
|
import { logger } from "../utils/logger";
|
|
12
13
|
import { parseArgs } from "./args";
|
|
13
|
-
import {
|
|
14
|
-
ensureDirs,
|
|
15
|
-
loadDaemonConfig,
|
|
16
|
-
saveDaemonConfig,
|
|
17
|
-
} from "./config-store";
|
|
14
|
+
import { ensureDirs, loadDaemonConfig, saveDaemonConfig } from "./config-store";
|
|
18
15
|
import {
|
|
19
16
|
createBunSqliteDriver,
|
|
20
17
|
createBunSqliteUsageTrackingDriver,
|
|
21
18
|
} from "@routstr/sdk/storage";
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
parseBalances,
|
|
25
|
-
runWalletCommand,
|
|
26
|
-
} from "./wallet";
|
|
19
|
+
import { createWalletAdapter } from "./wallet";
|
|
20
|
+
import { createCocodClient } from "./wallet/cocod-client";
|
|
27
21
|
import { createModelService } from "./models";
|
|
28
22
|
import { createDaemonRequestHandler } from "./http";
|
|
23
|
+
import { runIntegrationsForClients } from "../integrations";
|
|
24
|
+
import { RoutstrClient } from "@routstr/sdk";
|
|
29
25
|
|
|
30
26
|
async function main(): Promise<void> {
|
|
31
27
|
const args = parseArgs(process.argv);
|
|
@@ -52,10 +48,16 @@ async function main(): Promise<void> {
|
|
|
52
48
|
const providerRegistry = createProviderRegistryFromStore(store);
|
|
53
49
|
const storageAdapter = createStorageAdapterFromStore(store);
|
|
54
50
|
const modelManager = new ModelManager(discoveryAdapter);
|
|
51
|
+
// Create shared ProviderManager for consistent failure tracking across all requests
|
|
52
|
+
const providerManager = new ProviderManager(providerRegistry, store);
|
|
55
53
|
const { ensureProvidersBootstrapped, getRoutstr21Models } =
|
|
56
54
|
createModelService(modelManager);
|
|
57
55
|
|
|
58
|
-
const
|
|
56
|
+
const walletClient = createCocodClient({ cocodPath: config.cocodPath });
|
|
57
|
+
const walletAdapter = await createWalletAdapter({
|
|
58
|
+
cocodPath: config.cocodPath,
|
|
59
|
+
walletClient,
|
|
60
|
+
});
|
|
59
61
|
|
|
60
62
|
const server = createServer();
|
|
61
63
|
server.on(
|
|
@@ -64,6 +66,7 @@ async function main(): Promise<void> {
|
|
|
64
66
|
provider,
|
|
65
67
|
server,
|
|
66
68
|
store,
|
|
69
|
+
walletClient,
|
|
67
70
|
walletAdapter,
|
|
68
71
|
storageAdapter,
|
|
69
72
|
providerRegistry,
|
|
@@ -71,10 +74,9 @@ async function main(): Promise<void> {
|
|
|
71
74
|
modelManager,
|
|
72
75
|
ensureProvidersBootstrapped,
|
|
73
76
|
getRoutstr21Models,
|
|
74
|
-
runWalletCommand,
|
|
75
|
-
parseBalances,
|
|
76
77
|
mode: config.mode || "apikeys",
|
|
77
78
|
usageTrackingDriver,
|
|
79
|
+
providerManager,
|
|
78
80
|
}),
|
|
79
81
|
);
|
|
80
82
|
|
|
@@ -88,7 +90,7 @@ async function main(): Promise<void> {
|
|
|
88
90
|
// Ignore
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
const REFRESH_INTERVAL_MS =
|
|
93
|
+
const REFRESH_INTERVAL_MS = 21 * 60 * 1000; // 21 mins
|
|
92
94
|
|
|
93
95
|
// Recurring job to refresh routstr21 models
|
|
94
96
|
let refreshInterval: ReturnType<typeof setInterval> | null = null;
|
|
@@ -103,6 +105,15 @@ async function main(): Promise<void> {
|
|
|
103
105
|
try {
|
|
104
106
|
await getRoutstr21Models(true);
|
|
105
107
|
logger.log("Scheduled model refresh completed successfully.");
|
|
108
|
+
|
|
109
|
+
// Refresh integrations for all registered clients
|
|
110
|
+
const state = store.getState();
|
|
111
|
+
const clientIds = state.clientIds || [];
|
|
112
|
+
if (clientIds.length > 0) {
|
|
113
|
+
logger.log(`Refreshing ${clientIds.length} client integration(s)...`);
|
|
114
|
+
await runIntegrationsForClients(clientIds, updatedConfig, store);
|
|
115
|
+
logger.log("Client integrations refreshed.");
|
|
116
|
+
}
|
|
106
117
|
} catch (error) {
|
|
107
118
|
logger.error("Scheduled model refresh failed:", error);
|
|
108
119
|
}
|
|
@@ -117,8 +128,77 @@ async function main(): Promise<void> {
|
|
|
117
128
|
}
|
|
118
129
|
};
|
|
119
130
|
|
|
131
|
+
// Recurring job to refund pending tokens every 10 minutes
|
|
132
|
+
const REFUND_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
|
|
133
|
+
let refundInterval: ReturnType<typeof setInterval> | null = null;
|
|
134
|
+
|
|
135
|
+
const startRefundJob = async () => {
|
|
136
|
+
logger.log(
|
|
137
|
+
`Starting recurring refund job (every ${REFUND_INTERVAL_MS / 1000 / 60} minutes)`,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
refundInterval = setInterval(async () => {
|
|
141
|
+
logger.log("Running scheduled refund...");
|
|
142
|
+
try {
|
|
143
|
+
const state = store.getState() as any;
|
|
144
|
+
const pendingDistribution = (state.cachedTokens || []).map(
|
|
145
|
+
(t: { baseUrl: string; balance?: number }) => ({
|
|
146
|
+
baseUrl: t.baseUrl,
|
|
147
|
+
amount: t.balance || 0,
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
const apiKeysStored = (state.apiKeys || []).map(
|
|
151
|
+
(k: { baseUrl: string; balance?: number }) => ({
|
|
152
|
+
baseUrl: k.baseUrl,
|
|
153
|
+
amount: k.balance || 0,
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (pendingDistribution.length === 0 && apiKeysStored.length === 0) {
|
|
158
|
+
logger.log("No pending tokens to refund.");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const mintUrl = walletAdapter.getActiveMintUrl();
|
|
163
|
+
if (!mintUrl) {
|
|
164
|
+
logger.log("No active mint URL for refund.");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const client = new RoutstrClient(
|
|
169
|
+
walletAdapter,
|
|
170
|
+
storageAdapter,
|
|
171
|
+
providerRegistry,
|
|
172
|
+
"min",
|
|
173
|
+
"apikeys",
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const spender = client.getCashuSpender();
|
|
177
|
+
const results = await spender.refundProviders(mintUrl);
|
|
178
|
+
|
|
179
|
+
const successCount = results.filter(
|
|
180
|
+
(r: { success: boolean }) => r.success,
|
|
181
|
+
).length;
|
|
182
|
+
logger.log(
|
|
183
|
+
`Scheduled refund completed: ${successCount}/${results.length} providers refunded.`,
|
|
184
|
+
);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
logger.error("Scheduled refund failed:", error);
|
|
187
|
+
}
|
|
188
|
+
}, REFUND_INTERVAL_MS);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const stopRefundJob = () => {
|
|
192
|
+
if (refundInterval) {
|
|
193
|
+
clearInterval(refundInterval);
|
|
194
|
+
refundInterval = null;
|
|
195
|
+
logger.log("Stopped recurring refund job.");
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
120
199
|
server.on("close", () => {
|
|
121
200
|
stopModelRefreshJob();
|
|
201
|
+
stopRefundJob();
|
|
122
202
|
});
|
|
123
203
|
|
|
124
204
|
server.listen(port, async () => {
|
|
@@ -128,17 +208,27 @@ async function main(): Promise<void> {
|
|
|
128
208
|
void ensureProvidersBootstrapped()
|
|
129
209
|
.then(() => {
|
|
130
210
|
startModelRefreshJob();
|
|
211
|
+
// startRefundJob(); DISABLING refund job for now.
|
|
131
212
|
// Run an immediate refresh to populate models right away
|
|
132
213
|
logger.log("Running initial model refresh...");
|
|
133
214
|
return getRoutstr21Models(true);
|
|
134
215
|
})
|
|
135
|
-
.then(() => {
|
|
216
|
+
.then(async () => {
|
|
136
217
|
logger.log("Initial model refresh completed.");
|
|
218
|
+
// Refresh integrations for all registered clients after initial bootstrap
|
|
219
|
+
const state = store.getState();
|
|
220
|
+
const clientIds = state.clientIds || [];
|
|
221
|
+
if (clientIds.length > 0) {
|
|
222
|
+
logger.log(`Refreshing ${clientIds.length} client integration(s)...`);
|
|
223
|
+
await runIntegrationsForClients(clientIds, updatedConfig, store);
|
|
224
|
+
logger.log("Client integrations refreshed.");
|
|
225
|
+
}
|
|
137
226
|
})
|
|
138
227
|
.catch((error) => {
|
|
139
228
|
logger.error("Initial model refresh failed:", error);
|
|
140
|
-
// Still start the
|
|
229
|
+
// Still start the jobs even if initial refresh fails
|
|
141
230
|
startModelRefreshJob();
|
|
231
|
+
// startRefundJob(); DISABLING refund job for now.
|
|
142
232
|
});
|
|
143
233
|
});
|
|
144
234
|
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import { logger } from "../../utils/logger";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_CONFIG_DIR = `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`;
|
|
6
|
+
const DEFAULT_SOCKET_PATH =
|
|
7
|
+
process.env.COCOD_SOCKET || `${DEFAULT_CONFIG_DIR}/cocod.sock`;
|
|
8
|
+
|
|
9
|
+
type UnixRequestInit = RequestInit & { unix: string };
|
|
10
|
+
|
|
11
|
+
type CommandResponse<T> = {
|
|
12
|
+
output?: T;
|
|
13
|
+
error?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type CocodFetch = (
|
|
17
|
+
input: string | URL | Request,
|
|
18
|
+
init?: UnixRequestInit,
|
|
19
|
+
) => Promise<Response>;
|
|
20
|
+
|
|
21
|
+
type SpawnedProcess = {
|
|
22
|
+
exited: Promise<number>;
|
|
23
|
+
unref?: () => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type SpawnDaemon = (
|
|
27
|
+
args: string[],
|
|
28
|
+
env: Record<string, string>,
|
|
29
|
+
) => SpawnedProcess;
|
|
30
|
+
|
|
31
|
+
export type CocodState = "UNINITIALIZED" | "LOCKED" | "UNLOCKED" | "ERROR";
|
|
32
|
+
|
|
33
|
+
export type CocodBalanceOutput = Record<string, { sats?: number } | number>;
|
|
34
|
+
|
|
35
|
+
export class CocodHttpError extends Error {
|
|
36
|
+
status: number;
|
|
37
|
+
|
|
38
|
+
constructor(status: number, message: string) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "CocodHttpError";
|
|
41
|
+
this.status = status;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CocodClient {
|
|
46
|
+
ping(): Promise<boolean>;
|
|
47
|
+
getStatus(): Promise<CocodState>;
|
|
48
|
+
unlock(passphrase: string): Promise<string>;
|
|
49
|
+
getBalances(): Promise<Record<string, number>>;
|
|
50
|
+
receiveCashu(token: string): Promise<string>;
|
|
51
|
+
receiveBolt11(amount: number, mintUrl?: string): Promise<string>;
|
|
52
|
+
sendCashu(amount: number, mintUrl?: string): Promise<string>;
|
|
53
|
+
sendBolt11(invoice: string, mintUrl?: string): Promise<string>;
|
|
54
|
+
listMints(): Promise<string[]>;
|
|
55
|
+
addMint(url: string): Promise<string>;
|
|
56
|
+
getMintInfo(url: string): Promise<unknown>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolveCocodExecutable(cocodPath?: string | null): string {
|
|
60
|
+
const trimmed = cocodPath?.trim();
|
|
61
|
+
return trimmed || "cocod";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function isCocodInstalled(
|
|
65
|
+
cocodPath?: string | null,
|
|
66
|
+
): Promise<boolean> {
|
|
67
|
+
const executable = resolveCocodExecutable(cocodPath);
|
|
68
|
+
|
|
69
|
+
if (executable.includes("/")) {
|
|
70
|
+
return existsSync(executable);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const proc = Bun.spawn({
|
|
75
|
+
cmd: ["which", executable],
|
|
76
|
+
stdout: "ignore",
|
|
77
|
+
stderr: "ignore",
|
|
78
|
+
});
|
|
79
|
+
return (await proc.exited) === 0;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function normalizeBalances(
|
|
86
|
+
output: CocodBalanceOutput | undefined,
|
|
87
|
+
): Record<string, number> {
|
|
88
|
+
if (!output) return {};
|
|
89
|
+
|
|
90
|
+
return Object.fromEntries(
|
|
91
|
+
Object.entries(output).map(([mintUrl, value]) => {
|
|
92
|
+
if (typeof value === "number") {
|
|
93
|
+
return [mintUrl, value];
|
|
94
|
+
}
|
|
95
|
+
return [mintUrl, Number(value?.sats ?? 0)];
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseMintList(output: string | undefined): string[] {
|
|
101
|
+
return (output || "")
|
|
102
|
+
.split("\n")
|
|
103
|
+
.map((line) => line.trim())
|
|
104
|
+
.filter(Boolean);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function delay(ms: number): Promise<void> {
|
|
108
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function toErrorText(value: unknown): string {
|
|
112
|
+
if (typeof value === "string") {
|
|
113
|
+
return value.trim();
|
|
114
|
+
}
|
|
115
|
+
if (value === null || value === undefined) {
|
|
116
|
+
return "";
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
return JSON.stringify(value);
|
|
120
|
+
} catch {
|
|
121
|
+
return String(value);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function tokenFingerprint(token: string): string {
|
|
126
|
+
return createHash("sha256").update(token).digest("hex").slice(0, 12);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function createCocodClient(
|
|
130
|
+
options: {
|
|
131
|
+
cocodPath?: string | null;
|
|
132
|
+
socketPath?: string;
|
|
133
|
+
fetchImpl?: CocodFetch;
|
|
134
|
+
spawnDaemon?: SpawnDaemon;
|
|
135
|
+
pollIntervalMs?: number;
|
|
136
|
+
startupTimeoutMs?: number;
|
|
137
|
+
} = {},
|
|
138
|
+
): CocodClient {
|
|
139
|
+
const executable = resolveCocodExecutable(options.cocodPath);
|
|
140
|
+
const socketPath = options.socketPath || DEFAULT_SOCKET_PATH;
|
|
141
|
+
const fetchImpl = options.fetchImpl || (fetch as CocodFetch);
|
|
142
|
+
const pollIntervalMs = options.pollIntervalMs ?? 100;
|
|
143
|
+
const startupTimeoutMs = options.startupTimeoutMs ?? 5000;
|
|
144
|
+
|
|
145
|
+
const spawnDaemon: SpawnDaemon =
|
|
146
|
+
options.spawnDaemon ||
|
|
147
|
+
((args, env) => {
|
|
148
|
+
const proc = Bun.spawn(args, {
|
|
149
|
+
stdin: "ignore",
|
|
150
|
+
stdout: "ignore",
|
|
151
|
+
stderr: "ignore",
|
|
152
|
+
detached: true,
|
|
153
|
+
env,
|
|
154
|
+
});
|
|
155
|
+
proc.unref();
|
|
156
|
+
return proc;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
let startPromise: Promise<void> | null = null;
|
|
160
|
+
|
|
161
|
+
async function fetchJson<T>(
|
|
162
|
+
path: string,
|
|
163
|
+
init: Omit<UnixRequestInit, "unix"> = {},
|
|
164
|
+
): Promise<CommandResponse<T>> {
|
|
165
|
+
const method = init.method || "GET";
|
|
166
|
+
const requestInit: UnixRequestInit = {
|
|
167
|
+
...init,
|
|
168
|
+
unix: socketPath,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const response = await fetchImpl(`http://localhost${path}`, requestInit);
|
|
172
|
+
const rawText = await response.text();
|
|
173
|
+
// logger.debug(
|
|
174
|
+
// `[fetchJson] ${method} ${path} status=${response.status} body=${rawText}`,
|
|
175
|
+
// );
|
|
176
|
+
|
|
177
|
+
if (!rawText.trim()) {
|
|
178
|
+
throw new CocodHttpError(
|
|
179
|
+
response.ok ? 502 : response.status,
|
|
180
|
+
`Empty response from cocod for ${method} ${path}`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let data: CommandResponse<T>;
|
|
185
|
+
try {
|
|
186
|
+
data = JSON.parse(rawText) as CommandResponse<T>;
|
|
187
|
+
} catch {
|
|
188
|
+
throw new CocodHttpError(
|
|
189
|
+
response.ok ? 502 : response.status,
|
|
190
|
+
`Invalid JSON response from cocod for ${method} ${path}`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!data || typeof data !== "object") {
|
|
195
|
+
throw new CocodHttpError(
|
|
196
|
+
response.ok ? 502 : response.status,
|
|
197
|
+
`Unexpected response shape from cocod for ${method} ${path}`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const errorMessage = toErrorText((data as CommandResponse<T>).error);
|
|
202
|
+
if (errorMessage) {
|
|
203
|
+
throw new CocodHttpError(
|
|
204
|
+
response.ok ? 400 : response.status,
|
|
205
|
+
errorMessage,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!response.ok) {
|
|
210
|
+
throw new CocodHttpError(
|
|
211
|
+
response.status,
|
|
212
|
+
data.error || response.statusText || `HTTP ${response.status}`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return data;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function pingInternal(): Promise<boolean> {
|
|
220
|
+
try {
|
|
221
|
+
await fetchJson<string>("/ping");
|
|
222
|
+
return true;
|
|
223
|
+
} catch {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function startDaemon(): Promise<void> {
|
|
229
|
+
const env = { ...process.env, COCOD_SOCKET: socketPath };
|
|
230
|
+
const proc = spawnDaemon([executable, "daemon"], env);
|
|
231
|
+
const maxPolls = Math.ceil(startupTimeoutMs / pollIntervalMs);
|
|
232
|
+
let exitCode: number | null = null;
|
|
233
|
+
|
|
234
|
+
void proc.exited.then((code) => {
|
|
235
|
+
exitCode = code;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
for (let i = 0; i < maxPolls; i++) {
|
|
239
|
+
await delay(pollIntervalMs);
|
|
240
|
+
|
|
241
|
+
if (exitCode !== null) {
|
|
242
|
+
throw new Error(`cocod daemon exited early with code ${exitCode}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (await pingInternal()) {
|
|
246
|
+
logger.debug(`Connected to cocod daemon on ${socketPath}`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
throw new Error(
|
|
252
|
+
`cocod daemon failed to start within ${Math.round(startupTimeoutMs / 1000)} seconds`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function ensureDaemonRunning(): Promise<void> {
|
|
257
|
+
if (await pingInternal()) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!startPromise) {
|
|
262
|
+
logger.debug(`Starting cocod daemon via ${executable}...`);
|
|
263
|
+
startPromise = startDaemon().finally(() => {
|
|
264
|
+
startPromise = null;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
await startPromise;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function callDaemon<T>(
|
|
272
|
+
path: string,
|
|
273
|
+
init: Omit<UnixRequestInit, "unix"> = {},
|
|
274
|
+
): Promise<T> {
|
|
275
|
+
await ensureDaemonRunning();
|
|
276
|
+
const response = await fetchJson<T>(path, init);
|
|
277
|
+
return response.output as T;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function post<T>(path: string, body: Record<string, unknown>): Promise<T> {
|
|
281
|
+
return callDaemon<T>(path, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers: { "Content-Type": "application/json" },
|
|
284
|
+
body: JSON.stringify(body),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
async ping(): Promise<boolean> {
|
|
290
|
+
return pingInternal();
|
|
291
|
+
},
|
|
292
|
+
async getStatus(): Promise<CocodState> {
|
|
293
|
+
return callDaemon<CocodState>("/status");
|
|
294
|
+
},
|
|
295
|
+
async unlock(passphrase: string): Promise<string> {
|
|
296
|
+
return post<string>("/unlock", { passphrase });
|
|
297
|
+
},
|
|
298
|
+
async getBalances(): Promise<Record<string, number>> {
|
|
299
|
+
const output = await callDaemon<CocodBalanceOutput>("/balance");
|
|
300
|
+
return normalizeBalances(output);
|
|
301
|
+
},
|
|
302
|
+
async receiveCashu(token: string): Promise<string> {
|
|
303
|
+
logger.debug(
|
|
304
|
+
`[receiveCashu] Receiving Cashu token ${tokenFingerprint(token)}`,
|
|
305
|
+
);
|
|
306
|
+
const message = await callDaemon<string>("/receive/cashu", {
|
|
307
|
+
method: "POST",
|
|
308
|
+
headers: { "Content-Type": "application/json" },
|
|
309
|
+
body: JSON.stringify({ token }),
|
|
310
|
+
});
|
|
311
|
+
if (typeof message !== "string" || !message.trim()) {
|
|
312
|
+
throw new CocodHttpError(
|
|
313
|
+
502,
|
|
314
|
+
"Unexpected response from cocod while receiving Cashu token.",
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
logger.debug(`[receiveCashu] Full response.output:`, message);
|
|
318
|
+
return message;
|
|
319
|
+
},
|
|
320
|
+
async receiveBolt11(amount: number, mintUrl?: string): Promise<string> {
|
|
321
|
+
return post<string>("/receive/bolt11", { amount, mintUrl });
|
|
322
|
+
},
|
|
323
|
+
async sendCashu(amount: number, mintUrl?: string): Promise<string> {
|
|
324
|
+
return post<string>("/send/cashu", { amount, mintUrl });
|
|
325
|
+
},
|
|
326
|
+
async sendBolt11(invoice: string, mintUrl?: string): Promise<string> {
|
|
327
|
+
return post<string>("/send/bolt11", { invoice, mintUrl });
|
|
328
|
+
},
|
|
329
|
+
async listMints(): Promise<string[]> {
|
|
330
|
+
const output = await callDaemon<string>("/mints/list");
|
|
331
|
+
return parseMintList(output);
|
|
332
|
+
},
|
|
333
|
+
async addMint(url: string): Promise<string> {
|
|
334
|
+
return post<string>("/mints/add", { url });
|
|
335
|
+
},
|
|
336
|
+
async getMintInfo(url: string): Promise<unknown> {
|
|
337
|
+
return post<unknown>("/mints/info", { url });
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|