@toon-protocol/client-mcp 0.31.2 → 0.33.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/dist/{chunk-PRFPAEGG.js → chunk-7WCDT25E.js} +721 -196
- package/dist/chunk-7WCDT25E.js.map +1 -0
- package/dist/{chunk-ARFPM2PT.js → chunk-ALMTQYYJ.js} +19 -1
- package/dist/{chunk-ARFPM2PT.js.map → chunk-ALMTQYYJ.js.map} +1 -1
- package/dist/{chunk-I5L5CB4N.js → chunk-UZT7LAGY.js} +135 -5
- package/dist/chunk-UZT7LAGY.js.map +1 -0
- package/dist/daemon.js +3 -3
- package/dist/daemon.js.map +1 -1
- package/dist/index.d.ts +216 -76
- package/dist/index.js +3 -3
- package/dist/mcp.js +2 -2
- package/package.json +2 -2
- package/dist/chunk-I5L5CB4N.js.map +0 -1
- package/dist/chunk-PRFPAEGG.js.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createRequire as __cr } from 'module'; const require = __cr(import.meta.url);
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_KEYSTORE_PASSWORD,
|
|
4
|
+
ILP_PEER_INFO_KIND,
|
|
4
5
|
ToonError,
|
|
5
6
|
buildIlpPrepare,
|
|
6
7
|
configDir,
|
|
@@ -9,8 +10,9 @@ import {
|
|
|
9
10
|
deriveFullIdentity,
|
|
10
11
|
encodeEventToToon,
|
|
11
12
|
generateKeystore,
|
|
13
|
+
parseIlpPeerInfo,
|
|
12
14
|
readConfigFile
|
|
13
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-ALMTQYYJ.js";
|
|
14
16
|
import {
|
|
15
17
|
startManagedAnonProxy
|
|
16
18
|
} from "./chunk-SKQTKZIH.js";
|
|
@@ -128,6 +130,7 @@ var RelaySubscription = class {
|
|
|
128
130
|
log;
|
|
129
131
|
wsFactory;
|
|
130
132
|
decodeEvent;
|
|
133
|
+
onEvent;
|
|
131
134
|
/** Active subscriptions: subId -> filters (re-sent on every (re)connect). */
|
|
132
135
|
subscriptions = /* @__PURE__ */ new Map();
|
|
133
136
|
/** Ring buffer of received events, ordered by ascending `seq`. */
|
|
@@ -150,6 +153,7 @@ var RelaySubscription = class {
|
|
|
150
153
|
this.log = opts.logger ?? noop;
|
|
151
154
|
this.wsFactory = opts.wsFactory ?? defaultWebSocketFactory(this.socksProxy);
|
|
152
155
|
this.decodeEvent = opts.decodeEvent;
|
|
156
|
+
this.onEvent = opts.onEvent;
|
|
153
157
|
}
|
|
154
158
|
/** Whether the underlying socket is currently open. */
|
|
155
159
|
isConnected() {
|
|
@@ -250,7 +254,7 @@ var RelaySubscription = class {
|
|
|
250
254
|
}
|
|
251
255
|
scheduleReconnect() {
|
|
252
256
|
if (this.closing || this.reconnectTimer) return;
|
|
253
|
-
const
|
|
257
|
+
const delay2 = Math.min(
|
|
254
258
|
this.reconnectMaxMs,
|
|
255
259
|
this.reconnectBaseMs * 2 ** this.reconnectAttempts
|
|
256
260
|
);
|
|
@@ -258,7 +262,7 @@ var RelaySubscription = class {
|
|
|
258
262
|
this.reconnectTimer = setTimeout(() => {
|
|
259
263
|
this.reconnectTimer = null;
|
|
260
264
|
if (!this.closing) this.open();
|
|
261
|
-
},
|
|
265
|
+
}, delay2);
|
|
262
266
|
this.reconnectTimer.unref?.();
|
|
263
267
|
}
|
|
264
268
|
sendReq(subId, filters) {
|
|
@@ -330,6 +334,7 @@ var RelaySubscription = class {
|
|
|
330
334
|
const evicted = this.buffer.shift();
|
|
331
335
|
if (evicted) this.seen.delete(evicted.event.id);
|
|
332
336
|
}
|
|
337
|
+
this.onEvent?.(subId, event);
|
|
333
338
|
}
|
|
334
339
|
};
|
|
335
340
|
function toText(data) {
|
|
@@ -1239,54 +1244,499 @@ function saveApexChannel(path, destination, chain, record) {
|
|
|
1239
1244
|
writeFileSync2(path, JSON.stringify(store, null, 2), { mode: 384 });
|
|
1240
1245
|
}
|
|
1241
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, retryable = false) {
|
|
1302
|
+
super(message);
|
|
1303
|
+
this.retryable = retryable;
|
|
1304
|
+
this.name = "ApexDiscoveryError";
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
async function discoverApex(params) {
|
|
1308
|
+
const { relay, ilpAddress, pubkey, chain, childPeers } = params;
|
|
1309
|
+
const timeoutMs = params.timeoutMs ?? 15e3;
|
|
1310
|
+
const pollMs = params.pollMs ?? 250;
|
|
1311
|
+
const subId = relay.subscribe(
|
|
1312
|
+
[
|
|
1313
|
+
{
|
|
1314
|
+
kinds: [ILP_PEER_INFO_KIND],
|
|
1315
|
+
...pubkey ? { authors: [pubkey] } : {}
|
|
1316
|
+
}
|
|
1317
|
+
],
|
|
1318
|
+
`apex-discovery-${ilpAddress}`
|
|
1319
|
+
);
|
|
1320
|
+
try {
|
|
1321
|
+
const deadline = Date.now() + timeoutMs;
|
|
1322
|
+
let cursor = 0;
|
|
1323
|
+
while (Date.now() < deadline) {
|
|
1324
|
+
const { events, cursor: next } = relay.getEvents({ subId, cursor });
|
|
1325
|
+
cursor = next;
|
|
1326
|
+
const match = events.find((e) => matchesApex(e, ilpAddress));
|
|
1327
|
+
if (match) return mapAnnouncement(match, { chain, childPeers });
|
|
1328
|
+
await delay(pollMs);
|
|
1329
|
+
}
|
|
1330
|
+
throw new ApexDiscoveryError(
|
|
1331
|
+
`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?`,
|
|
1332
|
+
true
|
|
1333
|
+
// retryable: the apex may just be slow/offline
|
|
1334
|
+
);
|
|
1335
|
+
} finally {
|
|
1336
|
+
relay.unsubscribe(subId);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
function matchesApex(event, ilpAddress) {
|
|
1340
|
+
if (event.kind !== ILP_PEER_INFO_KIND) return false;
|
|
1341
|
+
try {
|
|
1342
|
+
const info = parseIlpPeerInfo(event);
|
|
1343
|
+
const addrs = info.ilpAddresses ?? [info.ilpAddress];
|
|
1344
|
+
return addrs.includes(ilpAddress) || info.ilpAddress === ilpAddress;
|
|
1345
|
+
} catch {
|
|
1346
|
+
return false;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
function mapAnnouncement(event, opts) {
|
|
1350
|
+
const info = parseIlpPeerInfo(event);
|
|
1351
|
+
const chains = info.supportedChains ?? [];
|
|
1352
|
+
if (chains.length === 0) {
|
|
1353
|
+
throw new ApexDiscoveryError(
|
|
1354
|
+
`Apex "${info.ilpAddress}" announced no supportedChains \u2014 cannot settle.`
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
const chainKey = (opts.chain ? chains.find((c) => c.split(":")[0] === opts.chain) : void 0) ?? chains[0];
|
|
1358
|
+
if (!chainKey) {
|
|
1359
|
+
throw new ApexDiscoveryError(
|
|
1360
|
+
`Apex "${info.ilpAddress}" announced no usable settlement chain.`
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
const family = chainKey.split(":")[0];
|
|
1364
|
+
const settlementAddress = info.settlementAddresses?.[chainKey];
|
|
1365
|
+
if (!settlementAddress) {
|
|
1366
|
+
throw new ApexDiscoveryError(
|
|
1367
|
+
`Apex "${info.ilpAddress}" announced no settlementAddress for chain "${chainKey}".`
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
const btpUrl = info.btpEndpoint;
|
|
1371
|
+
if (!btpUrl) {
|
|
1372
|
+
throw new ApexDiscoveryError(
|
|
1373
|
+
`Apex "${info.ilpAddress}" announced an empty btpEndpoint \u2014 cannot open a BTP session.`
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
const negotiation = {
|
|
1377
|
+
destination: info.ilpAddress,
|
|
1378
|
+
peerId: info.ilpAddress.split(".").at(-1) ?? info.ilpAddress,
|
|
1379
|
+
chain: family,
|
|
1380
|
+
chainKey,
|
|
1381
|
+
// EVM chainKeys are `evm:<network>:<chainId>`; non-EVM carry no numeric id.
|
|
1382
|
+
chainId: family === "evm" ? Number(chainKey.split(":")[2] ?? 0) : 0,
|
|
1383
|
+
settlementAddress,
|
|
1384
|
+
...info.preferredTokens?.[chainKey] ? { tokenAddress: info.preferredTokens[chainKey] } : {},
|
|
1385
|
+
...info.tokenNetworks?.[chainKey] ? { tokenNetwork: info.tokenNetworks[chainKey] } : {}
|
|
1386
|
+
};
|
|
1387
|
+
return {
|
|
1388
|
+
btpUrl,
|
|
1389
|
+
negotiation,
|
|
1390
|
+
...opts.childPeers && opts.childPeers.length > 0 ? { apexChildPeers: opts.childPeers } : {}
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
function delay(ms) {
|
|
1394
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1242
1397
|
// src/daemon/client-runner.ts
|
|
1398
|
+
var MERGED_BUFFER = 5e3;
|
|
1243
1399
|
var ClientRunner = class {
|
|
1244
1400
|
config;
|
|
1245
|
-
|
|
1246
|
-
|
|
1401
|
+
createClient;
|
|
1402
|
+
createRelay;
|
|
1247
1403
|
startReadProxy;
|
|
1248
1404
|
log;
|
|
1405
|
+
targetsPath;
|
|
1249
1406
|
startedAt = Date.now();
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1407
|
+
/** Apex write targets, keyed by btpUrl. */
|
|
1408
|
+
apexes = /* @__PURE__ */ new Map();
|
|
1409
|
+
/** Relay read targets, keyed by relayUrl. */
|
|
1410
|
+
relays = /* @__PURE__ */ new Map();
|
|
1411
|
+
/** Runner-level merged read buffer across all relays (de-duped by event.id). */
|
|
1412
|
+
merged = [];
|
|
1413
|
+
mergedSeen = /* @__PURE__ */ new Set();
|
|
1414
|
+
mergedSeq = 0;
|
|
1415
|
+
/**
|
|
1416
|
+
* Fan-out subscriptions (no relayUrl restriction): replayed onto relays added
|
|
1417
|
+
* later so a new relay immediately participates in existing reads.
|
|
1418
|
+
*/
|
|
1419
|
+
fanoutSubs = /* @__PURE__ */ new Map();
|
|
1420
|
+
subIdCounter = 0;
|
|
1421
|
+
defaultBtpUrl;
|
|
1422
|
+
defaultRelayUrl;
|
|
1423
|
+
/** Teardown for the daemon-managed read proxy (btp-direct + relay-`.anyone`). */
|
|
1256
1424
|
stopReadProxy;
|
|
1257
|
-
/** Error from the read proxy (kept separate from the paid-path `lastError`). */
|
|
1258
1425
|
readProxyError;
|
|
1259
1426
|
stopped = false;
|
|
1427
|
+
started = false;
|
|
1260
1428
|
constructor(deps) {
|
|
1261
1429
|
this.config = deps.config;
|
|
1262
|
-
this.
|
|
1263
|
-
this.
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1430
|
+
this.createClient = deps.createClient;
|
|
1431
|
+
this.log = deps.logger ?? (() => void 0);
|
|
1432
|
+
if (deps.targetsPath !== void 0) this.targetsPath = deps.targetsPath;
|
|
1433
|
+
this.defaultBtpUrl = deps.config.toonClientConfig.btpUrl ?? "";
|
|
1434
|
+
this.defaultRelayUrl = deps.config.relayUrl;
|
|
1435
|
+
this.createRelay = deps.createRelay ?? ((opts) => new RelaySubscription({
|
|
1436
|
+
relayUrl: opts.relayUrl,
|
|
1437
|
+
...opts.socksProxy ? { socksProxy: opts.socksProxy } : {},
|
|
1438
|
+
...opts.logger ? { logger: opts.logger } : {},
|
|
1439
|
+
onEvent: opts.onEvent,
|
|
1440
|
+
// The TOON relay sends events TOON-encoded (text) on reads, not JSON.
|
|
1268
1441
|
decodeEvent: (raw) => decodeEventFromToon(new TextEncoder().encode(raw))
|
|
1269
|
-
});
|
|
1442
|
+
}));
|
|
1270
1443
|
this.startReadProxy = deps.startReadProxy ?? ((opts) => startManagedAnonProxy({
|
|
1271
1444
|
socksPort: opts.socksPort,
|
|
1272
1445
|
...opts.log ? { log: opts.log } : {}
|
|
1273
1446
|
}));
|
|
1274
|
-
this.
|
|
1447
|
+
this.registerRelay(this.defaultRelayUrl, this.config.socksProxy);
|
|
1448
|
+
const defaultApex = this.makeApex({
|
|
1449
|
+
btpUrl: this.defaultBtpUrl,
|
|
1450
|
+
client: this.createClient(this.config.toonClientConfig),
|
|
1451
|
+
...this.config.apex ? { negotiation: this.config.apex } : {},
|
|
1452
|
+
childPeers: this.config.apexChildPeers ?? [],
|
|
1453
|
+
destination: this.config.destination,
|
|
1454
|
+
chain: this.config.chain,
|
|
1455
|
+
channelStorePath: this.config.toonClientConfig.channelStorePath ?? this.apexChannelStorePathFor(this.defaultBtpUrl),
|
|
1456
|
+
feePerEvent: this.config.feePerEvent,
|
|
1457
|
+
isDefault: true
|
|
1458
|
+
});
|
|
1459
|
+
this.apexes.set(defaultApex.btpUrl, defaultApex);
|
|
1275
1460
|
}
|
|
1276
1461
|
/**
|
|
1277
|
-
*
|
|
1278
|
-
*
|
|
1279
|
-
*
|
|
1462
|
+
* Start the live connections: the shared read proxy, every relay socket, the
|
|
1463
|
+
* default apex bootstrap (non-blocking), then replay persisted dynamic
|
|
1464
|
+
* targets. Returns immediately; apexes become ready asynchronously.
|
|
1280
1465
|
*/
|
|
1281
1466
|
start() {
|
|
1282
|
-
if (this.
|
|
1283
|
-
this.
|
|
1467
|
+
if (this.started) return;
|
|
1468
|
+
this.started = true;
|
|
1284
1469
|
if (this.config.manageReadProxy) void this.bringUpReadProxy();
|
|
1285
|
-
this.relay.start();
|
|
1470
|
+
for (const relay of this.relays.values()) relay.start();
|
|
1286
1471
|
void this.bootstrap();
|
|
1472
|
+
this.replayPersistedTargets();
|
|
1473
|
+
}
|
|
1474
|
+
/** Await the default apex's bootstrap (kicking it off if not already running). */
|
|
1475
|
+
bootstrap() {
|
|
1476
|
+
const apex = this.apexes.get(this.defaultBtpUrl);
|
|
1477
|
+
if (!apex) return Promise.resolve();
|
|
1478
|
+
return this.bootstrapApex(apex);
|
|
1479
|
+
}
|
|
1480
|
+
// ── Relays (reads) ─────────────────────────────────────────────────────────
|
|
1481
|
+
/**
|
|
1482
|
+
* Build + register a relay (idempotent by URL), wiring its events into the
|
|
1483
|
+
* merged buffer and replaying active fan-out subscriptions. Does NOT start the
|
|
1484
|
+
* socket — callers start it (so construction stays side-effect-free for tests).
|
|
1485
|
+
*/
|
|
1486
|
+
registerRelay(relayUrl, socksProxy) {
|
|
1487
|
+
const existing = this.relays.get(relayUrl);
|
|
1488
|
+
if (existing) return existing;
|
|
1489
|
+
const relay = this.createRelay({
|
|
1490
|
+
relayUrl,
|
|
1491
|
+
...socksProxy ? { socksProxy } : {},
|
|
1492
|
+
logger: this.log,
|
|
1493
|
+
onEvent: (subId, event) => this.pushMerged(relayUrl, subId, event)
|
|
1494
|
+
});
|
|
1495
|
+
this.relays.set(relayUrl, relay);
|
|
1496
|
+
for (const [subId, filters] of this.fanoutSubs)
|
|
1497
|
+
relay.subscribe(filters, subId);
|
|
1498
|
+
return relay;
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Add a relay read target at runtime. `.anyone` relays reuse the managed read
|
|
1502
|
+
* proxy (started here if needed). Persisted unless `persist` is false.
|
|
1503
|
+
*/
|
|
1504
|
+
async addRelay(relayUrl, persist = true) {
|
|
1505
|
+
if (this.relays.has(relayUrl)) return;
|
|
1506
|
+
let socksProxy = this.config.socksProxy;
|
|
1507
|
+
if (isAnyoneHost(relayUrl) && !socksProxy) {
|
|
1508
|
+
await this.ensureReadProxy();
|
|
1509
|
+
socksProxy = `socks5h://127.0.0.1:${this.config.readProxySocksPort ?? 9050}`;
|
|
1510
|
+
}
|
|
1511
|
+
const relay = this.registerRelay(relayUrl, socksProxy);
|
|
1512
|
+
relay.start();
|
|
1513
|
+
if (persist) saveRelayTarget(relayUrl, this.targetsPath);
|
|
1514
|
+
}
|
|
1515
|
+
/** Remove a relay read target. The config-seeded default cannot be removed. */
|
|
1516
|
+
removeRelay(relayUrl) {
|
|
1517
|
+
if (relayUrl === this.defaultRelayUrl) {
|
|
1518
|
+
throw new TargetError("Cannot remove the default (config-seeded) relay.");
|
|
1519
|
+
}
|
|
1520
|
+
const relay = this.relays.get(relayUrl);
|
|
1521
|
+
if (!relay) throw new TargetError(`No such relay: ${relayUrl}`);
|
|
1522
|
+
relay.close();
|
|
1523
|
+
this.relays.delete(relayUrl);
|
|
1524
|
+
this.merged = this.merged.filter((m) => {
|
|
1525
|
+
if (m.relayUrl === relayUrl) {
|
|
1526
|
+
this.mergedSeen.delete(m.event.id);
|
|
1527
|
+
return false;
|
|
1528
|
+
}
|
|
1529
|
+
return true;
|
|
1530
|
+
});
|
|
1531
|
+
removeRelayTarget(relayUrl, this.targetsPath);
|
|
1532
|
+
}
|
|
1533
|
+
/** Mirror a newly-buffered relay event into the merged cross-relay buffer. */
|
|
1534
|
+
pushMerged(relayUrl, subId, event) {
|
|
1535
|
+
if (this.mergedSeen.has(event.id)) return;
|
|
1536
|
+
this.mergedSeen.add(event.id);
|
|
1537
|
+
this.merged.push({ seq: ++this.mergedSeq, relayUrl, subId, event });
|
|
1538
|
+
if (this.merged.length > MERGED_BUFFER) {
|
|
1539
|
+
const evicted = this.merged.shift();
|
|
1540
|
+
if (evicted) this.mergedSeen.delete(evicted.event.id);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Register a free-read subscription. With no `relayUrl` it FANS OUT across
|
|
1545
|
+
* every relay (and onto relays added later); with one it targets that relay.
|
|
1546
|
+
*/
|
|
1547
|
+
subscribe(req) {
|
|
1548
|
+
const subId = req.subId ?? `sub-${++this.subIdCounter}`;
|
|
1549
|
+
const filters = Array.isArray(req.filters) ? req.filters : [req.filters];
|
|
1550
|
+
const targets = req.relayUrl ? [req.relayUrl] : [...this.relays.keys()];
|
|
1551
|
+
if (req.relayUrl && !this.relays.has(req.relayUrl)) {
|
|
1552
|
+
throw new TargetError(`No such relay: ${req.relayUrl}`);
|
|
1553
|
+
}
|
|
1554
|
+
if (!req.relayUrl) this.fanoutSubs.set(subId, filters);
|
|
1555
|
+
for (const url of targets) this.relays.get(url)?.subscribe(filters, subId);
|
|
1556
|
+
return { subId, relays: targets };
|
|
1557
|
+
}
|
|
1558
|
+
/** Drain merged events newer than the cursor (free read), optionally scoped. */
|
|
1559
|
+
getEvents(query) {
|
|
1560
|
+
const after = query.cursor ?? 0;
|
|
1561
|
+
const limit = query.limit ?? 200;
|
|
1562
|
+
const matches = this.merged.filter(
|
|
1563
|
+
(m) => m.seq > after && (query.subId === void 0 || m.subId === query.subId) && (query.relayUrl === void 0 || m.relayUrl === query.relayUrl)
|
|
1564
|
+
);
|
|
1565
|
+
const page = matches.slice(0, limit);
|
|
1566
|
+
const hasMore = matches.length > page.length;
|
|
1567
|
+
const last = page.at(-1);
|
|
1568
|
+
return {
|
|
1569
|
+
events: page.map((m) => m.event),
|
|
1570
|
+
cursor: last ? last.seq : after,
|
|
1571
|
+
hasMore
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
// ── Apexes (writes) ──────────────────────────────────────────────────────
|
|
1575
|
+
makeApex(init) {
|
|
1576
|
+
return {
|
|
1577
|
+
...init,
|
|
1578
|
+
ready: false,
|
|
1579
|
+
bootstrapping: false
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
/**
|
|
1583
|
+
* Bootstrap one apex (memoized): start, inject negotiation, open/resume the
|
|
1584
|
+
* channel, route child peers. Concurrent callers await the same in-flight
|
|
1585
|
+
* work rather than re-running it.
|
|
1586
|
+
*/
|
|
1587
|
+
bootstrapApex(apex) {
|
|
1588
|
+
if (apex.ready) return Promise.resolve();
|
|
1589
|
+
if (!apex.bootstrapPromise) {
|
|
1590
|
+
apex.bootstrapPromise = this.doBootstrapApex(apex);
|
|
1591
|
+
}
|
|
1592
|
+
return apex.bootstrapPromise;
|
|
1593
|
+
}
|
|
1594
|
+
async doBootstrapApex(apex) {
|
|
1595
|
+
apex.bootstrapping = true;
|
|
1596
|
+
try {
|
|
1597
|
+
await apex.client.start();
|
|
1598
|
+
this.injectApexNegotiation(apex);
|
|
1599
|
+
apex.apexChannelId = await this.openOrResumeApexChannel(apex);
|
|
1600
|
+
this.routeChildPeersThroughApexChannel(apex);
|
|
1601
|
+
apex.ready = true;
|
|
1602
|
+
apex.lastError = void 0;
|
|
1603
|
+
this.log(
|
|
1604
|
+
`[runner] apex ${apex.btpUrl} ready; channel ${apex.apexChannelId}`
|
|
1605
|
+
);
|
|
1606
|
+
} catch (err) {
|
|
1607
|
+
apex.lastError = err instanceof Error ? err.message : String(err);
|
|
1608
|
+
this.log(
|
|
1609
|
+
`[runner] apex ${apex.btpUrl} bootstrap failed: ${apex.lastError}`
|
|
1610
|
+
);
|
|
1611
|
+
} finally {
|
|
1612
|
+
apex.bootstrapping = false;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Add an apex write target. Settlement params are discovered by reading the
|
|
1617
|
+
* apex's kind:10032 off the given relay (added first if unknown). Persisted.
|
|
1618
|
+
*/
|
|
1619
|
+
async addApex(req) {
|
|
1620
|
+
await this.addRelay(req.relayUrl);
|
|
1621
|
+
const relay = this.relays.get(req.relayUrl);
|
|
1622
|
+
if (!relay) throw new TargetError(`Relay unavailable: ${req.relayUrl}`);
|
|
1623
|
+
const discovered = await discoverApex({
|
|
1624
|
+
relay,
|
|
1625
|
+
ilpAddress: req.ilpAddress,
|
|
1626
|
+
...req.pubkey ? { pubkey: req.pubkey } : {},
|
|
1627
|
+
...req.chain ? { chain: req.chain } : {},
|
|
1628
|
+
...req.childPeers ? { childPeers: req.childPeers } : {}
|
|
1629
|
+
});
|
|
1630
|
+
const feePerEvent = req.feePerEvent !== void 0 ? BigInt(req.feePerEvent) : this.config.feePerEvent;
|
|
1631
|
+
await this.instantiateApex(
|
|
1632
|
+
{
|
|
1633
|
+
btpUrl: discovered.btpUrl,
|
|
1634
|
+
negotiation: discovered.negotiation,
|
|
1635
|
+
...discovered.apexChildPeers ? { apexChildPeers: discovered.apexChildPeers } : {},
|
|
1636
|
+
feePerEvent: req.feePerEvent ?? feePerEvent.toString(),
|
|
1637
|
+
discoveredFrom: req.relayUrl
|
|
1638
|
+
},
|
|
1639
|
+
true
|
|
1640
|
+
);
|
|
1641
|
+
const apex = this.apexes.get(discovered.btpUrl);
|
|
1642
|
+
if (!apex) {
|
|
1643
|
+
throw new TargetError(
|
|
1644
|
+
`Apex ${discovered.btpUrl} failed to register after discovery.`
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
return {
|
|
1648
|
+
btpUrl: apex.btpUrl,
|
|
1649
|
+
destination: apex.destination,
|
|
1650
|
+
chain: apex.chain,
|
|
1651
|
+
ready: apex.ready
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
/** Build + register + bootstrap an apex from a (persisted) target record. */
|
|
1655
|
+
async instantiateApex(target, persist) {
|
|
1656
|
+
if (this.apexes.has(target.btpUrl)) return;
|
|
1657
|
+
const clientConfig = this.deriveApexClientConfig(
|
|
1658
|
+
target.btpUrl,
|
|
1659
|
+
target.negotiation.destination
|
|
1660
|
+
);
|
|
1661
|
+
const apex = this.makeApex({
|
|
1662
|
+
btpUrl: target.btpUrl,
|
|
1663
|
+
client: this.createClient(clientConfig),
|
|
1664
|
+
negotiation: target.negotiation,
|
|
1665
|
+
childPeers: target.apexChildPeers ?? [],
|
|
1666
|
+
destination: target.negotiation.destination,
|
|
1667
|
+
chain: target.negotiation.chain,
|
|
1668
|
+
channelStorePath: this.apexChannelStorePathFor(target.btpUrl),
|
|
1669
|
+
feePerEvent: BigInt(target.feePerEvent ?? this.config.feePerEvent),
|
|
1670
|
+
isDefault: false
|
|
1671
|
+
});
|
|
1672
|
+
this.apexes.set(apex.btpUrl, apex);
|
|
1673
|
+
if (persist) saveApexTarget(target, this.targetsPath);
|
|
1674
|
+
await this.bootstrapApex(apex);
|
|
1675
|
+
}
|
|
1676
|
+
/** Remove an apex write target. The config-seeded default cannot be removed. */
|
|
1677
|
+
async removeApex(btpUrl) {
|
|
1678
|
+
if (btpUrl === this.defaultBtpUrl) {
|
|
1679
|
+
throw new TargetError("Cannot remove the default (config-seeded) apex.");
|
|
1680
|
+
}
|
|
1681
|
+
const apex = this.apexes.get(btpUrl);
|
|
1682
|
+
if (!apex) throw new TargetError(`No such apex: ${btpUrl}`);
|
|
1683
|
+
try {
|
|
1684
|
+
await apex.client.stop();
|
|
1685
|
+
} catch (err) {
|
|
1686
|
+
this.log(
|
|
1687
|
+
`[runner] apex ${btpUrl} stop error: ${err instanceof Error ? err.message : String(err)}`
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
this.apexes.delete(btpUrl);
|
|
1691
|
+
removeApexTarget(btpUrl, this.targetsPath);
|
|
1692
|
+
}
|
|
1693
|
+
/** Derive a per-apex ToonClientConfig from the default (shared identity/transport). */
|
|
1694
|
+
deriveApexClientConfig(btpUrl, destination) {
|
|
1695
|
+
const base = this.config.toonClientConfig;
|
|
1696
|
+
return {
|
|
1697
|
+
...base,
|
|
1698
|
+
btpUrl,
|
|
1699
|
+
destinationAddress: destination,
|
|
1700
|
+
// Distinct nonce-watermark store per apex so parallel ChannelManagers in
|
|
1701
|
+
// this process never race a shared channels.json.
|
|
1702
|
+
channelStorePath: this.apexChannelStorePathFor(btpUrl),
|
|
1703
|
+
ilpInfo: { ...base.ilpInfo, btpEndpoint: btpUrl }
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
apexChannelStorePathFor(btpUrl) {
|
|
1707
|
+
return `${configDir()}/channels-${sanitize(btpUrl)}.json`;
|
|
1708
|
+
}
|
|
1709
|
+
// ── Persisted-target replay ────────────────────────────────────────────────
|
|
1710
|
+
replayPersistedTargets() {
|
|
1711
|
+
let store;
|
|
1712
|
+
try {
|
|
1713
|
+
store = loadTargets(this.targetsPath);
|
|
1714
|
+
} catch (err) {
|
|
1715
|
+
this.log(
|
|
1716
|
+
`[runner] failed to load targets store: ${err instanceof Error ? err.message : String(err)}`
|
|
1717
|
+
);
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
for (const r of store.relays) {
|
|
1721
|
+
if (r.relayUrl === this.defaultRelayUrl) continue;
|
|
1722
|
+
void this.addRelay(r.relayUrl, false).catch(
|
|
1723
|
+
(err) => this.log(`[runner] replay relay ${r.relayUrl} failed: ${errMsg2(err)}`)
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
for (const a of store.apexes) {
|
|
1727
|
+
if (a.btpUrl === this.defaultBtpUrl) continue;
|
|
1728
|
+
void this.instantiateApex(a, false).catch(
|
|
1729
|
+
(err) => this.log(`[runner] replay apex ${a.btpUrl} failed: ${errMsg2(err)}`)
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
// ── Shared read proxy ──────────────────────────────────────────────────────
|
|
1734
|
+
async ensureReadProxy() {
|
|
1735
|
+
if (this.stopReadProxy) return;
|
|
1736
|
+
await this.bringUpReadProxy();
|
|
1287
1737
|
}
|
|
1288
|
-
/** Start the daemon-managed `anon` read proxy (best-effort; logs on failure). */
|
|
1289
1738
|
async bringUpReadProxy() {
|
|
1739
|
+
if (this.stopReadProxy) return;
|
|
1290
1740
|
const socksPort = this.config.readProxySocksPort ?? 9050;
|
|
1291
1741
|
try {
|
|
1292
1742
|
this.log(
|
|
@@ -1308,36 +1758,13 @@ var ClientRunner = class {
|
|
|
1308
1758
|
this.log(`[runner] managed read proxy failed: ${this.readProxyError}`);
|
|
1309
1759
|
}
|
|
1310
1760
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
this.apexChannelId = await this.openOrResumeApexChannel();
|
|
1317
|
-
this.routeChildPeersThroughApexChannel();
|
|
1318
|
-
this.ready = true;
|
|
1319
|
-
this.lastError = void 0;
|
|
1320
|
-
this.log(`[runner] ready; apex channel ${this.apexChannelId}`);
|
|
1321
|
-
} catch (err) {
|
|
1322
|
-
this.lastError = err instanceof Error ? err.message : String(err);
|
|
1323
|
-
this.log(`[runner] bootstrap failed: ${this.lastError}`);
|
|
1324
|
-
} finally {
|
|
1325
|
-
this.bootstrapping = false;
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
/**
|
|
1329
|
-
* Open the apex channel — or, on a restart, RESUME the existing one.
|
|
1330
|
-
*
|
|
1331
|
-
* `ChannelManager` persists the nonce watermark (by channelId) but not the
|
|
1332
|
-
* peer→channelId mapping, so a naive `openChannel()` after restart re-deposits
|
|
1333
|
-
* into a fresh channel and reverts on-chain. We persist the channelId here and,
|
|
1334
|
-
* when present, `trackChannel()` the live channel (which rehydrates the nonce
|
|
1335
|
-
* from the channel store) — no on-chain write, watermark continues.
|
|
1336
|
-
*/
|
|
1337
|
-
async openOrResumeApexChannel() {
|
|
1338
|
-
const { destination, chain, apexChannelStorePath } = this.config;
|
|
1761
|
+
// ── Channel / negotiation helpers (per-apex) ───────────────────────────────
|
|
1762
|
+
/** Open the apex channel — or, on a restart, RESUME the existing one. */
|
|
1763
|
+
async openOrResumeApexChannel(apex) {
|
|
1764
|
+
const { destination, chain } = apex;
|
|
1765
|
+
const { apexChannelStorePath } = this.config;
|
|
1339
1766
|
const saved = loadApexChannel(apexChannelStorePath, destination, chain);
|
|
1340
|
-
const cm =
|
|
1767
|
+
const cm = apex.client.channelManager;
|
|
1341
1768
|
if (saved && cm && typeof cm.trackChannel === "function") {
|
|
1342
1769
|
cm.trackChannel(saved.channelId, saved.context);
|
|
1343
1770
|
this.log(
|
|
@@ -1345,62 +1772,47 @@ var ClientRunner = class {
|
|
|
1345
1772
|
);
|
|
1346
1773
|
return saved.channelId;
|
|
1347
1774
|
}
|
|
1348
|
-
const channelId = await
|
|
1349
|
-
if (
|
|
1350
|
-
const a =
|
|
1775
|
+
const channelId = await apex.client.openChannel(destination);
|
|
1776
|
+
if (apex.negotiation) {
|
|
1777
|
+
const a = apex.negotiation;
|
|
1351
1778
|
saveApexChannel(apexChannelStorePath, destination, chain, {
|
|
1352
1779
|
channelId,
|
|
1353
1780
|
context: {
|
|
1354
1781
|
chainType: a.chain,
|
|
1355
1782
|
chainId: a.chainId,
|
|
1356
1783
|
tokenNetworkAddress: a.tokenNetwork ?? "",
|
|
1357
|
-
tokenAddress: a.tokenAddress,
|
|
1784
|
+
...a.tokenAddress ? { tokenAddress: a.tokenAddress } : {},
|
|
1358
1785
|
recipient: a.settlementAddress
|
|
1359
1786
|
}
|
|
1360
1787
|
});
|
|
1361
1788
|
}
|
|
1362
1789
|
return channelId;
|
|
1363
1790
|
}
|
|
1364
|
-
/**
|
|
1365
|
-
* Inject the apex settlement negotiation directly into the ToonClient when
|
|
1366
|
-
* configured. Required for HS / direct-apex mode where bootstrap discovers 0
|
|
1367
|
-
* peers (no relay-based discovery). Mirrors the docker entrypoint's approach.
|
|
1368
|
-
*/
|
|
1791
|
+
/** Inject the apex settlement negotiation directly into its ToonClient. */
|
|
1369
1792
|
injectApexNegotiation(apex) {
|
|
1370
|
-
|
|
1371
|
-
|
|
1793
|
+
const a = apex.negotiation;
|
|
1794
|
+
if (!a) return;
|
|
1795
|
+
const negotiations = apex.client.peerNegotiations;
|
|
1372
1796
|
if (!(negotiations instanceof Map)) {
|
|
1373
1797
|
throw new Error(
|
|
1374
1798
|
"ToonClient.peerNegotiations layout changed \u2014 cannot inject apex negotiation"
|
|
1375
1799
|
);
|
|
1376
1800
|
}
|
|
1377
|
-
negotiations.set(
|
|
1378
|
-
chain:
|
|
1379
|
-
chainType:
|
|
1380
|
-
chainId:
|
|
1381
|
-
settlementAddress:
|
|
1382
|
-
tokenAddress:
|
|
1383
|
-
tokenNetwork:
|
|
1801
|
+
negotiations.set(a.peerId, {
|
|
1802
|
+
chain: a.chainKey,
|
|
1803
|
+
chainType: a.chain,
|
|
1804
|
+
chainId: a.chainId,
|
|
1805
|
+
settlementAddress: a.settlementAddress,
|
|
1806
|
+
tokenAddress: a.tokenAddress,
|
|
1807
|
+
tokenNetwork: a.tokenNetwork
|
|
1384
1808
|
});
|
|
1385
|
-
this.log(`[runner] injected apex negotiation for peer "${
|
|
1809
|
+
this.log(`[runner] injected apex negotiation for peer "${a.peerId}"`);
|
|
1386
1810
|
}
|
|
1387
|
-
/**
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
* the destination's last segment (`town`/`dvm`/`mill`), so without this each
|
|
1393
|
-
* child would (a) fail the "no negotiation for peer" guard and (b) try to open
|
|
1394
|
-
* a SECOND on-chain channel to the same apex receive (which reverts —
|
|
1395
|
-
* channel-exists). So: inject the same apex negotiation under each child peer
|
|
1396
|
-
* AND pre-map its peer→channel to the already-open apex channel so
|
|
1397
|
-
* `ensureChannel` reuses it (no second open; one shared nonce sequence).
|
|
1398
|
-
*/
|
|
1399
|
-
routeChildPeersThroughApexChannel() {
|
|
1400
|
-
const apex = this.config.apex;
|
|
1401
|
-
const children = this.config.apexChildPeers ?? [];
|
|
1402
|
-
if (!apex || !this.apexChannelId || children.length === 0) return;
|
|
1403
|
-
const client = this.client;
|
|
1811
|
+
/** Route apex CHILD peers (dvm/mill) through the SAME apex payment channel. */
|
|
1812
|
+
routeChildPeersThroughApexChannel(apex) {
|
|
1813
|
+
const a = apex.negotiation;
|
|
1814
|
+
if (!a || !apex.apexChannelId || apex.childPeers.length === 0) return;
|
|
1815
|
+
const client = apex.client;
|
|
1404
1816
|
const negotiations = client.peerNegotiations;
|
|
1405
1817
|
const peerChannels = client.channelManager?.peerChannels;
|
|
1406
1818
|
if (!(negotiations instanceof Map) || !(peerChannels instanceof Map)) {
|
|
@@ -1409,70 +1821,103 @@ var ClientRunner = class {
|
|
|
1409
1821
|
);
|
|
1410
1822
|
return;
|
|
1411
1823
|
}
|
|
1412
|
-
for (const peer of
|
|
1413
|
-
if (peer ===
|
|
1824
|
+
for (const peer of apex.childPeers) {
|
|
1825
|
+
if (peer === a.peerId) continue;
|
|
1414
1826
|
negotiations.set(peer, {
|
|
1415
|
-
chain:
|
|
1416
|
-
chainType:
|
|
1417
|
-
chainId:
|
|
1418
|
-
settlementAddress:
|
|
1419
|
-
tokenAddress:
|
|
1420
|
-
tokenNetwork:
|
|
1827
|
+
chain: a.chainKey,
|
|
1828
|
+
chainType: a.chain,
|
|
1829
|
+
chainId: a.chainId,
|
|
1830
|
+
settlementAddress: a.settlementAddress,
|
|
1831
|
+
tokenAddress: a.tokenAddress,
|
|
1832
|
+
tokenNetwork: a.tokenNetwork
|
|
1421
1833
|
});
|
|
1422
|
-
peerChannels.set(peer,
|
|
1834
|
+
peerChannels.set(peer, apex.apexChannelId);
|
|
1423
1835
|
this.log(
|
|
1424
|
-
`[runner] routed child peer "${peer}" through apex channel ${
|
|
1836
|
+
`[runner] routed child peer "${peer}" through apex channel ${apex.apexChannelId}`
|
|
1425
1837
|
);
|
|
1426
1838
|
}
|
|
1427
1839
|
}
|
|
1840
|
+
// ── Status ─────────────────────────────────────────────────────────────────
|
|
1841
|
+
defaultApex() {
|
|
1842
|
+
return this.apexes.get(this.defaultBtpUrl);
|
|
1843
|
+
}
|
|
1844
|
+
/** Whether any apex has finished bootstrapping. */
|
|
1428
1845
|
isReady() {
|
|
1429
|
-
return this.ready;
|
|
1846
|
+
return [...this.apexes.values()].some((a) => a.ready);
|
|
1430
1847
|
}
|
|
1431
1848
|
isBootstrapping() {
|
|
1432
|
-
return this.bootstrapping;
|
|
1849
|
+
return [...this.apexes.values()].some((a) => a.bootstrapping);
|
|
1433
1850
|
}
|
|
1434
1851
|
getStatus() {
|
|
1435
|
-
const
|
|
1852
|
+
const apex = this.defaultApex();
|
|
1853
|
+
const client = apex?.client;
|
|
1854
|
+
const net = client?.getNetworkStatus();
|
|
1436
1855
|
const network = net ? ["evm", "solana", "mina"].map((c) => ({
|
|
1437
1856
|
chain: c,
|
|
1438
1857
|
ready: net[c] === "configured",
|
|
1439
1858
|
detail: net[c]
|
|
1440
1859
|
})) : void 0;
|
|
1860
|
+
const relay = this.relays.get(this.defaultRelayUrl);
|
|
1441
1861
|
return {
|
|
1442
1862
|
uptimeMs: Date.now() - this.startedAt,
|
|
1443
|
-
bootstrapping:
|
|
1444
|
-
ready:
|
|
1863
|
+
bootstrapping: apex?.bootstrapping ?? false,
|
|
1864
|
+
ready: apex?.ready ?? false,
|
|
1445
1865
|
settlementChain: this.config.chain,
|
|
1446
1866
|
identity: {
|
|
1447
|
-
nostrPubkey: safe(() =>
|
|
1448
|
-
evmAddress: safe(() =>
|
|
1449
|
-
solanaAddress: safe(() =>
|
|
1450
|
-
minaAddress: safe(() =>
|
|
1867
|
+
nostrPubkey: safe(() => client?.getPublicKey()) ?? "",
|
|
1868
|
+
evmAddress: safe(() => client?.getEvmAddress()),
|
|
1869
|
+
solanaAddress: safe(() => client?.getSolanaAddress()),
|
|
1870
|
+
minaAddress: safe(() => client?.getMinaAddress())
|
|
1451
1871
|
},
|
|
1452
1872
|
transport: {
|
|
1453
1873
|
type: this.config.socksProxy ? "socks5" : "direct",
|
|
1454
|
-
socksProxy: this.config.socksProxy,
|
|
1455
|
-
btpUrl:
|
|
1874
|
+
...this.config.socksProxy ? { socksProxy: this.config.socksProxy } : {},
|
|
1875
|
+
...apex ? { btpUrl: apex.btpUrl } : {}
|
|
1456
1876
|
},
|
|
1457
1877
|
relay: {
|
|
1458
|
-
url: this.
|
|
1459
|
-
connected:
|
|
1460
|
-
buffered:
|
|
1461
|
-
subscriptions:
|
|
1878
|
+
url: this.defaultRelayUrl,
|
|
1879
|
+
connected: relay?.isConnected() ?? false,
|
|
1880
|
+
buffered: relay?.bufferedCount() ?? 0,
|
|
1881
|
+
subscriptions: relay?.activeSubscriptions() ?? [],
|
|
1462
1882
|
...this.readProxyError ? { proxyError: this.readProxyError } : {}
|
|
1463
1883
|
},
|
|
1464
1884
|
...network ? { network } : {},
|
|
1465
|
-
...
|
|
1885
|
+
...apex?.lastError ? { lastError: apex.lastError } : {}
|
|
1466
1886
|
};
|
|
1467
1887
|
}
|
|
1468
|
-
/**
|
|
1888
|
+
/** Full registry of relay + apex targets with per-target status. */
|
|
1889
|
+
getTargets() {
|
|
1890
|
+
const relays = [...this.relays.entries()].map(
|
|
1891
|
+
([relayUrl, r]) => ({
|
|
1892
|
+
relayUrl,
|
|
1893
|
+
connected: r.isConnected(),
|
|
1894
|
+
buffered: r.bufferedCount(),
|
|
1895
|
+
subscriptions: r.activeSubscriptions(),
|
|
1896
|
+
isDefault: relayUrl === this.defaultRelayUrl
|
|
1897
|
+
})
|
|
1898
|
+
);
|
|
1899
|
+
const apexes = [...this.apexes.values()].map((a) => ({
|
|
1900
|
+
btpUrl: a.btpUrl,
|
|
1901
|
+
destination: a.destination,
|
|
1902
|
+
chain: a.chain,
|
|
1903
|
+
ready: a.ready,
|
|
1904
|
+
bootstrapping: a.bootstrapping,
|
|
1905
|
+
...a.apexChannelId ? { channelId: a.apexChannelId } : {},
|
|
1906
|
+
...a.lastError ? { lastError: a.lastError } : {},
|
|
1907
|
+
isDefault: a.isDefault
|
|
1908
|
+
}));
|
|
1909
|
+
return { relays, apexes };
|
|
1910
|
+
}
|
|
1911
|
+
// ── Paid operations ──────────────────────────────────────────────────────
|
|
1912
|
+
/** Pay-to-write a single event through the selected (or default) apex. */
|
|
1469
1913
|
async publish(req) {
|
|
1470
|
-
this.
|
|
1471
|
-
|
|
1472
|
-
const
|
|
1473
|
-
const
|
|
1474
|
-
const
|
|
1475
|
-
|
|
1914
|
+
const apex = this.selectApex(req.btpUrl);
|
|
1915
|
+
this.assertApexReady(apex);
|
|
1916
|
+
const channelId = apex.apexChannelId ?? await apex.client.openChannel(req.destination);
|
|
1917
|
+
const fee = req.fee !== void 0 ? BigInt(req.fee) : apex.feePerEvent;
|
|
1918
|
+
const claim = await apex.client.signBalanceProof(channelId, fee);
|
|
1919
|
+
const result = await apex.client.publishEvent(req.event, {
|
|
1920
|
+
...req.destination ? { destination: req.destination } : {},
|
|
1476
1921
|
claim,
|
|
1477
1922
|
ilpAmount: fee
|
|
1478
1923
|
});
|
|
@@ -1481,63 +1926,47 @@ var ClientRunner = class {
|
|
|
1481
1926
|
}
|
|
1482
1927
|
return {
|
|
1483
1928
|
eventId: result.eventId ?? req.event.id,
|
|
1484
|
-
data: result.data,
|
|
1929
|
+
...result.data !== void 0 ? { data: result.data } : {},
|
|
1485
1930
|
channelId,
|
|
1486
|
-
nonce:
|
|
1931
|
+
nonce: apex.client.getChannelNonce(channelId)
|
|
1487
1932
|
};
|
|
1488
1933
|
}
|
|
1489
|
-
/**
|
|
1490
|
-
|
|
1491
|
-
const
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
getEvents(query) {
|
|
1496
|
-
const opts = {};
|
|
1497
|
-
if (query.subId !== void 0) opts.subId = query.subId;
|
|
1498
|
-
if (query.cursor !== void 0) opts.cursor = query.cursor;
|
|
1499
|
-
if (query.limit !== void 0) opts.limit = query.limit;
|
|
1500
|
-
const { events, cursor, hasMore } = this.relay.getEvents(opts);
|
|
1501
|
-
return { events, cursor, hasMore };
|
|
1502
|
-
}
|
|
1503
|
-
/** Open (or return) a payment channel for a destination. */
|
|
1504
|
-
async openChannel(destination) {
|
|
1505
|
-
this.assertReady();
|
|
1506
|
-
const channelId = await this.client.openChannel(
|
|
1507
|
-
destination ?? this.config.destination
|
|
1934
|
+
/** Open (or return) a payment channel on the selected (or default) apex. */
|
|
1935
|
+
async openChannel(destination, btpUrl) {
|
|
1936
|
+
const apex = this.selectApex(btpUrl);
|
|
1937
|
+
this.assertApexReady(apex);
|
|
1938
|
+
const channelId = await apex.client.openChannel(
|
|
1939
|
+
destination ?? apex.destination
|
|
1508
1940
|
);
|
|
1509
|
-
if (!destination || destination ===
|
|
1510
|
-
|
|
1941
|
+
if (!destination || destination === apex.destination) {
|
|
1942
|
+
apex.apexChannelId = channelId;
|
|
1511
1943
|
}
|
|
1512
1944
|
return { channelId };
|
|
1513
1945
|
}
|
|
1514
|
-
/** List tracked channels with
|
|
1946
|
+
/** List tracked channels across ALL apexes with nonce + cumulative amount. */
|
|
1515
1947
|
getChannels() {
|
|
1516
|
-
const
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1948
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1949
|
+
const channels = [];
|
|
1950
|
+
for (const apex of this.apexes.values()) {
|
|
1951
|
+
for (const channelId of apex.client.getTrackedChannels()) {
|
|
1952
|
+
if (seen.has(channelId)) continue;
|
|
1953
|
+
seen.add(channelId);
|
|
1954
|
+
channels.push({
|
|
1955
|
+
channelId,
|
|
1956
|
+
nonce: apex.client.getChannelNonce(channelId),
|
|
1957
|
+
cumulativeAmount: apex.client.getChannelCumulativeAmount(channelId).toString()
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1521
1961
|
return { channels };
|
|
1522
1962
|
}
|
|
1523
|
-
/**
|
|
1524
|
-
* Swap source asset → target asset against a mill peer.
|
|
1525
|
-
*
|
|
1526
|
-
* Uses SDK `streamSwap`, which builds a NIP-59 gift-wrapped kind:20032 swap
|
|
1527
|
-
* rumor per packet and sends it over the open BTP session. The source-asset
|
|
1528
|
-
* balance proof is signed by the ToonClient's ChannelManager against the apex
|
|
1529
|
-
* channel — so the mill peer MUST be routed via `apexChildPeers` (otherwise
|
|
1530
|
-
* there is no channel to sign against and the mill rejects with F99).
|
|
1531
|
-
*
|
|
1532
|
-
* A fresh ephemeral gift-wrap key is generated per swap (used for sealing the
|
|
1533
|
-
* rumor AND decrypting the FULFILL claims) — independent of the daemon's
|
|
1534
|
-
* settlement identity, so callers never need to expose a key.
|
|
1535
|
-
*/
|
|
1963
|
+
/** Swap source→target asset against a mill peer via the selected apex. */
|
|
1536
1964
|
async swap(req) {
|
|
1537
|
-
this.
|
|
1965
|
+
const apex = this.selectApex(req.btpUrl);
|
|
1966
|
+
this.assertApexReady(apex);
|
|
1538
1967
|
const senderSecretKey = generateSecretKey2();
|
|
1539
1968
|
const result = await streamSwap({
|
|
1540
|
-
client:
|
|
1969
|
+
client: apex.client,
|
|
1541
1970
|
millPubkey: req.millPubkey,
|
|
1542
1971
|
millIlpAddress: req.destination,
|
|
1543
1972
|
pair: req.pair,
|
|
@@ -1567,33 +1996,42 @@ var ClientRunner = class {
|
|
|
1567
1996
|
...firstReject ? { code: firstReject.code, message: firstReject.message } : {}
|
|
1568
1997
|
};
|
|
1569
1998
|
}
|
|
1570
|
-
/** Graceful teardown
|
|
1999
|
+
/** Graceful teardown: close every relay + stop every apex client + read proxy. */
|
|
1571
2000
|
async stop() {
|
|
1572
2001
|
if (this.stopped) return;
|
|
1573
2002
|
this.stopped = true;
|
|
1574
|
-
this.relay.close();
|
|
2003
|
+
for (const relay of this.relays.values()) relay.close();
|
|
1575
2004
|
if (this.stopReadProxy) {
|
|
1576
2005
|
try {
|
|
1577
2006
|
await this.stopReadProxy();
|
|
1578
2007
|
} catch (err) {
|
|
1579
|
-
this.log(
|
|
1580
|
-
`[runner] read proxy stop error: ${err instanceof Error ? err.message : String(err)}`
|
|
1581
|
-
);
|
|
2008
|
+
this.log(`[runner] read proxy stop error: ${errMsg2(err)}`);
|
|
1582
2009
|
}
|
|
1583
2010
|
this.stopReadProxy = void 0;
|
|
1584
2011
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
`[runner] client stop error
|
|
1590
|
-
|
|
2012
|
+
for (const apex of this.apexes.values()) {
|
|
2013
|
+
try {
|
|
2014
|
+
await apex.client.stop();
|
|
2015
|
+
} catch (err) {
|
|
2016
|
+
this.log(`[runner] client stop error (${apex.btpUrl}): ${errMsg2(err)}`);
|
|
2017
|
+
}
|
|
1591
2018
|
}
|
|
1592
2019
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
2020
|
+
// ── internals ────────────────────────────────────────────────────────────
|
|
2021
|
+
selectApex(btpUrl) {
|
|
2022
|
+
if (btpUrl) {
|
|
2023
|
+
const apex = this.apexes.get(btpUrl);
|
|
2024
|
+
if (!apex) throw new TargetError(`No such apex: ${btpUrl}`);
|
|
2025
|
+
return apex;
|
|
2026
|
+
}
|
|
2027
|
+
const def = this.defaultApex();
|
|
2028
|
+
if (!def) throw new NotReadyError("No apex configured.");
|
|
2029
|
+
return def;
|
|
2030
|
+
}
|
|
2031
|
+
assertApexReady(apex) {
|
|
2032
|
+
if (!apex.ready) {
|
|
1595
2033
|
throw new NotReadyError(
|
|
1596
|
-
|
|
2034
|
+
apex.bootstrapping ? "Apex is still bootstrapping (BTP/anon coming up) \u2014 retry shortly." : apex.lastError ?? "Apex is not ready."
|
|
1597
2035
|
);
|
|
1598
2036
|
}
|
|
1599
2037
|
}
|
|
@@ -1611,6 +2049,12 @@ var PublishRejectedError = class extends Error {
|
|
|
1611
2049
|
this.name = "PublishRejectedError";
|
|
1612
2050
|
}
|
|
1613
2051
|
};
|
|
2052
|
+
var TargetError = class extends Error {
|
|
2053
|
+
constructor(message) {
|
|
2054
|
+
super(message);
|
|
2055
|
+
this.name = "TargetError";
|
|
2056
|
+
}
|
|
2057
|
+
};
|
|
1614
2058
|
function safe(fn) {
|
|
1615
2059
|
try {
|
|
1616
2060
|
return fn();
|
|
@@ -1618,6 +2062,19 @@ function safe(fn) {
|
|
|
1618
2062
|
return void 0;
|
|
1619
2063
|
}
|
|
1620
2064
|
}
|
|
2065
|
+
function errMsg2(err) {
|
|
2066
|
+
return err instanceof Error ? err.message : String(err);
|
|
2067
|
+
}
|
|
2068
|
+
function sanitize(s) {
|
|
2069
|
+
return s.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
2070
|
+
}
|
|
2071
|
+
function isAnyoneHost(url) {
|
|
2072
|
+
try {
|
|
2073
|
+
return new URL(url).hostname.endsWith(".anyone");
|
|
2074
|
+
} catch {
|
|
2075
|
+
return false;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
1621
2078
|
|
|
1622
2079
|
// src/daemon/routes.ts
|
|
1623
2080
|
function registerRoutes(app, runner) {
|
|
@@ -1642,19 +2099,21 @@ function registerRoutes(app, runner) {
|
|
|
1642
2099
|
detail: "body.filters is required (a NIP-01 filter or array of filters)."
|
|
1643
2100
|
});
|
|
1644
2101
|
}
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
async (req) => {
|
|
1650
|
-
const q = req.query;
|
|
1651
|
-
const query = {};
|
|
1652
|
-
if (q.subId) query.subId = q.subId;
|
|
1653
|
-
if (q.cursor !== void 0) query.cursor = Number(q.cursor);
|
|
1654
|
-
if (q.limit !== void 0) query.limit = Number(q.limit);
|
|
1655
|
-
return runner.getEvents(query);
|
|
2102
|
+
try {
|
|
2103
|
+
return runner.subscribe(body);
|
|
2104
|
+
} catch (err) {
|
|
2105
|
+
return mapError(reply, err);
|
|
1656
2106
|
}
|
|
1657
|
-
);
|
|
2107
|
+
});
|
|
2108
|
+
app.get("/events", async (req) => {
|
|
2109
|
+
const q = req.query;
|
|
2110
|
+
const query = {};
|
|
2111
|
+
if (q.subId) query.subId = q.subId;
|
|
2112
|
+
if (q.cursor !== void 0) query.cursor = Number(q.cursor);
|
|
2113
|
+
if (q.limit !== void 0) query.limit = Number(q.limit);
|
|
2114
|
+
if (q.relayUrl) query.relayUrl = q.relayUrl;
|
|
2115
|
+
return runner.getEvents(query);
|
|
2116
|
+
});
|
|
1658
2117
|
app.post("/channels", async (req, reply) => {
|
|
1659
2118
|
try {
|
|
1660
2119
|
return await runner.openChannel(req.body?.destination);
|
|
@@ -1676,6 +2135,62 @@ function registerRoutes(app, runner) {
|
|
|
1676
2135
|
return mapError(reply, err);
|
|
1677
2136
|
}
|
|
1678
2137
|
});
|
|
2138
|
+
app.get("/targets", async () => runner.getTargets());
|
|
2139
|
+
app.post("/relays", async (req, reply) => {
|
|
2140
|
+
const url = req.body?.relayUrl;
|
|
2141
|
+
if (!url) {
|
|
2142
|
+
return sendError(reply, 400, "invalid_relay", {
|
|
2143
|
+
detail: "body.relayUrl is required."
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
try {
|
|
2147
|
+
await runner.addRelay(url);
|
|
2148
|
+
return runner.getTargets();
|
|
2149
|
+
} catch (err) {
|
|
2150
|
+
return mapError(reply, err);
|
|
2151
|
+
}
|
|
2152
|
+
});
|
|
2153
|
+
app.delete("/relays", async (req, reply) => {
|
|
2154
|
+
const url = req.body?.relayUrl;
|
|
2155
|
+
if (!url) {
|
|
2156
|
+
return sendError(reply, 400, "invalid_relay", {
|
|
2157
|
+
detail: "body.relayUrl is required."
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
try {
|
|
2161
|
+
runner.removeRelay(url);
|
|
2162
|
+
return runner.getTargets();
|
|
2163
|
+
} catch (err) {
|
|
2164
|
+
return mapError(reply, err);
|
|
2165
|
+
}
|
|
2166
|
+
});
|
|
2167
|
+
app.post("/apex", async (req, reply) => {
|
|
2168
|
+
const body = req.body;
|
|
2169
|
+
if (!body || !body.ilpAddress || !body.relayUrl) {
|
|
2170
|
+
return sendError(reply, 400, "invalid_apex", {
|
|
2171
|
+
detail: "body.ilpAddress and body.relayUrl are required."
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
try {
|
|
2175
|
+
return await runner.addApex(body);
|
|
2176
|
+
} catch (err) {
|
|
2177
|
+
return mapError(reply, err);
|
|
2178
|
+
}
|
|
2179
|
+
});
|
|
2180
|
+
app.delete("/apex", async (req, reply) => {
|
|
2181
|
+
const url = req.body?.btpUrl;
|
|
2182
|
+
if (!url) {
|
|
2183
|
+
return sendError(reply, 400, "invalid_apex", {
|
|
2184
|
+
detail: "body.btpUrl is required."
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
try {
|
|
2188
|
+
await runner.removeApex(url);
|
|
2189
|
+
return runner.getTargets();
|
|
2190
|
+
} catch (err) {
|
|
2191
|
+
return mapError(reply, err);
|
|
2192
|
+
}
|
|
2193
|
+
});
|
|
1679
2194
|
}
|
|
1680
2195
|
function isSignedEvent(event) {
|
|
1681
2196
|
if (typeof event !== "object" || event === null) return false;
|
|
@@ -1692,6 +2207,16 @@ function mapError(reply, err) {
|
|
|
1692
2207
|
if (err instanceof PublishRejectedError) {
|
|
1693
2208
|
return sendError(reply, 502, "rejected", { detail: err.message });
|
|
1694
2209
|
}
|
|
2210
|
+
if (err instanceof TargetError) {
|
|
2211
|
+
const status = /no such/i.test(err.message) ? 404 : 400;
|
|
2212
|
+
return sendError(reply, status, "invalid_target", { detail: err.message });
|
|
2213
|
+
}
|
|
2214
|
+
if (err instanceof ApexDiscoveryError) {
|
|
2215
|
+
return err.retryable ? sendError(reply, 504, "discovery_timeout", {
|
|
2216
|
+
detail: err.message,
|
|
2217
|
+
retryable: true
|
|
2218
|
+
}) : sendError(reply, 502, "discovery_failed", { detail: err.message });
|
|
2219
|
+
}
|
|
1695
2220
|
return sendError(reply, 500, "internal_error", {
|
|
1696
2221
|
detail: err instanceof Error ? err.message : String(err)
|
|
1697
2222
|
});
|
|
@@ -1714,4 +2239,4 @@ export {
|
|
|
1714
2239
|
PublishRejectedError,
|
|
1715
2240
|
registerRoutes
|
|
1716
2241
|
};
|
|
1717
|
-
//# sourceMappingURL=chunk-
|
|
2242
|
+
//# sourceMappingURL=chunk-7WCDT25E.js.map
|