@toon-protocol/client-mcp 0.31.1 → 0.32.0
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/README.md +48 -7
- package/dist/{chunk-MI2IWKHX.js → chunk-ALMTQYYJ.js} +104 -4
- package/dist/{chunk-MI2IWKHX.js.map → chunk-ALMTQYYJ.js.map} +1 -1
- package/dist/{chunk-SECSBCDA.js → chunk-AYZWVPNQ.js} +853 -187
- package/dist/chunk-AYZWVPNQ.js.map +1 -0
- package/dist/{chunk-JOGMX4IT.js → chunk-CPVOJ7FE.js} +130 -5
- package/dist/chunk-CPVOJ7FE.js.map +1 -0
- package/dist/daemon.js +7 -4
- package/dist/daemon.js.map +1 -1
- package/dist/index.d.ts +309 -72
- package/dist/index.js +12 -4
- package/dist/mcp.js +2 -2
- package/package.json +3 -3
- package/dist/chunk-JOGMX4IT.js.map +0 -1
- package/dist/chunk-SECSBCDA.js.map +0 -1
|
@@ -1,14 +1,119 @@
|
|
|
1
1
|
import { createRequire as __cr } from 'module'; const require = __cr(import.meta.url);
|
|
2
2
|
import {
|
|
3
|
+
DEFAULT_KEYSTORE_PASSWORD,
|
|
4
|
+
ILP_PEER_INFO_KIND,
|
|
3
5
|
ToonError,
|
|
4
6
|
buildIlpPrepare,
|
|
7
|
+
configDir,
|
|
5
8
|
decodeEventFromToon,
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
defaultConfigPath,
|
|
10
|
+
deriveFullIdentity,
|
|
11
|
+
encodeEventToToon,
|
|
12
|
+
generateKeystore,
|
|
13
|
+
parseIlpPeerInfo,
|
|
14
|
+
readConfigFile
|
|
15
|
+
} from "./chunk-ALMTQYYJ.js";
|
|
16
|
+
import {
|
|
17
|
+
startManagedAnonProxy
|
|
18
|
+
} from "./chunk-SKQTKZIH.js";
|
|
8
19
|
import {
|
|
9
20
|
__require
|
|
10
21
|
} from "./chunk-F22GNSF6.js";
|
|
11
22
|
|
|
23
|
+
// src/daemon/first-run.ts
|
|
24
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
25
|
+
import { dirname, join } from "path";
|
|
26
|
+
function defaultKeystorePath() {
|
|
27
|
+
return join(configDir(), "keystore.json");
|
|
28
|
+
}
|
|
29
|
+
function hasConfiguredIdentity(file) {
|
|
30
|
+
return Boolean(
|
|
31
|
+
process.env["TOON_CLIENT_MNEMONIC"]?.trim() || file.keystorePath || file.mnemonic
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
var CONFIG_HELP = {
|
|
35
|
+
transport: "Point btpUrl/relayUrl at your apex. For an .anyone hidden service use ws://<host>.anyone:3000/btp + ws://<host>.anyone:7100 \u2014 the daemon auto-routes through a managed anon proxy. For a direct apex use ws://<host>:3000/btp + ws://<host>:7100.",
|
|
36
|
+
btpUrl: "REQUIRED. BTP WebSocket URL of the apex/connector for paid writes.",
|
|
37
|
+
relayUrl: "Town relay WebSocket URL for FREE reads. Default ws://localhost:7100.",
|
|
38
|
+
managedAnonProxy: "Optional override \u2014 leave unset to auto-infer from btpUrl (.anyone host \u2192 managed anon proxy, anything else \u2192 direct).",
|
|
39
|
+
keystorePath: "Auto-generated encrypted identity. Do not hand-edit; back up your seed phrase."
|
|
40
|
+
};
|
|
41
|
+
async function scaffoldFirstRun(opts = {}) {
|
|
42
|
+
const log = opts.log ?? ((m) => console.error(m));
|
|
43
|
+
const configPath = opts.configPath ?? process.env["TOON_CLIENT_CONFIG"] ?? defaultConfigPath();
|
|
44
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
45
|
+
const file = readConfigFile(configPath);
|
|
46
|
+
let updated = { ...file };
|
|
47
|
+
let changed = false;
|
|
48
|
+
if (!hasConfiguredIdentity(file)) {
|
|
49
|
+
const keystorePath = defaultKeystorePath();
|
|
50
|
+
const envPassword = process.env["TOON_CLIENT_KEYSTORE_PASSWORD"];
|
|
51
|
+
const password = envPassword ?? DEFAULT_KEYSTORE_PASSWORD;
|
|
52
|
+
let mnemonic;
|
|
53
|
+
if (existsSync(keystorePath)) {
|
|
54
|
+
mnemonic = "";
|
|
55
|
+
log(
|
|
56
|
+
`[toon-clientd] first run: relinking existing keystore at ${keystorePath}`
|
|
57
|
+
);
|
|
58
|
+
} else {
|
|
59
|
+
const generated = generateKeystore(keystorePath, password);
|
|
60
|
+
mnemonic = generated.mnemonic;
|
|
61
|
+
}
|
|
62
|
+
updated = {
|
|
63
|
+
...updated,
|
|
64
|
+
keystorePath,
|
|
65
|
+
// Only flag auto-password when WE chose it, so a user-imported keystore
|
|
66
|
+
// (custom password) still requires the env var.
|
|
67
|
+
...envPassword ? {} : { keystoreAutoPassword: true }
|
|
68
|
+
};
|
|
69
|
+
changed = true;
|
|
70
|
+
if (mnemonic) {
|
|
71
|
+
await printNewIdentity(mnemonic, keystorePath, Boolean(envPassword), log);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!existsSync(configPath)) {
|
|
75
|
+
updated = {
|
|
76
|
+
_help: CONFIG_HELP,
|
|
77
|
+
btpUrl: "",
|
|
78
|
+
relayUrl: "ws://localhost:7100",
|
|
79
|
+
...updated
|
|
80
|
+
};
|
|
81
|
+
changed = true;
|
|
82
|
+
log(
|
|
83
|
+
`[toon-clientd] wrote starter config at ${configPath} \u2014 set "btpUrl" to your apex before publishing.`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (changed) writeConfigFile(configPath, updated);
|
|
87
|
+
}
|
|
88
|
+
async function printNewIdentity(mnemonic, keystorePath, hasEnvPassword, log) {
|
|
89
|
+
const id = await deriveFullIdentity(mnemonic);
|
|
90
|
+
const lines = [
|
|
91
|
+
"",
|
|
92
|
+
"\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
|
|
93
|
+
" TOON client: generated a new identity (first run)",
|
|
94
|
+
"\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
|
|
95
|
+
` Nostr pubkey : ${id.nostr.pubkey}`,
|
|
96
|
+
` EVM address : ${id.evm.address}`,
|
|
97
|
+
...id.solana.publicKey ? [` Solana : ${id.solana.publicKey}`] : [],
|
|
98
|
+
...id.mina.publicKey ? [` Mina : ${id.mina.publicKey}`] : [],
|
|
99
|
+
"",
|
|
100
|
+
" Seed phrase (BACK THIS UP \u2014 shown only once):",
|
|
101
|
+
` ${mnemonic}`,
|
|
102
|
+
"",
|
|
103
|
+
` Encrypted keystore: ${keystorePath}`,
|
|
104
|
+
hasEnvPassword ? " Encrypted with TOON_CLIENT_KEYSTORE_PASSWORD." : " Encrypted with the default password (set TOON_CLIENT_KEYSTORE_PASSWORD\n + re-import to use your own). Identity reloads automatically on restart.",
|
|
105
|
+
"\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
|
|
106
|
+
""
|
|
107
|
+
];
|
|
108
|
+
log(lines.join("\n"));
|
|
109
|
+
}
|
|
110
|
+
function writeConfigFile(path, file) {
|
|
111
|
+
writeFileSync(path, JSON.stringify(file, null, 2) + "\n", {
|
|
112
|
+
encoding: "utf8",
|
|
113
|
+
mode: 384
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
12
117
|
// src/relay-subscription.ts
|
|
13
118
|
import { createRequire } from "module";
|
|
14
119
|
var nodeRequire = createRequire(import.meta.url);
|
|
@@ -25,6 +130,7 @@ var RelaySubscription = class {
|
|
|
25
130
|
log;
|
|
26
131
|
wsFactory;
|
|
27
132
|
decodeEvent;
|
|
133
|
+
onEvent;
|
|
28
134
|
/** Active subscriptions: subId -> filters (re-sent on every (re)connect). */
|
|
29
135
|
subscriptions = /* @__PURE__ */ new Map();
|
|
30
136
|
/** Ring buffer of received events, ordered by ascending `seq`. */
|
|
@@ -47,6 +153,7 @@ var RelaySubscription = class {
|
|
|
47
153
|
this.log = opts.logger ?? noop;
|
|
48
154
|
this.wsFactory = opts.wsFactory ?? defaultWebSocketFactory(this.socksProxy);
|
|
49
155
|
this.decodeEvent = opts.decodeEvent;
|
|
156
|
+
this.onEvent = opts.onEvent;
|
|
50
157
|
}
|
|
51
158
|
/** Whether the underlying socket is currently open. */
|
|
52
159
|
isConnected() {
|
|
@@ -147,7 +254,7 @@ var RelaySubscription = class {
|
|
|
147
254
|
}
|
|
148
255
|
scheduleReconnect() {
|
|
149
256
|
if (this.closing || this.reconnectTimer) return;
|
|
150
|
-
const
|
|
257
|
+
const delay2 = Math.min(
|
|
151
258
|
this.reconnectMaxMs,
|
|
152
259
|
this.reconnectBaseMs * 2 ** this.reconnectAttempts
|
|
153
260
|
);
|
|
@@ -155,7 +262,7 @@ var RelaySubscription = class {
|
|
|
155
262
|
this.reconnectTimer = setTimeout(() => {
|
|
156
263
|
this.reconnectTimer = null;
|
|
157
264
|
if (!this.closing) this.open();
|
|
158
|
-
},
|
|
265
|
+
}, delay2);
|
|
159
266
|
this.reconnectTimer.unref?.();
|
|
160
267
|
}
|
|
161
268
|
sendReq(subId, filters) {
|
|
@@ -227,6 +334,7 @@ var RelaySubscription = class {
|
|
|
227
334
|
const evicted = this.buffer.shift();
|
|
228
335
|
if (evicted) this.seen.delete(evicted.event.id);
|
|
229
336
|
}
|
|
337
|
+
this.onEvent?.(subId, event);
|
|
230
338
|
}
|
|
231
339
|
};
|
|
232
340
|
function toText(data) {
|
|
@@ -1114,8 +1222,8 @@ function getRandomValues(buf) {
|
|
|
1114
1222
|
}
|
|
1115
1223
|
|
|
1116
1224
|
// src/daemon/apex-channel-store.ts
|
|
1117
|
-
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
1118
|
-
import { dirname } from "path";
|
|
1225
|
+
import { mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1226
|
+
import { dirname as dirname2 } from "path";
|
|
1119
1227
|
function key(destination, chain) {
|
|
1120
1228
|
return `${destination}|${chain}`;
|
|
1121
1229
|
}
|
|
@@ -1132,76 +1240,528 @@ function loadApexChannel(path, destination, chain) {
|
|
|
1132
1240
|
function saveApexChannel(path, destination, chain, record) {
|
|
1133
1241
|
const store = readStore(path);
|
|
1134
1242
|
store[key(destination, chain)] = record;
|
|
1135
|
-
|
|
1136
|
-
|
|
1243
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
1244
|
+
writeFileSync2(path, JSON.stringify(store, null, 2), { mode: 384 });
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// src/daemon/targets-store.ts
|
|
1248
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1249
|
+
import { dirname as dirname3, join as join2 } from "path";
|
|
1250
|
+
function defaultTargetsPath() {
|
|
1251
|
+
return join2(configDir(), "targets.json");
|
|
1252
|
+
}
|
|
1253
|
+
function loadTargets(path = defaultTargetsPath()) {
|
|
1254
|
+
let parsed;
|
|
1255
|
+
try {
|
|
1256
|
+
parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
1257
|
+
} catch {
|
|
1258
|
+
return { relays: [], apexes: [] };
|
|
1259
|
+
}
|
|
1260
|
+
return {
|
|
1261
|
+
relays: Array.isArray(parsed.relays) ? parsed.relays : [],
|
|
1262
|
+
apexes: Array.isArray(parsed.apexes) ? parsed.apexes : []
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
function write(path, data) {
|
|
1266
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
1267
|
+
writeFileSync3(path, JSON.stringify(data, null, 2), { mode: 384 });
|
|
1268
|
+
}
|
|
1269
|
+
function saveRelayTarget(relayUrl, path = defaultTargetsPath()) {
|
|
1270
|
+
const store = loadTargets(path);
|
|
1271
|
+
if (!store.relays.some((r) => r.relayUrl === relayUrl)) {
|
|
1272
|
+
store.relays.push({ relayUrl });
|
|
1273
|
+
write(path, store);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
function removeRelayTarget(relayUrl, path = defaultTargetsPath()) {
|
|
1277
|
+
const store = loadTargets(path);
|
|
1278
|
+
const next = store.relays.filter((r) => r.relayUrl !== relayUrl);
|
|
1279
|
+
if (next.length === store.relays.length) return false;
|
|
1280
|
+
store.relays = next;
|
|
1281
|
+
write(path, store);
|
|
1282
|
+
return true;
|
|
1283
|
+
}
|
|
1284
|
+
function saveApexTarget(target, path = defaultTargetsPath()) {
|
|
1285
|
+
const store = loadTargets(path);
|
|
1286
|
+
store.apexes = store.apexes.filter((a) => a.btpUrl !== target.btpUrl);
|
|
1287
|
+
store.apexes.push(target);
|
|
1288
|
+
write(path, store);
|
|
1289
|
+
}
|
|
1290
|
+
function removeApexTarget(btpUrl, path = defaultTargetsPath()) {
|
|
1291
|
+
const store = loadTargets(path);
|
|
1292
|
+
const next = store.apexes.filter((a) => a.btpUrl !== btpUrl);
|
|
1293
|
+
if (next.length === store.apexes.length) return false;
|
|
1294
|
+
store.apexes = next;
|
|
1295
|
+
write(path, store);
|
|
1296
|
+
return true;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// src/daemon/apex-discovery.ts
|
|
1300
|
+
var ApexDiscoveryError = class extends Error {
|
|
1301
|
+
constructor(message) {
|
|
1302
|
+
super(message);
|
|
1303
|
+
this.name = "ApexDiscoveryError";
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
async function discoverApex(params) {
|
|
1307
|
+
const { relay, ilpAddress, pubkey, chain, childPeers } = params;
|
|
1308
|
+
const timeoutMs = params.timeoutMs ?? 15e3;
|
|
1309
|
+
const pollMs = params.pollMs ?? 250;
|
|
1310
|
+
const subId = relay.subscribe(
|
|
1311
|
+
[
|
|
1312
|
+
{
|
|
1313
|
+
kinds: [ILP_PEER_INFO_KIND],
|
|
1314
|
+
...pubkey ? { authors: [pubkey] } : {}
|
|
1315
|
+
}
|
|
1316
|
+
],
|
|
1317
|
+
`apex-discovery-${ilpAddress}`
|
|
1318
|
+
);
|
|
1319
|
+
try {
|
|
1320
|
+
const deadline = Date.now() + timeoutMs;
|
|
1321
|
+
let cursor = 0;
|
|
1322
|
+
while (Date.now() < deadline) {
|
|
1323
|
+
const { events, cursor: next } = relay.getEvents({ subId, cursor });
|
|
1324
|
+
cursor = next;
|
|
1325
|
+
const match = events.find((e) => matchesApex(e, ilpAddress));
|
|
1326
|
+
if (match) return mapAnnouncement(match, { chain, childPeers });
|
|
1327
|
+
await delay(pollMs);
|
|
1328
|
+
}
|
|
1329
|
+
throw new ApexDiscoveryError(
|
|
1330
|
+
`Timed out after ${timeoutMs}ms waiting for the apex kind:${ILP_PEER_INFO_KIND} announcement for "${ilpAddress}" on the relay. Is the relay reachable and the apex online?`
|
|
1331
|
+
);
|
|
1332
|
+
} finally {
|
|
1333
|
+
relay.unsubscribe(subId);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
function matchesApex(event, ilpAddress) {
|
|
1337
|
+
if (event.kind !== ILP_PEER_INFO_KIND) return false;
|
|
1338
|
+
try {
|
|
1339
|
+
const info = parseIlpPeerInfo(event);
|
|
1340
|
+
const addrs = info.ilpAddresses ?? [info.ilpAddress];
|
|
1341
|
+
return addrs.includes(ilpAddress) || info.ilpAddress === ilpAddress;
|
|
1342
|
+
} catch {
|
|
1343
|
+
return false;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
function mapAnnouncement(event, opts) {
|
|
1347
|
+
const info = parseIlpPeerInfo(event);
|
|
1348
|
+
const chains = info.supportedChains ?? [];
|
|
1349
|
+
if (chains.length === 0) {
|
|
1350
|
+
throw new ApexDiscoveryError(
|
|
1351
|
+
`Apex "${info.ilpAddress}" announced no supportedChains \u2014 cannot settle.`
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
const chainKey = (opts.chain ? chains.find((c) => c.split(":")[0] === opts.chain) : void 0) ?? chains[0];
|
|
1355
|
+
if (!chainKey) {
|
|
1356
|
+
throw new ApexDiscoveryError(
|
|
1357
|
+
`Apex "${info.ilpAddress}" announced no usable settlement chain.`
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
const family = chainKey.split(":")[0];
|
|
1361
|
+
const settlementAddress = info.settlementAddresses?.[chainKey];
|
|
1362
|
+
if (!settlementAddress) {
|
|
1363
|
+
throw new ApexDiscoveryError(
|
|
1364
|
+
`Apex "${info.ilpAddress}" announced no settlementAddress for chain "${chainKey}".`
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
const btpUrl = info.btpEndpoint;
|
|
1368
|
+
if (!btpUrl) {
|
|
1369
|
+
throw new ApexDiscoveryError(
|
|
1370
|
+
`Apex "${info.ilpAddress}" announced an empty btpEndpoint \u2014 cannot open a BTP session.`
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
const negotiation = {
|
|
1374
|
+
destination: info.ilpAddress,
|
|
1375
|
+
peerId: info.ilpAddress.split(".").at(-1) ?? info.ilpAddress,
|
|
1376
|
+
chain: family,
|
|
1377
|
+
chainKey,
|
|
1378
|
+
// EVM chainKeys are `evm:<network>:<chainId>`; non-EVM carry no numeric id.
|
|
1379
|
+
chainId: family === "evm" ? Number(chainKey.split(":")[2] ?? 0) : 0,
|
|
1380
|
+
settlementAddress,
|
|
1381
|
+
...info.preferredTokens?.[chainKey] ? { tokenAddress: info.preferredTokens[chainKey] } : {},
|
|
1382
|
+
...info.tokenNetworks?.[chainKey] ? { tokenNetwork: info.tokenNetworks[chainKey] } : {}
|
|
1383
|
+
};
|
|
1384
|
+
return {
|
|
1385
|
+
btpUrl,
|
|
1386
|
+
negotiation,
|
|
1387
|
+
...opts.childPeers && opts.childPeers.length > 0 ? { apexChildPeers: opts.childPeers } : {}
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
function delay(ms) {
|
|
1391
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1137
1392
|
}
|
|
1138
1393
|
|
|
1139
1394
|
// src/daemon/client-runner.ts
|
|
1395
|
+
var MERGED_BUFFER = 5e3;
|
|
1140
1396
|
var ClientRunner = class {
|
|
1141
1397
|
config;
|
|
1142
|
-
|
|
1143
|
-
|
|
1398
|
+
createClient;
|
|
1399
|
+
createRelay;
|
|
1400
|
+
startReadProxy;
|
|
1144
1401
|
log;
|
|
1402
|
+
targetsPath;
|
|
1145
1403
|
startedAt = Date.now();
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1404
|
+
/** Apex write targets, keyed by btpUrl. */
|
|
1405
|
+
apexes = /* @__PURE__ */ new Map();
|
|
1406
|
+
/** Relay read targets, keyed by relayUrl. */
|
|
1407
|
+
relays = /* @__PURE__ */ new Map();
|
|
1408
|
+
/** Runner-level merged read buffer across all relays (de-duped by event.id). */
|
|
1409
|
+
merged = [];
|
|
1410
|
+
mergedSeen = /* @__PURE__ */ new Set();
|
|
1411
|
+
mergedSeq = 0;
|
|
1412
|
+
/**
|
|
1413
|
+
* Fan-out subscriptions (no relayUrl restriction): replayed onto relays added
|
|
1414
|
+
* later so a new relay immediately participates in existing reads.
|
|
1415
|
+
*/
|
|
1416
|
+
fanoutSubs = /* @__PURE__ */ new Map();
|
|
1417
|
+
subIdCounter = 0;
|
|
1418
|
+
defaultBtpUrl;
|
|
1419
|
+
defaultRelayUrl;
|
|
1420
|
+
/** Teardown for the daemon-managed read proxy (btp-direct + relay-`.anyone`). */
|
|
1421
|
+
stopReadProxy;
|
|
1422
|
+
readProxyError;
|
|
1151
1423
|
stopped = false;
|
|
1424
|
+
started = false;
|
|
1152
1425
|
constructor(deps) {
|
|
1153
1426
|
this.config = deps.config;
|
|
1154
|
-
this.
|
|
1155
|
-
this.
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1427
|
+
this.createClient = deps.createClient;
|
|
1428
|
+
this.log = deps.logger ?? (() => void 0);
|
|
1429
|
+
if (deps.targetsPath !== void 0) this.targetsPath = deps.targetsPath;
|
|
1430
|
+
this.defaultBtpUrl = deps.config.toonClientConfig.btpUrl ?? "";
|
|
1431
|
+
this.defaultRelayUrl = deps.config.relayUrl;
|
|
1432
|
+
this.createRelay = deps.createRelay ?? ((opts) => new RelaySubscription({
|
|
1433
|
+
relayUrl: opts.relayUrl,
|
|
1434
|
+
...opts.socksProxy ? { socksProxy: opts.socksProxy } : {},
|
|
1435
|
+
...opts.logger ? { logger: opts.logger } : {},
|
|
1436
|
+
onEvent: opts.onEvent,
|
|
1437
|
+
// The TOON relay sends events TOON-encoded (text) on reads, not JSON.
|
|
1160
1438
|
decodeEvent: (raw) => decodeEventFromToon(new TextEncoder().encode(raw))
|
|
1439
|
+
}));
|
|
1440
|
+
this.startReadProxy = deps.startReadProxy ?? ((opts) => startManagedAnonProxy({
|
|
1441
|
+
socksPort: opts.socksPort,
|
|
1442
|
+
...opts.log ? { log: opts.log } : {}
|
|
1443
|
+
}));
|
|
1444
|
+
this.registerRelay(this.defaultRelayUrl, this.config.socksProxy);
|
|
1445
|
+
const defaultApex = this.makeApex({
|
|
1446
|
+
btpUrl: this.defaultBtpUrl,
|
|
1447
|
+
client: this.createClient(this.config.toonClientConfig),
|
|
1448
|
+
...this.config.apex ? { negotiation: this.config.apex } : {},
|
|
1449
|
+
childPeers: this.config.apexChildPeers ?? [],
|
|
1450
|
+
destination: this.config.destination,
|
|
1451
|
+
chain: this.config.chain,
|
|
1452
|
+
channelStorePath: this.config.toonClientConfig.channelStorePath ?? this.apexChannelStorePathFor(this.defaultBtpUrl),
|
|
1453
|
+
feePerEvent: this.config.feePerEvent,
|
|
1454
|
+
isDefault: true
|
|
1161
1455
|
});
|
|
1162
|
-
this.
|
|
1456
|
+
this.apexes.set(defaultApex.btpUrl, defaultApex);
|
|
1163
1457
|
}
|
|
1164
1458
|
/**
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
1167
|
-
*
|
|
1459
|
+
* Start the live connections: the shared read proxy, every relay socket, the
|
|
1460
|
+
* default apex bootstrap (non-blocking), then replay persisted dynamic
|
|
1461
|
+
* targets. Returns immediately; apexes become ready asynchronously.
|
|
1168
1462
|
*/
|
|
1169
1463
|
start() {
|
|
1170
|
-
if (this.
|
|
1171
|
-
this.
|
|
1172
|
-
this.
|
|
1464
|
+
if (this.started) return;
|
|
1465
|
+
this.started = true;
|
|
1466
|
+
if (this.config.manageReadProxy) void this.bringUpReadProxy();
|
|
1467
|
+
for (const relay of this.relays.values()) relay.start();
|
|
1173
1468
|
void this.bootstrap();
|
|
1469
|
+
this.replayPersistedTargets();
|
|
1470
|
+
}
|
|
1471
|
+
/** Await the default apex's bootstrap (kicking it off if not already running). */
|
|
1472
|
+
bootstrap() {
|
|
1473
|
+
const apex = this.apexes.get(this.defaultBtpUrl);
|
|
1474
|
+
if (!apex) return Promise.resolve();
|
|
1475
|
+
return this.bootstrapApex(apex);
|
|
1174
1476
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1477
|
+
// ── Relays (reads) ─────────────────────────────────────────────────────────
|
|
1478
|
+
/**
|
|
1479
|
+
* Build + register a relay (idempotent by URL), wiring its events into the
|
|
1480
|
+
* merged buffer and replaying active fan-out subscriptions. Does NOT start the
|
|
1481
|
+
* socket — callers start it (so construction stays side-effect-free for tests).
|
|
1482
|
+
*/
|
|
1483
|
+
registerRelay(relayUrl, socksProxy) {
|
|
1484
|
+
const existing = this.relays.get(relayUrl);
|
|
1485
|
+
if (existing) return existing;
|
|
1486
|
+
const relay = this.createRelay({
|
|
1487
|
+
relayUrl,
|
|
1488
|
+
...socksProxy ? { socksProxy } : {},
|
|
1489
|
+
logger: this.log,
|
|
1490
|
+
onEvent: (subId, event) => this.pushMerged(relayUrl, subId, event)
|
|
1491
|
+
});
|
|
1492
|
+
this.relays.set(relayUrl, relay);
|
|
1493
|
+
for (const [subId, filters] of this.fanoutSubs)
|
|
1494
|
+
relay.subscribe(filters, subId);
|
|
1495
|
+
return relay;
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Add a relay read target at runtime. `.anyone` relays reuse the managed read
|
|
1499
|
+
* proxy (started here if needed). Persisted unless `persist` is false.
|
|
1500
|
+
*/
|
|
1501
|
+
async addRelay(relayUrl, persist = true) {
|
|
1502
|
+
if (this.relays.has(relayUrl)) return;
|
|
1503
|
+
let socksProxy = this.config.socksProxy;
|
|
1504
|
+
if (isAnyoneHost(relayUrl) && !socksProxy) {
|
|
1505
|
+
await this.ensureReadProxy();
|
|
1506
|
+
socksProxy = `socks5h://127.0.0.1:${this.config.readProxySocksPort ?? 9050}`;
|
|
1507
|
+
}
|
|
1508
|
+
const relay = this.registerRelay(relayUrl, socksProxy);
|
|
1509
|
+
relay.start();
|
|
1510
|
+
if (persist) saveRelayTarget(relayUrl, this.targetsPath);
|
|
1511
|
+
}
|
|
1512
|
+
/** Remove a relay read target. The config-seeded default cannot be removed. */
|
|
1513
|
+
removeRelay(relayUrl) {
|
|
1514
|
+
if (relayUrl === this.defaultRelayUrl) {
|
|
1515
|
+
throw new TargetError("Cannot remove the default (config-seeded) relay.");
|
|
1516
|
+
}
|
|
1517
|
+
const relay = this.relays.get(relayUrl);
|
|
1518
|
+
if (!relay) throw new TargetError(`No such relay: ${relayUrl}`);
|
|
1519
|
+
relay.close();
|
|
1520
|
+
this.relays.delete(relayUrl);
|
|
1521
|
+
this.merged = this.merged.filter((m) => {
|
|
1522
|
+
if (m.relayUrl === relayUrl) {
|
|
1523
|
+
this.mergedSeen.delete(m.event.id);
|
|
1524
|
+
return false;
|
|
1525
|
+
}
|
|
1526
|
+
return true;
|
|
1527
|
+
});
|
|
1528
|
+
removeRelayTarget(relayUrl, this.targetsPath);
|
|
1529
|
+
}
|
|
1530
|
+
/** Mirror a newly-buffered relay event into the merged cross-relay buffer. */
|
|
1531
|
+
pushMerged(relayUrl, subId, event) {
|
|
1532
|
+
if (this.mergedSeen.has(event.id)) return;
|
|
1533
|
+
this.mergedSeen.add(event.id);
|
|
1534
|
+
this.merged.push({ seq: ++this.mergedSeq, relayUrl, subId, event });
|
|
1535
|
+
if (this.merged.length > MERGED_BUFFER) {
|
|
1536
|
+
const evicted = this.merged.shift();
|
|
1537
|
+
if (evicted) this.mergedSeen.delete(evicted.event.id);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* Register a free-read subscription. With no `relayUrl` it FANS OUT across
|
|
1542
|
+
* every relay (and onto relays added later); with one it targets that relay.
|
|
1543
|
+
*/
|
|
1544
|
+
subscribe(req) {
|
|
1545
|
+
const subId = req.subId ?? `sub-${++this.subIdCounter}`;
|
|
1546
|
+
const filters = Array.isArray(req.filters) ? req.filters : [req.filters];
|
|
1547
|
+
const targets = req.relayUrl ? [req.relayUrl] : [...this.relays.keys()];
|
|
1548
|
+
if (req.relayUrl && !this.relays.has(req.relayUrl)) {
|
|
1549
|
+
throw new TargetError(`No such relay: ${req.relayUrl}`);
|
|
1550
|
+
}
|
|
1551
|
+
if (!req.relayUrl) this.fanoutSubs.set(subId, filters);
|
|
1552
|
+
for (const url of targets) this.relays.get(url)?.subscribe(filters, subId);
|
|
1553
|
+
return { subId, relays: targets };
|
|
1554
|
+
}
|
|
1555
|
+
/** Drain merged events newer than the cursor (free read), optionally scoped. */
|
|
1556
|
+
getEvents(query) {
|
|
1557
|
+
const after = query.cursor ?? 0;
|
|
1558
|
+
const limit = query.limit ?? 200;
|
|
1559
|
+
const matches = this.merged.filter(
|
|
1560
|
+
(m) => m.seq > after && (query.subId === void 0 || m.subId === query.subId) && (query.relayUrl === void 0 || m.relayUrl === query.relayUrl)
|
|
1561
|
+
);
|
|
1562
|
+
const page = matches.slice(0, limit);
|
|
1563
|
+
const hasMore = matches.length > page.length;
|
|
1564
|
+
const last = page.at(-1);
|
|
1565
|
+
return {
|
|
1566
|
+
events: page.map((m) => m.event),
|
|
1567
|
+
cursor: last ? last.seq : after,
|
|
1568
|
+
hasMore
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
// ── Apexes (writes) ──────────────────────────────────────────────────────
|
|
1572
|
+
makeApex(init) {
|
|
1573
|
+
return {
|
|
1574
|
+
...init,
|
|
1575
|
+
ready: false,
|
|
1576
|
+
bootstrapping: false
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Bootstrap one apex (memoized): start, inject negotiation, open/resume the
|
|
1581
|
+
* channel, route child peers. Concurrent callers await the same in-flight
|
|
1582
|
+
* work rather than re-running it.
|
|
1583
|
+
*/
|
|
1584
|
+
bootstrapApex(apex) {
|
|
1585
|
+
if (apex.ready) return Promise.resolve();
|
|
1586
|
+
if (!apex.bootstrapPromise) {
|
|
1587
|
+
apex.bootstrapPromise = this.doBootstrapApex(apex);
|
|
1588
|
+
}
|
|
1589
|
+
return apex.bootstrapPromise;
|
|
1590
|
+
}
|
|
1591
|
+
async doBootstrapApex(apex) {
|
|
1592
|
+
apex.bootstrapping = true;
|
|
1177
1593
|
try {
|
|
1178
|
-
await
|
|
1179
|
-
this.injectApexNegotiation(
|
|
1180
|
-
|
|
1181
|
-
this.routeChildPeersThroughApexChannel();
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
this.log(
|
|
1594
|
+
await apex.client.start();
|
|
1595
|
+
this.injectApexNegotiation(apex);
|
|
1596
|
+
apex.apexChannelId = await this.openOrResumeApexChannel(apex);
|
|
1597
|
+
this.routeChildPeersThroughApexChannel(apex);
|
|
1598
|
+
apex.ready = true;
|
|
1599
|
+
apex.lastError = void 0;
|
|
1600
|
+
this.log(
|
|
1601
|
+
`[runner] apex ${apex.btpUrl} ready; channel ${apex.apexChannelId}`
|
|
1602
|
+
);
|
|
1185
1603
|
} catch (err) {
|
|
1186
|
-
|
|
1187
|
-
this.log(
|
|
1604
|
+
apex.lastError = err instanceof Error ? err.message : String(err);
|
|
1605
|
+
this.log(
|
|
1606
|
+
`[runner] apex ${apex.btpUrl} bootstrap failed: ${apex.lastError}`
|
|
1607
|
+
);
|
|
1188
1608
|
} finally {
|
|
1189
|
-
|
|
1609
|
+
apex.bootstrapping = false;
|
|
1190
1610
|
}
|
|
1191
1611
|
}
|
|
1192
1612
|
/**
|
|
1193
|
-
*
|
|
1194
|
-
*
|
|
1195
|
-
* `ChannelManager` persists the nonce watermark (by channelId) but not the
|
|
1196
|
-
* peer→channelId mapping, so a naive `openChannel()` after restart re-deposits
|
|
1197
|
-
* into a fresh channel and reverts on-chain. We persist the channelId here and,
|
|
1198
|
-
* when present, `trackChannel()` the live channel (which rehydrates the nonce
|
|
1199
|
-
* from the channel store) — no on-chain write, watermark continues.
|
|
1613
|
+
* Add an apex write target. Settlement params are discovered by reading the
|
|
1614
|
+
* apex's kind:10032 off the given relay (added first if unknown). Persisted.
|
|
1200
1615
|
*/
|
|
1201
|
-
async
|
|
1202
|
-
|
|
1616
|
+
async addApex(req) {
|
|
1617
|
+
await this.addRelay(req.relayUrl);
|
|
1618
|
+
const relay = this.relays.get(req.relayUrl);
|
|
1619
|
+
if (!relay) throw new TargetError(`Relay unavailable: ${req.relayUrl}`);
|
|
1620
|
+
const discovered = await discoverApex({
|
|
1621
|
+
relay,
|
|
1622
|
+
ilpAddress: req.ilpAddress,
|
|
1623
|
+
...req.pubkey ? { pubkey: req.pubkey } : {},
|
|
1624
|
+
...req.chain ? { chain: req.chain } : {},
|
|
1625
|
+
...req.childPeers ? { childPeers: req.childPeers } : {}
|
|
1626
|
+
});
|
|
1627
|
+
const feePerEvent = req.feePerEvent !== void 0 ? BigInt(req.feePerEvent) : this.config.feePerEvent;
|
|
1628
|
+
await this.instantiateApex(
|
|
1629
|
+
{
|
|
1630
|
+
btpUrl: discovered.btpUrl,
|
|
1631
|
+
negotiation: discovered.negotiation,
|
|
1632
|
+
...discovered.apexChildPeers ? { apexChildPeers: discovered.apexChildPeers } : {},
|
|
1633
|
+
feePerEvent: req.feePerEvent ?? feePerEvent.toString(),
|
|
1634
|
+
discoveredFrom: req.relayUrl
|
|
1635
|
+
},
|
|
1636
|
+
true
|
|
1637
|
+
);
|
|
1638
|
+
const apex = this.apexes.get(discovered.btpUrl);
|
|
1639
|
+
if (!apex) {
|
|
1640
|
+
throw new TargetError(
|
|
1641
|
+
`Apex ${discovered.btpUrl} failed to register after discovery.`
|
|
1642
|
+
);
|
|
1643
|
+
}
|
|
1644
|
+
return {
|
|
1645
|
+
btpUrl: apex.btpUrl,
|
|
1646
|
+
destination: apex.destination,
|
|
1647
|
+
chain: apex.chain,
|
|
1648
|
+
ready: apex.ready
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
/** Build + register + bootstrap an apex from a (persisted) target record. */
|
|
1652
|
+
async instantiateApex(target, persist) {
|
|
1653
|
+
if (this.apexes.has(target.btpUrl)) return;
|
|
1654
|
+
const clientConfig = this.deriveApexClientConfig(
|
|
1655
|
+
target.btpUrl,
|
|
1656
|
+
target.negotiation.destination
|
|
1657
|
+
);
|
|
1658
|
+
const apex = this.makeApex({
|
|
1659
|
+
btpUrl: target.btpUrl,
|
|
1660
|
+
client: this.createClient(clientConfig),
|
|
1661
|
+
negotiation: target.negotiation,
|
|
1662
|
+
childPeers: target.apexChildPeers ?? [],
|
|
1663
|
+
destination: target.negotiation.destination,
|
|
1664
|
+
chain: target.negotiation.chain,
|
|
1665
|
+
channelStorePath: this.apexChannelStorePathFor(target.btpUrl),
|
|
1666
|
+
feePerEvent: BigInt(target.feePerEvent ?? this.config.feePerEvent),
|
|
1667
|
+
isDefault: false
|
|
1668
|
+
});
|
|
1669
|
+
this.apexes.set(apex.btpUrl, apex);
|
|
1670
|
+
if (persist) saveApexTarget(target, this.targetsPath);
|
|
1671
|
+
await this.bootstrapApex(apex);
|
|
1672
|
+
}
|
|
1673
|
+
/** Remove an apex write target. The config-seeded default cannot be removed. */
|
|
1674
|
+
async removeApex(btpUrl) {
|
|
1675
|
+
if (btpUrl === this.defaultBtpUrl) {
|
|
1676
|
+
throw new TargetError("Cannot remove the default (config-seeded) apex.");
|
|
1677
|
+
}
|
|
1678
|
+
const apex = this.apexes.get(btpUrl);
|
|
1679
|
+
if (!apex) throw new TargetError(`No such apex: ${btpUrl}`);
|
|
1680
|
+
try {
|
|
1681
|
+
await apex.client.stop();
|
|
1682
|
+
} catch (err) {
|
|
1683
|
+
this.log(
|
|
1684
|
+
`[runner] apex ${btpUrl} stop error: ${err instanceof Error ? err.message : String(err)}`
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
this.apexes.delete(btpUrl);
|
|
1688
|
+
removeApexTarget(btpUrl, this.targetsPath);
|
|
1689
|
+
}
|
|
1690
|
+
/** Derive a per-apex ToonClientConfig from the default (shared identity/transport). */
|
|
1691
|
+
deriveApexClientConfig(btpUrl, destination) {
|
|
1692
|
+
const base = this.config.toonClientConfig;
|
|
1693
|
+
return {
|
|
1694
|
+
...base,
|
|
1695
|
+
btpUrl,
|
|
1696
|
+
destinationAddress: destination,
|
|
1697
|
+
// Distinct nonce-watermark store per apex so parallel ChannelManagers in
|
|
1698
|
+
// this process never race a shared channels.json.
|
|
1699
|
+
channelStorePath: this.apexChannelStorePathFor(btpUrl),
|
|
1700
|
+
ilpInfo: { ...base.ilpInfo, btpEndpoint: btpUrl }
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
apexChannelStorePathFor(btpUrl) {
|
|
1704
|
+
return `${configDir()}/channels-${sanitize(btpUrl)}.json`;
|
|
1705
|
+
}
|
|
1706
|
+
// ── Persisted-target replay ────────────────────────────────────────────────
|
|
1707
|
+
replayPersistedTargets() {
|
|
1708
|
+
let store;
|
|
1709
|
+
try {
|
|
1710
|
+
store = loadTargets(this.targetsPath);
|
|
1711
|
+
} catch (err) {
|
|
1712
|
+
this.log(
|
|
1713
|
+
`[runner] failed to load targets store: ${err instanceof Error ? err.message : String(err)}`
|
|
1714
|
+
);
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
for (const r of store.relays) {
|
|
1718
|
+
if (r.relayUrl === this.defaultRelayUrl) continue;
|
|
1719
|
+
void this.addRelay(r.relayUrl, false).catch(
|
|
1720
|
+
(err) => this.log(`[runner] replay relay ${r.relayUrl} failed: ${errMsg2(err)}`)
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
for (const a of store.apexes) {
|
|
1724
|
+
if (a.btpUrl === this.defaultBtpUrl) continue;
|
|
1725
|
+
void this.instantiateApex(a, false).catch(
|
|
1726
|
+
(err) => this.log(`[runner] replay apex ${a.btpUrl} failed: ${errMsg2(err)}`)
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
// ── Shared read proxy ──────────────────────────────────────────────────────
|
|
1731
|
+
async ensureReadProxy() {
|
|
1732
|
+
if (this.stopReadProxy) return;
|
|
1733
|
+
await this.bringUpReadProxy();
|
|
1734
|
+
}
|
|
1735
|
+
async bringUpReadProxy() {
|
|
1736
|
+
if (this.stopReadProxy) return;
|
|
1737
|
+
const socksPort = this.config.readProxySocksPort ?? 9050;
|
|
1738
|
+
try {
|
|
1739
|
+
this.log(
|
|
1740
|
+
`[runner] starting managed read proxy on 127.0.0.1:${socksPort}`
|
|
1741
|
+
);
|
|
1742
|
+
const proxy = await this.startReadProxy({
|
|
1743
|
+
socksPort,
|
|
1744
|
+
log: (m) => this.log(`[anon] ${m}`)
|
|
1745
|
+
});
|
|
1746
|
+
if (this.stopped) {
|
|
1747
|
+
await proxy.stop();
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
this.stopReadProxy = () => proxy.stop();
|
|
1751
|
+
this.readProxyError = void 0;
|
|
1752
|
+
this.log("[runner] managed read proxy ready");
|
|
1753
|
+
} catch (err) {
|
|
1754
|
+
this.readProxyError = err instanceof Error ? err.message : String(err);
|
|
1755
|
+
this.log(`[runner] managed read proxy failed: ${this.readProxyError}`);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
// ── Channel / negotiation helpers (per-apex) ───────────────────────────────
|
|
1759
|
+
/** Open the apex channel — or, on a restart, RESUME the existing one. */
|
|
1760
|
+
async openOrResumeApexChannel(apex) {
|
|
1761
|
+
const { destination, chain } = apex;
|
|
1762
|
+
const { apexChannelStorePath } = this.config;
|
|
1203
1763
|
const saved = loadApexChannel(apexChannelStorePath, destination, chain);
|
|
1204
|
-
const cm =
|
|
1764
|
+
const cm = apex.client.channelManager;
|
|
1205
1765
|
if (saved && cm && typeof cm.trackChannel === "function") {
|
|
1206
1766
|
cm.trackChannel(saved.channelId, saved.context);
|
|
1207
1767
|
this.log(
|
|
@@ -1209,62 +1769,47 @@ var ClientRunner = class {
|
|
|
1209
1769
|
);
|
|
1210
1770
|
return saved.channelId;
|
|
1211
1771
|
}
|
|
1212
|
-
const channelId = await
|
|
1213
|
-
if (
|
|
1214
|
-
const a =
|
|
1772
|
+
const channelId = await apex.client.openChannel(destination);
|
|
1773
|
+
if (apex.negotiation) {
|
|
1774
|
+
const a = apex.negotiation;
|
|
1215
1775
|
saveApexChannel(apexChannelStorePath, destination, chain, {
|
|
1216
1776
|
channelId,
|
|
1217
1777
|
context: {
|
|
1218
1778
|
chainType: a.chain,
|
|
1219
1779
|
chainId: a.chainId,
|
|
1220
1780
|
tokenNetworkAddress: a.tokenNetwork ?? "",
|
|
1221
|
-
tokenAddress: a.tokenAddress,
|
|
1781
|
+
...a.tokenAddress ? { tokenAddress: a.tokenAddress } : {},
|
|
1222
1782
|
recipient: a.settlementAddress
|
|
1223
1783
|
}
|
|
1224
1784
|
});
|
|
1225
1785
|
}
|
|
1226
1786
|
return channelId;
|
|
1227
1787
|
}
|
|
1228
|
-
/**
|
|
1229
|
-
* Inject the apex settlement negotiation directly into the ToonClient when
|
|
1230
|
-
* configured. Required for HS / direct-apex mode where bootstrap discovers 0
|
|
1231
|
-
* peers (no relay-based discovery). Mirrors the docker entrypoint's approach.
|
|
1232
|
-
*/
|
|
1788
|
+
/** Inject the apex settlement negotiation directly into its ToonClient. */
|
|
1233
1789
|
injectApexNegotiation(apex) {
|
|
1234
|
-
|
|
1235
|
-
|
|
1790
|
+
const a = apex.negotiation;
|
|
1791
|
+
if (!a) return;
|
|
1792
|
+
const negotiations = apex.client.peerNegotiations;
|
|
1236
1793
|
if (!(negotiations instanceof Map)) {
|
|
1237
1794
|
throw new Error(
|
|
1238
1795
|
"ToonClient.peerNegotiations layout changed \u2014 cannot inject apex negotiation"
|
|
1239
1796
|
);
|
|
1240
1797
|
}
|
|
1241
|
-
negotiations.set(
|
|
1242
|
-
chain:
|
|
1243
|
-
chainType:
|
|
1244
|
-
chainId:
|
|
1245
|
-
settlementAddress:
|
|
1246
|
-
tokenAddress:
|
|
1247
|
-
tokenNetwork:
|
|
1798
|
+
negotiations.set(a.peerId, {
|
|
1799
|
+
chain: a.chainKey,
|
|
1800
|
+
chainType: a.chain,
|
|
1801
|
+
chainId: a.chainId,
|
|
1802
|
+
settlementAddress: a.settlementAddress,
|
|
1803
|
+
tokenAddress: a.tokenAddress,
|
|
1804
|
+
tokenNetwork: a.tokenNetwork
|
|
1248
1805
|
});
|
|
1249
|
-
this.log(`[runner] injected apex negotiation for peer "${
|
|
1806
|
+
this.log(`[runner] injected apex negotiation for peer "${a.peerId}"`);
|
|
1250
1807
|
}
|
|
1251
|
-
/**
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
* the destination's last segment (`town`/`dvm`/`mill`), so without this each
|
|
1257
|
-
* child would (a) fail the "no negotiation for peer" guard and (b) try to open
|
|
1258
|
-
* a SECOND on-chain channel to the same apex receive (which reverts —
|
|
1259
|
-
* channel-exists). So: inject the same apex negotiation under each child peer
|
|
1260
|
-
* AND pre-map its peer→channel to the already-open apex channel so
|
|
1261
|
-
* `ensureChannel` reuses it (no second open; one shared nonce sequence).
|
|
1262
|
-
*/
|
|
1263
|
-
routeChildPeersThroughApexChannel() {
|
|
1264
|
-
const apex = this.config.apex;
|
|
1265
|
-
const children = this.config.apexChildPeers ?? [];
|
|
1266
|
-
if (!apex || !this.apexChannelId || children.length === 0) return;
|
|
1267
|
-
const client = this.client;
|
|
1808
|
+
/** Route apex CHILD peers (dvm/mill) through the SAME apex payment channel. */
|
|
1809
|
+
routeChildPeersThroughApexChannel(apex) {
|
|
1810
|
+
const a = apex.negotiation;
|
|
1811
|
+
if (!a || !apex.apexChannelId || apex.childPeers.length === 0) return;
|
|
1812
|
+
const client = apex.client;
|
|
1268
1813
|
const negotiations = client.peerNegotiations;
|
|
1269
1814
|
const peerChannels = client.channelManager?.peerChannels;
|
|
1270
1815
|
if (!(negotiations instanceof Map) || !(peerChannels instanceof Map)) {
|
|
@@ -1273,69 +1818,103 @@ var ClientRunner = class {
|
|
|
1273
1818
|
);
|
|
1274
1819
|
return;
|
|
1275
1820
|
}
|
|
1276
|
-
for (const peer of
|
|
1277
|
-
if (peer ===
|
|
1821
|
+
for (const peer of apex.childPeers) {
|
|
1822
|
+
if (peer === a.peerId) continue;
|
|
1278
1823
|
negotiations.set(peer, {
|
|
1279
|
-
chain:
|
|
1280
|
-
chainType:
|
|
1281
|
-
chainId:
|
|
1282
|
-
settlementAddress:
|
|
1283
|
-
tokenAddress:
|
|
1284
|
-
tokenNetwork:
|
|
1824
|
+
chain: a.chainKey,
|
|
1825
|
+
chainType: a.chain,
|
|
1826
|
+
chainId: a.chainId,
|
|
1827
|
+
settlementAddress: a.settlementAddress,
|
|
1828
|
+
tokenAddress: a.tokenAddress,
|
|
1829
|
+
tokenNetwork: a.tokenNetwork
|
|
1285
1830
|
});
|
|
1286
|
-
peerChannels.set(peer,
|
|
1831
|
+
peerChannels.set(peer, apex.apexChannelId);
|
|
1287
1832
|
this.log(
|
|
1288
|
-
`[runner] routed child peer "${peer}" through apex channel ${
|
|
1833
|
+
`[runner] routed child peer "${peer}" through apex channel ${apex.apexChannelId}`
|
|
1289
1834
|
);
|
|
1290
1835
|
}
|
|
1291
1836
|
}
|
|
1837
|
+
// ── Status ─────────────────────────────────────────────────────────────────
|
|
1838
|
+
defaultApex() {
|
|
1839
|
+
return this.apexes.get(this.defaultBtpUrl);
|
|
1840
|
+
}
|
|
1841
|
+
/** Whether any apex has finished bootstrapping. */
|
|
1292
1842
|
isReady() {
|
|
1293
|
-
return this.ready;
|
|
1843
|
+
return [...this.apexes.values()].some((a) => a.ready);
|
|
1294
1844
|
}
|
|
1295
1845
|
isBootstrapping() {
|
|
1296
|
-
return this.bootstrapping;
|
|
1846
|
+
return [...this.apexes.values()].some((a) => a.bootstrapping);
|
|
1297
1847
|
}
|
|
1298
1848
|
getStatus() {
|
|
1299
|
-
const
|
|
1849
|
+
const apex = this.defaultApex();
|
|
1850
|
+
const client = apex?.client;
|
|
1851
|
+
const net = client?.getNetworkStatus();
|
|
1300
1852
|
const network = net ? ["evm", "solana", "mina"].map((c) => ({
|
|
1301
1853
|
chain: c,
|
|
1302
1854
|
ready: net[c] === "configured",
|
|
1303
1855
|
detail: net[c]
|
|
1304
1856
|
})) : void 0;
|
|
1857
|
+
const relay = this.relays.get(this.defaultRelayUrl);
|
|
1305
1858
|
return {
|
|
1306
1859
|
uptimeMs: Date.now() - this.startedAt,
|
|
1307
|
-
bootstrapping:
|
|
1308
|
-
ready:
|
|
1860
|
+
bootstrapping: apex?.bootstrapping ?? false,
|
|
1861
|
+
ready: apex?.ready ?? false,
|
|
1309
1862
|
settlementChain: this.config.chain,
|
|
1310
1863
|
identity: {
|
|
1311
|
-
nostrPubkey: safe(() =>
|
|
1312
|
-
evmAddress: safe(() =>
|
|
1313
|
-
solanaAddress: safe(() =>
|
|
1314
|
-
minaAddress: safe(() =>
|
|
1864
|
+
nostrPubkey: safe(() => client?.getPublicKey()) ?? "",
|
|
1865
|
+
evmAddress: safe(() => client?.getEvmAddress()),
|
|
1866
|
+
solanaAddress: safe(() => client?.getSolanaAddress()),
|
|
1867
|
+
minaAddress: safe(() => client?.getMinaAddress())
|
|
1315
1868
|
},
|
|
1316
1869
|
transport: {
|
|
1317
1870
|
type: this.config.socksProxy ? "socks5" : "direct",
|
|
1318
|
-
socksProxy: this.config.socksProxy,
|
|
1319
|
-
btpUrl:
|
|
1871
|
+
...this.config.socksProxy ? { socksProxy: this.config.socksProxy } : {},
|
|
1872
|
+
...apex ? { btpUrl: apex.btpUrl } : {}
|
|
1320
1873
|
},
|
|
1321
1874
|
relay: {
|
|
1322
|
-
url: this.
|
|
1323
|
-
connected:
|
|
1324
|
-
buffered:
|
|
1325
|
-
subscriptions:
|
|
1875
|
+
url: this.defaultRelayUrl,
|
|
1876
|
+
connected: relay?.isConnected() ?? false,
|
|
1877
|
+
buffered: relay?.bufferedCount() ?? 0,
|
|
1878
|
+
subscriptions: relay?.activeSubscriptions() ?? [],
|
|
1879
|
+
...this.readProxyError ? { proxyError: this.readProxyError } : {}
|
|
1326
1880
|
},
|
|
1327
1881
|
...network ? { network } : {},
|
|
1328
|
-
...
|
|
1882
|
+
...apex?.lastError ? { lastError: apex.lastError } : {}
|
|
1329
1883
|
};
|
|
1330
1884
|
}
|
|
1331
|
-
/**
|
|
1885
|
+
/** Full registry of relay + apex targets with per-target status. */
|
|
1886
|
+
getTargets() {
|
|
1887
|
+
const relays = [...this.relays.entries()].map(
|
|
1888
|
+
([relayUrl, r]) => ({
|
|
1889
|
+
relayUrl,
|
|
1890
|
+
connected: r.isConnected(),
|
|
1891
|
+
buffered: r.bufferedCount(),
|
|
1892
|
+
subscriptions: r.activeSubscriptions(),
|
|
1893
|
+
isDefault: relayUrl === this.defaultRelayUrl
|
|
1894
|
+
})
|
|
1895
|
+
);
|
|
1896
|
+
const apexes = [...this.apexes.values()].map((a) => ({
|
|
1897
|
+
btpUrl: a.btpUrl,
|
|
1898
|
+
destination: a.destination,
|
|
1899
|
+
chain: a.chain,
|
|
1900
|
+
ready: a.ready,
|
|
1901
|
+
bootstrapping: a.bootstrapping,
|
|
1902
|
+
...a.apexChannelId ? { channelId: a.apexChannelId } : {},
|
|
1903
|
+
...a.lastError ? { lastError: a.lastError } : {},
|
|
1904
|
+
isDefault: a.isDefault
|
|
1905
|
+
}));
|
|
1906
|
+
return { relays, apexes };
|
|
1907
|
+
}
|
|
1908
|
+
// ── Paid operations ──────────────────────────────────────────────────────
|
|
1909
|
+
/** Pay-to-write a single event through the selected (or default) apex. */
|
|
1332
1910
|
async publish(req) {
|
|
1333
|
-
this.
|
|
1334
|
-
|
|
1335
|
-
const
|
|
1336
|
-
const
|
|
1337
|
-
const
|
|
1338
|
-
|
|
1911
|
+
const apex = this.selectApex(req.btpUrl);
|
|
1912
|
+
this.assertApexReady(apex);
|
|
1913
|
+
const channelId = apex.apexChannelId ?? await apex.client.openChannel(req.destination);
|
|
1914
|
+
const fee = req.fee !== void 0 ? BigInt(req.fee) : apex.feePerEvent;
|
|
1915
|
+
const claim = await apex.client.signBalanceProof(channelId, fee);
|
|
1916
|
+
const result = await apex.client.publishEvent(req.event, {
|
|
1917
|
+
...req.destination ? { destination: req.destination } : {},
|
|
1339
1918
|
claim,
|
|
1340
1919
|
ilpAmount: fee
|
|
1341
1920
|
});
|
|
@@ -1344,63 +1923,47 @@ var ClientRunner = class {
|
|
|
1344
1923
|
}
|
|
1345
1924
|
return {
|
|
1346
1925
|
eventId: result.eventId ?? req.event.id,
|
|
1347
|
-
data: result.data,
|
|
1926
|
+
...result.data !== void 0 ? { data: result.data } : {},
|
|
1348
1927
|
channelId,
|
|
1349
|
-
nonce:
|
|
1928
|
+
nonce: apex.client.getChannelNonce(channelId)
|
|
1350
1929
|
};
|
|
1351
1930
|
}
|
|
1352
|
-
/**
|
|
1353
|
-
|
|
1354
|
-
const
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
getEvents(query) {
|
|
1359
|
-
const opts = {};
|
|
1360
|
-
if (query.subId !== void 0) opts.subId = query.subId;
|
|
1361
|
-
if (query.cursor !== void 0) opts.cursor = query.cursor;
|
|
1362
|
-
if (query.limit !== void 0) opts.limit = query.limit;
|
|
1363
|
-
const { events, cursor, hasMore } = this.relay.getEvents(opts);
|
|
1364
|
-
return { events, cursor, hasMore };
|
|
1365
|
-
}
|
|
1366
|
-
/** Open (or return) a payment channel for a destination. */
|
|
1367
|
-
async openChannel(destination) {
|
|
1368
|
-
this.assertReady();
|
|
1369
|
-
const channelId = await this.client.openChannel(
|
|
1370
|
-
destination ?? this.config.destination
|
|
1931
|
+
/** Open (or return) a payment channel on the selected (or default) apex. */
|
|
1932
|
+
async openChannel(destination, btpUrl) {
|
|
1933
|
+
const apex = this.selectApex(btpUrl);
|
|
1934
|
+
this.assertApexReady(apex);
|
|
1935
|
+
const channelId = await apex.client.openChannel(
|
|
1936
|
+
destination ?? apex.destination
|
|
1371
1937
|
);
|
|
1372
|
-
if (!destination || destination ===
|
|
1373
|
-
|
|
1938
|
+
if (!destination || destination === apex.destination) {
|
|
1939
|
+
apex.apexChannelId = channelId;
|
|
1374
1940
|
}
|
|
1375
1941
|
return { channelId };
|
|
1376
1942
|
}
|
|
1377
|
-
/** List tracked channels with
|
|
1943
|
+
/** List tracked channels across ALL apexes with nonce + cumulative amount. */
|
|
1378
1944
|
getChannels() {
|
|
1379
|
-
const
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1945
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1946
|
+
const channels = [];
|
|
1947
|
+
for (const apex of this.apexes.values()) {
|
|
1948
|
+
for (const channelId of apex.client.getTrackedChannels()) {
|
|
1949
|
+
if (seen.has(channelId)) continue;
|
|
1950
|
+
seen.add(channelId);
|
|
1951
|
+
channels.push({
|
|
1952
|
+
channelId,
|
|
1953
|
+
nonce: apex.client.getChannelNonce(channelId),
|
|
1954
|
+
cumulativeAmount: apex.client.getChannelCumulativeAmount(channelId).toString()
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1384
1958
|
return { channels };
|
|
1385
1959
|
}
|
|
1386
|
-
/**
|
|
1387
|
-
* Swap source asset → target asset against a mill peer.
|
|
1388
|
-
*
|
|
1389
|
-
* Uses SDK `streamSwap`, which builds a NIP-59 gift-wrapped kind:20032 swap
|
|
1390
|
-
* rumor per packet and sends it over the open BTP session. The source-asset
|
|
1391
|
-
* balance proof is signed by the ToonClient's ChannelManager against the apex
|
|
1392
|
-
* channel — so the mill peer MUST be routed via `apexChildPeers` (otherwise
|
|
1393
|
-
* there is no channel to sign against and the mill rejects with F99).
|
|
1394
|
-
*
|
|
1395
|
-
* A fresh ephemeral gift-wrap key is generated per swap (used for sealing the
|
|
1396
|
-
* rumor AND decrypting the FULFILL claims) — independent of the daemon's
|
|
1397
|
-
* settlement identity, so callers never need to expose a key.
|
|
1398
|
-
*/
|
|
1960
|
+
/** Swap source→target asset against a mill peer via the selected apex. */
|
|
1399
1961
|
async swap(req) {
|
|
1400
|
-
this.
|
|
1962
|
+
const apex = this.selectApex(req.btpUrl);
|
|
1963
|
+
this.assertApexReady(apex);
|
|
1401
1964
|
const senderSecretKey = generateSecretKey2();
|
|
1402
1965
|
const result = await streamSwap({
|
|
1403
|
-
client:
|
|
1966
|
+
client: apex.client,
|
|
1404
1967
|
millPubkey: req.millPubkey,
|
|
1405
1968
|
millIlpAddress: req.destination,
|
|
1406
1969
|
pair: req.pair,
|
|
@@ -1430,23 +1993,42 @@ var ClientRunner = class {
|
|
|
1430
1993
|
...firstReject ? { code: firstReject.code, message: firstReject.message } : {}
|
|
1431
1994
|
};
|
|
1432
1995
|
}
|
|
1433
|
-
/** Graceful teardown
|
|
1996
|
+
/** Graceful teardown: close every relay + stop every apex client + read proxy. */
|
|
1434
1997
|
async stop() {
|
|
1435
1998
|
if (this.stopped) return;
|
|
1436
1999
|
this.stopped = true;
|
|
1437
|
-
this.relay.close();
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
`[runner]
|
|
1443
|
-
|
|
2000
|
+
for (const relay of this.relays.values()) relay.close();
|
|
2001
|
+
if (this.stopReadProxy) {
|
|
2002
|
+
try {
|
|
2003
|
+
await this.stopReadProxy();
|
|
2004
|
+
} catch (err) {
|
|
2005
|
+
this.log(`[runner] read proxy stop error: ${errMsg2(err)}`);
|
|
2006
|
+
}
|
|
2007
|
+
this.stopReadProxy = void 0;
|
|
2008
|
+
}
|
|
2009
|
+
for (const apex of this.apexes.values()) {
|
|
2010
|
+
try {
|
|
2011
|
+
await apex.client.stop();
|
|
2012
|
+
} catch (err) {
|
|
2013
|
+
this.log(`[runner] client stop error (${apex.btpUrl}): ${errMsg2(err)}`);
|
|
2014
|
+
}
|
|
1444
2015
|
}
|
|
1445
2016
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
2017
|
+
// ── internals ────────────────────────────────────────────────────────────
|
|
2018
|
+
selectApex(btpUrl) {
|
|
2019
|
+
if (btpUrl) {
|
|
2020
|
+
const apex = this.apexes.get(btpUrl);
|
|
2021
|
+
if (!apex) throw new TargetError(`No such apex: ${btpUrl}`);
|
|
2022
|
+
return apex;
|
|
2023
|
+
}
|
|
2024
|
+
const def = this.defaultApex();
|
|
2025
|
+
if (!def) throw new NotReadyError("No apex configured.");
|
|
2026
|
+
return def;
|
|
2027
|
+
}
|
|
2028
|
+
assertApexReady(apex) {
|
|
2029
|
+
if (!apex.ready) {
|
|
1448
2030
|
throw new NotReadyError(
|
|
1449
|
-
|
|
2031
|
+
apex.bootstrapping ? "Apex is still bootstrapping (BTP/anon coming up) \u2014 retry shortly." : apex.lastError ?? "Apex is not ready."
|
|
1450
2032
|
);
|
|
1451
2033
|
}
|
|
1452
2034
|
}
|
|
@@ -1464,6 +2046,12 @@ var PublishRejectedError = class extends Error {
|
|
|
1464
2046
|
this.name = "PublishRejectedError";
|
|
1465
2047
|
}
|
|
1466
2048
|
};
|
|
2049
|
+
var TargetError = class extends Error {
|
|
2050
|
+
constructor(message) {
|
|
2051
|
+
super(message);
|
|
2052
|
+
this.name = "TargetError";
|
|
2053
|
+
}
|
|
2054
|
+
};
|
|
1467
2055
|
function safe(fn) {
|
|
1468
2056
|
try {
|
|
1469
2057
|
return fn();
|
|
@@ -1471,6 +2059,19 @@ function safe(fn) {
|
|
|
1471
2059
|
return void 0;
|
|
1472
2060
|
}
|
|
1473
2061
|
}
|
|
2062
|
+
function errMsg2(err) {
|
|
2063
|
+
return err instanceof Error ? err.message : String(err);
|
|
2064
|
+
}
|
|
2065
|
+
function sanitize(s) {
|
|
2066
|
+
return s.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
2067
|
+
}
|
|
2068
|
+
function isAnyoneHost(url) {
|
|
2069
|
+
try {
|
|
2070
|
+
return new URL(url).hostname.endsWith(".anyone");
|
|
2071
|
+
} catch {
|
|
2072
|
+
return false;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
1474
2075
|
|
|
1475
2076
|
// src/daemon/routes.ts
|
|
1476
2077
|
function registerRoutes(app, runner) {
|
|
@@ -1495,19 +2096,21 @@ function registerRoutes(app, runner) {
|
|
|
1495
2096
|
detail: "body.filters is required (a NIP-01 filter or array of filters)."
|
|
1496
2097
|
});
|
|
1497
2098
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
async (req) => {
|
|
1503
|
-
const q = req.query;
|
|
1504
|
-
const query = {};
|
|
1505
|
-
if (q.subId) query.subId = q.subId;
|
|
1506
|
-
if (q.cursor !== void 0) query.cursor = Number(q.cursor);
|
|
1507
|
-
if (q.limit !== void 0) query.limit = Number(q.limit);
|
|
1508
|
-
return runner.getEvents(query);
|
|
2099
|
+
try {
|
|
2100
|
+
return runner.subscribe(body);
|
|
2101
|
+
} catch (err) {
|
|
2102
|
+
return mapError(reply, err);
|
|
1509
2103
|
}
|
|
1510
|
-
);
|
|
2104
|
+
});
|
|
2105
|
+
app.get("/events", async (req) => {
|
|
2106
|
+
const q = req.query;
|
|
2107
|
+
const query = {};
|
|
2108
|
+
if (q.subId) query.subId = q.subId;
|
|
2109
|
+
if (q.cursor !== void 0) query.cursor = Number(q.cursor);
|
|
2110
|
+
if (q.limit !== void 0) query.limit = Number(q.limit);
|
|
2111
|
+
if (q.relayUrl) query.relayUrl = q.relayUrl;
|
|
2112
|
+
return runner.getEvents(query);
|
|
2113
|
+
});
|
|
1511
2114
|
app.post("/channels", async (req, reply) => {
|
|
1512
2115
|
try {
|
|
1513
2116
|
return await runner.openChannel(req.body?.destination);
|
|
@@ -1529,6 +2132,62 @@ function registerRoutes(app, runner) {
|
|
|
1529
2132
|
return mapError(reply, err);
|
|
1530
2133
|
}
|
|
1531
2134
|
});
|
|
2135
|
+
app.get("/targets", async () => runner.getTargets());
|
|
2136
|
+
app.post("/relays", async (req, reply) => {
|
|
2137
|
+
const url = req.body?.relayUrl;
|
|
2138
|
+
if (!url) {
|
|
2139
|
+
return sendError(reply, 400, "invalid_relay", {
|
|
2140
|
+
detail: "body.relayUrl is required."
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
try {
|
|
2144
|
+
await runner.addRelay(url);
|
|
2145
|
+
return runner.getTargets();
|
|
2146
|
+
} catch (err) {
|
|
2147
|
+
return mapError(reply, err);
|
|
2148
|
+
}
|
|
2149
|
+
});
|
|
2150
|
+
app.delete("/relays", async (req, reply) => {
|
|
2151
|
+
const url = req.body?.relayUrl;
|
|
2152
|
+
if (!url) {
|
|
2153
|
+
return sendError(reply, 400, "invalid_relay", {
|
|
2154
|
+
detail: "body.relayUrl is required."
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
try {
|
|
2158
|
+
runner.removeRelay(url);
|
|
2159
|
+
return runner.getTargets();
|
|
2160
|
+
} catch (err) {
|
|
2161
|
+
return mapError(reply, err);
|
|
2162
|
+
}
|
|
2163
|
+
});
|
|
2164
|
+
app.post("/apex", async (req, reply) => {
|
|
2165
|
+
const body = req.body;
|
|
2166
|
+
if (!body || !body.ilpAddress || !body.relayUrl) {
|
|
2167
|
+
return sendError(reply, 400, "invalid_apex", {
|
|
2168
|
+
detail: "body.ilpAddress and body.relayUrl are required."
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
try {
|
|
2172
|
+
return await runner.addApex(body);
|
|
2173
|
+
} catch (err) {
|
|
2174
|
+
return mapError(reply, err);
|
|
2175
|
+
}
|
|
2176
|
+
});
|
|
2177
|
+
app.delete("/apex", async (req, reply) => {
|
|
2178
|
+
const url = req.body?.btpUrl;
|
|
2179
|
+
if (!url) {
|
|
2180
|
+
return sendError(reply, 400, "invalid_apex", {
|
|
2181
|
+
detail: "body.btpUrl is required."
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
try {
|
|
2185
|
+
await runner.removeApex(url);
|
|
2186
|
+
return runner.getTargets();
|
|
2187
|
+
} catch (err) {
|
|
2188
|
+
return mapError(reply, err);
|
|
2189
|
+
}
|
|
2190
|
+
});
|
|
1532
2191
|
}
|
|
1533
2192
|
function isSignedEvent(event) {
|
|
1534
2193
|
if (typeof event !== "object" || event === null) return false;
|
|
@@ -1545,6 +2204,10 @@ function mapError(reply, err) {
|
|
|
1545
2204
|
if (err instanceof PublishRejectedError) {
|
|
1546
2205
|
return sendError(reply, 502, "rejected", { detail: err.message });
|
|
1547
2206
|
}
|
|
2207
|
+
if (err instanceof TargetError) {
|
|
2208
|
+
const status = /no such/i.test(err.message) ? 404 : 400;
|
|
2209
|
+
return sendError(reply, status, "invalid_target", { detail: err.message });
|
|
2210
|
+
}
|
|
1548
2211
|
return sendError(reply, 500, "internal_error", {
|
|
1549
2212
|
detail: err instanceof Error ? err.message : String(err)
|
|
1550
2213
|
});
|
|
@@ -1558,10 +2221,13 @@ function sendError(reply, status, error, extra = {}) {
|
|
|
1558
2221
|
}
|
|
1559
2222
|
|
|
1560
2223
|
export {
|
|
2224
|
+
defaultKeystorePath,
|
|
2225
|
+
hasConfiguredIdentity,
|
|
2226
|
+
scaffoldFirstRun,
|
|
1561
2227
|
RelaySubscription,
|
|
1562
2228
|
ClientRunner,
|
|
1563
2229
|
NotReadyError,
|
|
1564
2230
|
PublishRejectedError,
|
|
1565
2231
|
registerRoutes
|
|
1566
2232
|
};
|
|
1567
|
-
//# sourceMappingURL=chunk-
|
|
2233
|
+
//# sourceMappingURL=chunk-AYZWVPNQ.js.map
|