@toon-protocol/client 0.16.0 → 0.18.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/index.d.ts +439 -28
- package/dist/index.js +630 -99
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -153,7 +153,7 @@ async function deriveSolanaKey(seed, accountIndex = 0) {
|
|
|
153
153
|
const { ed25519: ed255194 } = await import("@noble/curves/ed25519.js");
|
|
154
154
|
const encoder = new TextEncoder();
|
|
155
155
|
let I = hmac(sha512, encoder.encode("ed25519 seed"), seed);
|
|
156
|
-
let
|
|
156
|
+
let key2 = I.slice(0, 32);
|
|
157
157
|
let chainCode = I.slice(32);
|
|
158
158
|
const indices = [
|
|
159
159
|
2147483692,
|
|
@@ -168,18 +168,18 @@ async function deriveSolanaKey(seed, accountIndex = 0) {
|
|
|
168
168
|
for (const index of indices) {
|
|
169
169
|
const data = new Uint8Array(37);
|
|
170
170
|
data[0] = 0;
|
|
171
|
-
data.set(
|
|
171
|
+
data.set(key2, 1);
|
|
172
172
|
data[33] = index >>> 24 & 255;
|
|
173
173
|
data[34] = index >>> 16 & 255;
|
|
174
174
|
data[35] = index >>> 8 & 255;
|
|
175
175
|
data[36] = index & 255;
|
|
176
176
|
I = hmac(sha512, chainCode, data);
|
|
177
|
-
|
|
177
|
+
key2 = I.slice(0, 32);
|
|
178
178
|
chainCode = I.slice(32);
|
|
179
179
|
}
|
|
180
|
-
const publicKeyBytes = ed255194.getPublicKey(
|
|
180
|
+
const publicKeyBytes = ed255194.getPublicKey(key2);
|
|
181
181
|
const keypair = new Uint8Array(64);
|
|
182
|
-
keypair.set(
|
|
182
|
+
keypair.set(key2, 0);
|
|
183
183
|
keypair.set(publicKeyBytes, 32);
|
|
184
184
|
const publicKey = toBase58(publicKeyBytes);
|
|
185
185
|
return { secretKey: keypair, publicKey };
|
|
@@ -894,9 +894,13 @@ function deserializeIlpPacket(buf) {
|
|
|
894
894
|
}
|
|
895
895
|
function deserializeIlpFulfill(buf) {
|
|
896
896
|
let offset = 1;
|
|
897
|
+
if (buf.length < offset + 32) {
|
|
898
|
+
throw new Error("Buffer underflow reading FULFILL fulfillment");
|
|
899
|
+
}
|
|
900
|
+
const fulfillment = buf.slice(offset, offset + 32);
|
|
897
901
|
offset += 32;
|
|
898
902
|
const { value: data } = decodeVarOctetString(buf, offset);
|
|
899
|
-
return { type: ILPPacketType.FULFILL, data };
|
|
903
|
+
return { type: ILPPacketType.FULFILL, fulfillment, data };
|
|
900
904
|
}
|
|
901
905
|
function deserializeIlpReject(buf) {
|
|
902
906
|
let offset = 1;
|
|
@@ -1207,8 +1211,10 @@ var IsomorphicBtpClient = class {
|
|
|
1207
1211
|
this.pendingRequests.delete(id);
|
|
1208
1212
|
if (json["type"] === "FULFILL") {
|
|
1209
1213
|
const responseData = json["data"] ? this.base64ToUint8Array(json["data"]) : new Uint8Array(0);
|
|
1214
|
+
const fulfillment = json["fulfillment"] ? this.base64ToUint8Array(json["fulfillment"]) : new Uint8Array(32);
|
|
1210
1215
|
pending.resolve({
|
|
1211
1216
|
type: ILPPacketType.FULFILL,
|
|
1217
|
+
fulfillment,
|
|
1212
1218
|
data: responseData
|
|
1213
1219
|
});
|
|
1214
1220
|
} else {
|
|
@@ -1269,6 +1275,74 @@ var IsomorphicBtpClient = class {
|
|
|
1269
1275
|
}
|
|
1270
1276
|
};
|
|
1271
1277
|
|
|
1278
|
+
// src/utils/condition.ts
|
|
1279
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
1280
|
+
import { randomBytes } from "@noble/hashes/utils.js";
|
|
1281
|
+
var CONDITION_LENGTH = 32;
|
|
1282
|
+
function mintExecutionCondition() {
|
|
1283
|
+
const preimage = randomBytes(CONDITION_LENGTH);
|
|
1284
|
+
return { preimage, condition: sha256(preimage) };
|
|
1285
|
+
}
|
|
1286
|
+
function isZeroCondition(condition) {
|
|
1287
|
+
if (condition === void 0) return true;
|
|
1288
|
+
for (const byte of condition) {
|
|
1289
|
+
if (byte !== 0) return false;
|
|
1290
|
+
}
|
|
1291
|
+
return true;
|
|
1292
|
+
}
|
|
1293
|
+
function assertValidCondition(condition) {
|
|
1294
|
+
if (condition.length !== CONDITION_LENGTH) {
|
|
1295
|
+
throw new Error(
|
|
1296
|
+
`executionCondition must be exactly ${CONDITION_LENGTH} bytes, got ${condition.length}`
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function fulfillmentMatchesCondition(fulfillment, condition) {
|
|
1301
|
+
if (fulfillment === void 0 || fulfillment.length !== CONDITION_LENGTH) {
|
|
1302
|
+
return false;
|
|
1303
|
+
}
|
|
1304
|
+
const digest = sha256(fulfillment);
|
|
1305
|
+
for (let i = 0; i < CONDITION_LENGTH; i++) {
|
|
1306
|
+
if (digest[i] !== condition[i]) return false;
|
|
1307
|
+
}
|
|
1308
|
+
return true;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// src/adapters/ilp-send.ts
|
|
1312
|
+
function resolveExpiresAt(expiresAt, timeoutMs) {
|
|
1313
|
+
if (expiresAt === void 0) return new Date(Date.now() + timeoutMs);
|
|
1314
|
+
return expiresAt instanceof Date ? new Date(expiresAt.getTime()) : new Date(expiresAt);
|
|
1315
|
+
}
|
|
1316
|
+
var FULFILLMENT_MISMATCH_CODE = "F99";
|
|
1317
|
+
var FULFILLMENT_MISMATCH_MESSAGE = "FULFILL fulfillment does not match execution condition (sha256(fulfillment) != executionCondition) \u2014 packet counted failed";
|
|
1318
|
+
function mapIlpResponse(packet, sentCondition) {
|
|
1319
|
+
if (packet.type === ILPPacketType.FULFILL) {
|
|
1320
|
+
const dataField = packet.data.length > 0 ? { data: toBase64(packet.data) } : {};
|
|
1321
|
+
if (sentCondition === void 0 || isZeroCondition(sentCondition)) {
|
|
1322
|
+
return { accepted: true, ...dataField };
|
|
1323
|
+
}
|
|
1324
|
+
if (!fulfillmentMatchesCondition(packet.fulfillment, sentCondition)) {
|
|
1325
|
+
return {
|
|
1326
|
+
accepted: false,
|
|
1327
|
+
code: FULFILLMENT_MISMATCH_CODE,
|
|
1328
|
+
message: FULFILLMENT_MISMATCH_MESSAGE,
|
|
1329
|
+
...dataField
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
return {
|
|
1333
|
+
accepted: true,
|
|
1334
|
+
fulfillment: toBase64(packet.fulfillment),
|
|
1335
|
+
...dataField
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
return {
|
|
1339
|
+
accepted: false,
|
|
1340
|
+
code: packet.code,
|
|
1341
|
+
message: packet.message,
|
|
1342
|
+
...packet.data.length > 0 ? { data: toBase64(packet.data) } : {}
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1272
1346
|
// src/adapters/BtpRuntimeClient.ts
|
|
1273
1347
|
function isConnectionError(error) {
|
|
1274
1348
|
const msg = error.message.toLowerCase();
|
|
@@ -1324,6 +1398,12 @@ var BtpRuntimeClient = class {
|
|
|
1324
1398
|
/**
|
|
1325
1399
|
* Sends an ILP packet via BTP with auto-reconnect on connection errors.
|
|
1326
1400
|
* Satisfies IlpClient interface.
|
|
1401
|
+
*
|
|
1402
|
+
* `params` may carry a sender-chosen `executionCondition` and an explicit
|
|
1403
|
+
* `expiresAt` (toon-client#350); omitting both is the legacy zero-condition
|
|
1404
|
+
* path, unchanged. With a non-zero condition the FULFILL preimage is
|
|
1405
|
+
* verified (`sha256(fulfillment) == condition`) and a mismatch is surfaced
|
|
1406
|
+
* as a failed result — see `mapIlpResponse`.
|
|
1327
1407
|
*/
|
|
1328
1408
|
async sendIlpPacket(params) {
|
|
1329
1409
|
return withRetry(() => this._sendIlpPacketOnce(params), {
|
|
@@ -1339,6 +1419,9 @@ var BtpRuntimeClient = class {
|
|
|
1339
1419
|
/**
|
|
1340
1420
|
* Sends a balance proof claim via BTP protocol data, then sends an ILP packet.
|
|
1341
1421
|
* Auto-reconnects on connection errors.
|
|
1422
|
+
*
|
|
1423
|
+
* Sender-chosen `executionCondition` / explicit `expiresAt` semantics are
|
|
1424
|
+
* identical to {@link sendIlpPacket}.
|
|
1342
1425
|
*/
|
|
1343
1426
|
async sendIlpPacketWithClaim(params, claim) {
|
|
1344
1427
|
return withRetry(() => this._sendIlpPacketWithClaimOnce(params, claim), {
|
|
@@ -1381,6 +1464,30 @@ var BtpRuntimeClient = class {
|
|
|
1381
1464
|
encodeUtf8(JSON.stringify(claim))
|
|
1382
1465
|
);
|
|
1383
1466
|
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Build the ILP PREPARE for a send, applying the sender-chosen condition /
|
|
1469
|
+
* explicit expiry when provided (toon-client#350) and validating that a
|
|
1470
|
+
* non-zero condition is exactly 32 bytes (the OER serializer would
|
|
1471
|
+
* otherwise silently zero-fill it, downgrading the packet to the legacy
|
|
1472
|
+
* unverified class).
|
|
1473
|
+
*/
|
|
1474
|
+
buildPrepare(params) {
|
|
1475
|
+
const condition = params.executionCondition;
|
|
1476
|
+
if (condition !== void 0 && !isZeroCondition(condition)) {
|
|
1477
|
+
assertValidCondition(condition);
|
|
1478
|
+
}
|
|
1479
|
+
return {
|
|
1480
|
+
prepare: {
|
|
1481
|
+
type: 12,
|
|
1482
|
+
amount: BigInt(params.amount),
|
|
1483
|
+
destination: params.destination,
|
|
1484
|
+
executionCondition: condition ?? new Uint8Array(32),
|
|
1485
|
+
expiresAt: resolveExpiresAt(params.expiresAt, params.timeout ?? 3e4),
|
|
1486
|
+
data: fromBase64(params.data)
|
|
1487
|
+
},
|
|
1488
|
+
condition
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1384
1491
|
/**
|
|
1385
1492
|
* Single-attempt ILP packet send. Reconnects if not connected.
|
|
1386
1493
|
*/
|
|
@@ -1388,26 +1495,9 @@ var BtpRuntimeClient = class {
|
|
|
1388
1495
|
if (!this._isConnected) {
|
|
1389
1496
|
await this.reconnect();
|
|
1390
1497
|
}
|
|
1391
|
-
const
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
destination: params.destination,
|
|
1395
|
-
executionCondition: new Uint8Array(32),
|
|
1396
|
-
expiresAt: new Date(Date.now() + (params.timeout ?? 3e4)),
|
|
1397
|
-
data: fromBase64(params.data)
|
|
1398
|
-
});
|
|
1399
|
-
if (response.type === ILPPacketType.FULFILL) {
|
|
1400
|
-
return {
|
|
1401
|
-
accepted: true,
|
|
1402
|
-
data: response.data.length > 0 ? toBase64(response.data) : void 0
|
|
1403
|
-
};
|
|
1404
|
-
}
|
|
1405
|
-
return {
|
|
1406
|
-
accepted: false,
|
|
1407
|
-
code: response.code,
|
|
1408
|
-
message: response.message,
|
|
1409
|
-
data: response.data.length > 0 ? toBase64(response.data) : void 0
|
|
1410
|
-
};
|
|
1498
|
+
const { prepare, condition } = this.buildPrepare(params);
|
|
1499
|
+
const response = await this.btpClient.sendPacket(prepare);
|
|
1500
|
+
return mapIlpResponse(response, condition);
|
|
1411
1501
|
}
|
|
1412
1502
|
/**
|
|
1413
1503
|
* Single-attempt claim + ILP packet send. Reconnects if not connected.
|
|
@@ -1426,29 +1516,9 @@ var BtpRuntimeClient = class {
|
|
|
1426
1516
|
data: encodeUtf8(JSON.stringify(claim))
|
|
1427
1517
|
}
|
|
1428
1518
|
];
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
amount: BigInt(params.amount),
|
|
1433
|
-
destination: params.destination,
|
|
1434
|
-
executionCondition: new Uint8Array(32),
|
|
1435
|
-
expiresAt: new Date(Date.now() + (params.timeout ?? 3e4)),
|
|
1436
|
-
data: fromBase64(params.data)
|
|
1437
|
-
},
|
|
1438
|
-
protocolData
|
|
1439
|
-
);
|
|
1440
|
-
if (response.type === ILPPacketType.FULFILL) {
|
|
1441
|
-
return {
|
|
1442
|
-
accepted: true,
|
|
1443
|
-
data: response.data.length > 0 ? toBase64(response.data) : void 0
|
|
1444
|
-
};
|
|
1445
|
-
}
|
|
1446
|
-
return {
|
|
1447
|
-
accepted: false,
|
|
1448
|
-
code: response.code,
|
|
1449
|
-
message: response.message,
|
|
1450
|
-
data: response.data.length > 0 ? toBase64(response.data) : void 0
|
|
1451
|
-
};
|
|
1519
|
+
const { prepare, condition } = this.buildPrepare(params);
|
|
1520
|
+
const response = await this.btpClient.sendPacket(prepare, protocolData);
|
|
1521
|
+
return mapIlpResponse(response, condition);
|
|
1452
1522
|
}
|
|
1453
1523
|
};
|
|
1454
1524
|
|
|
@@ -1480,6 +1550,12 @@ var HttpIlpClient = class {
|
|
|
1480
1550
|
* Send an ILP PREPARE via `POST /ilp` WITHOUT a claim. The connector accepts
|
|
1481
1551
|
* this only on free/zero-amount routes; paid writes must use
|
|
1482
1552
|
* {@link sendIlpPacketWithClaim}. Satisfies the IlpClient interface.
|
|
1553
|
+
*
|
|
1554
|
+
* `params` may carry a sender-chosen `executionCondition` and an explicit
|
|
1555
|
+
* `expiresAt` (toon-client#350); omitting both is the legacy zero-condition
|
|
1556
|
+
* path, unchanged. With a non-zero condition the FULFILL preimage is
|
|
1557
|
+
* verified (`sha256(fulfillment) == condition`) and a mismatch is surfaced
|
|
1558
|
+
* as a failed result — see {@link mapIlpResponse}.
|
|
1483
1559
|
*/
|
|
1484
1560
|
async sendIlpPacket(params) {
|
|
1485
1561
|
return withRetry(() => this.postPrepare(params), {
|
|
@@ -1494,6 +1570,9 @@ var HttpIlpClient = class {
|
|
|
1494
1570
|
* as the `ILP-Payment-Channel-Claim` header. `claim` is the SAME JSON object
|
|
1495
1571
|
* the BTP path attaches as the `payment-channel-claim` protocolData entry —
|
|
1496
1572
|
* we base64(JSON.stringify(claim)) it, byte-for-byte identical to BTP.
|
|
1573
|
+
*
|
|
1574
|
+
* Sender-chosen `executionCondition` / explicit `expiresAt` semantics are
|
|
1575
|
+
* identical to {@link sendIlpPacket}.
|
|
1497
1576
|
*/
|
|
1498
1577
|
async sendIlpPacketWithClaim(params, claim) {
|
|
1499
1578
|
return withRetry(() => this.postPrepare(params, claim), {
|
|
@@ -1546,12 +1625,16 @@ var HttpIlpClient = class {
|
|
|
1546
1625
|
*/
|
|
1547
1626
|
async postPrepare(params, claim) {
|
|
1548
1627
|
const requestTimeout = params.timeout ?? this.timeout;
|
|
1628
|
+
const condition = params.executionCondition;
|
|
1629
|
+
if (condition !== void 0 && !isZeroCondition(condition)) {
|
|
1630
|
+
assertValidCondition(condition);
|
|
1631
|
+
}
|
|
1549
1632
|
const prepare = serializeIlpPrepare({
|
|
1550
1633
|
type: ILPPacketType.PREPARE,
|
|
1551
1634
|
amount: BigInt(params.amount),
|
|
1552
1635
|
destination: params.destination,
|
|
1553
|
-
executionCondition: new Uint8Array(32),
|
|
1554
|
-
expiresAt:
|
|
1636
|
+
executionCondition: condition ?? new Uint8Array(32),
|
|
1637
|
+
expiresAt: resolveExpiresAt(params.expiresAt, requestTimeout),
|
|
1555
1638
|
data: fromBase64(params.data)
|
|
1556
1639
|
});
|
|
1557
1640
|
const headers = {
|
|
@@ -1574,7 +1657,7 @@ var HttpIlpClient = class {
|
|
|
1574
1657
|
signal: controller.signal
|
|
1575
1658
|
});
|
|
1576
1659
|
clearTimeout(timeoutId);
|
|
1577
|
-
return await this.mapResponse(response);
|
|
1660
|
+
return await this.mapResponse(response, condition);
|
|
1578
1661
|
} catch (error) {
|
|
1579
1662
|
clearTimeout(timeoutId);
|
|
1580
1663
|
throw this.mapTransportError(error, requestTimeout);
|
|
@@ -1584,26 +1667,18 @@ var HttpIlpClient = class {
|
|
|
1584
1667
|
* Map a `200 OK` body (OER FULFILL/REJECT) to an IlpSendResult; map a non-2xx
|
|
1585
1668
|
* to a transport error. Per the wire contract, ILP-level rejects arrive as a
|
|
1586
1669
|
* 200 + REJECT body — only HTTP non-2xx means a transport-layer failure.
|
|
1670
|
+
*
|
|
1671
|
+
* When `sentCondition` is non-zero the FULFILL preimage is verified against
|
|
1672
|
+
* it; a mismatch yields `accepted: false` (shared `mapIlpResponse` logic,
|
|
1673
|
+
* identical to the BTP path).
|
|
1587
1674
|
*/
|
|
1588
|
-
async mapResponse(response) {
|
|
1675
|
+
async mapResponse(response, sentCondition) {
|
|
1589
1676
|
if (response.ok) {
|
|
1590
1677
|
const buf = new Uint8Array(await response.arrayBuffer());
|
|
1591
1678
|
if (buf.length === 0) {
|
|
1592
1679
|
throw new ConnectorError("Empty 200 body from /ilp (expected OER ILP response)");
|
|
1593
1680
|
}
|
|
1594
|
-
|
|
1595
|
-
if (ilp.type === ILPPacketType.FULFILL) {
|
|
1596
|
-
return {
|
|
1597
|
-
accepted: true,
|
|
1598
|
-
data: ilp.data.length > 0 ? toBase64(ilp.data) : void 0
|
|
1599
|
-
};
|
|
1600
|
-
}
|
|
1601
|
-
return {
|
|
1602
|
-
accepted: false,
|
|
1603
|
-
code: ilp.code,
|
|
1604
|
-
message: ilp.message,
|
|
1605
|
-
data: ilp.data.length > 0 ? toBase64(ilp.data) : void 0
|
|
1606
|
-
};
|
|
1681
|
+
return mapIlpResponse(deserializeIlpPacket(buf), sentCondition);
|
|
1607
1682
|
}
|
|
1608
1683
|
const body = await response.text().catch(() => "");
|
|
1609
1684
|
const detail = body ? `: ${body}` : "";
|
|
@@ -1663,17 +1738,17 @@ function readDiscoveredIlpPeer(peer) {
|
|
|
1663
1738
|
}
|
|
1664
1739
|
function selectIlpTransport(peer, options = {}) {
|
|
1665
1740
|
const needsDuplex = options.needsDuplex ?? false;
|
|
1666
|
-
const
|
|
1741
|
+
const http4 = peer.httpEndpoint?.trim() || void 0;
|
|
1667
1742
|
const btp = peer.btpEndpoint?.trim() || void 0;
|
|
1668
1743
|
const canUpgrade = peer.supportsUpgrade === true;
|
|
1669
1744
|
if (needsDuplex) {
|
|
1670
1745
|
if (btp) return { kind: "btp", btpEndpoint: btp };
|
|
1671
|
-
if (
|
|
1746
|
+
if (http4 && canUpgrade) return { kind: "http-upgradable", httpEndpoint: http4 };
|
|
1672
1747
|
throw new Error(
|
|
1673
1748
|
"Duplex transport required but peer exposes neither a btpEndpoint nor an upgradable httpEndpoint"
|
|
1674
1749
|
);
|
|
1675
1750
|
}
|
|
1676
|
-
if (
|
|
1751
|
+
if (http4) return { kind: "http", httpEndpoint: http4, canUpgrade };
|
|
1677
1752
|
if (btp) return { kind: "btp", btpEndpoint: btp };
|
|
1678
1753
|
throw new Error("Peer exposes neither an httpEndpoint nor a btpEndpoint");
|
|
1679
1754
|
}
|
|
@@ -1692,7 +1767,7 @@ import { base58Encode as base58Encode2 } from "@toon-protocol/core";
|
|
|
1692
1767
|
|
|
1693
1768
|
// src/channel/solana-payment-channel.ts
|
|
1694
1769
|
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
1695
|
-
import { sha256 } from "@noble/hashes/sha2.js";
|
|
1770
|
+
import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
|
|
1696
1771
|
import { base58Encode, base58Decode } from "@toon-protocol/core";
|
|
1697
1772
|
var TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
1698
1773
|
var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
|
|
@@ -1781,7 +1856,7 @@ function findProgramAddress(seeds, programId) {
|
|
|
1781
1856
|
input.set(programId, offset);
|
|
1782
1857
|
offset += programId.length;
|
|
1783
1858
|
input.set(PDA_MARKER, offset);
|
|
1784
|
-
const hash =
|
|
1859
|
+
const hash = sha2562(input);
|
|
1785
1860
|
if (!isOnCurve(hash)) return { pda: hash, bump };
|
|
1786
1861
|
}
|
|
1787
1862
|
throw new Error("Could not find a viable PDA bump seed");
|
|
@@ -1894,13 +1969,13 @@ async function buildAndSendTransaction(rpcUrl, feePayer, instructions, additiona
|
|
|
1894
1969
|
isWritable: true
|
|
1895
1970
|
});
|
|
1896
1971
|
for (const ix of instructions) {
|
|
1897
|
-
for (const
|
|
1898
|
-
const existing = accountMap.get(
|
|
1972
|
+
for (const key2 of ix.keys) {
|
|
1973
|
+
const existing = accountMap.get(key2.pubkey);
|
|
1899
1974
|
if (existing) {
|
|
1900
|
-
existing.isSigner = existing.isSigner ||
|
|
1901
|
-
existing.isWritable = existing.isWritable ||
|
|
1975
|
+
existing.isSigner = existing.isSigner || key2.isSigner;
|
|
1976
|
+
existing.isWritable = existing.isWritable || key2.isWritable;
|
|
1902
1977
|
} else {
|
|
1903
|
-
accountMap.set(
|
|
1978
|
+
accountMap.set(key2.pubkey, { ...key2 });
|
|
1904
1979
|
}
|
|
1905
1980
|
}
|
|
1906
1981
|
if (!accountMap.has(ix.programId)) {
|
|
@@ -3234,7 +3309,7 @@ var SolanaSigner = class {
|
|
|
3234
3309
|
|
|
3235
3310
|
// src/signing/mina-signer.ts
|
|
3236
3311
|
import { hexToMinaBase58PrivateKey as hexToMinaBase58PrivateKey3 } from "@toon-protocol/core";
|
|
3237
|
-
import { sha256 as
|
|
3312
|
+
import { sha256 as sha2563 } from "@noble/hashes/sha2.js";
|
|
3238
3313
|
import { bytesToHex } from "@noble/hashes/utils.js";
|
|
3239
3314
|
|
|
3240
3315
|
// src/channel/mina-payment-channel.ts
|
|
@@ -3365,7 +3440,7 @@ var DEFAULT_MINA_TOKEN_ID = "MINA";
|
|
|
3365
3440
|
var MINA_CLAIM_NETWORK = "devnet";
|
|
3366
3441
|
function deriveMinaSalt(zkAppAddress, nonce) {
|
|
3367
3442
|
const digestHex = bytesToHex(
|
|
3368
|
-
|
|
3443
|
+
sha2563(new TextEncoder().encode(`mina-pc-salt:${zkAppAddress}:${nonce}`))
|
|
3369
3444
|
);
|
|
3370
3445
|
const salt = BigInt("0x" + digestHex.slice(0, 60));
|
|
3371
3446
|
return salt === 0n ? 1n : salt;
|
|
@@ -4196,8 +4271,8 @@ async function requestBlobStorage(client, secretKey, params) {
|
|
|
4196
4271
|
};
|
|
4197
4272
|
}
|
|
4198
4273
|
function extractArweaveTxId(base64Data) {
|
|
4199
|
-
const
|
|
4200
|
-
if (!
|
|
4274
|
+
const http4 = parseFulfillHttp(base64Data);
|
|
4275
|
+
if (!http4.isHttp) {
|
|
4201
4276
|
const legacy = decodeUtf8(fromBase64(base64Data));
|
|
4202
4277
|
if (!ARWEAVE_TX_ID_REGEX.test(legacy)) {
|
|
4203
4278
|
throw new Error(
|
|
@@ -4206,18 +4281,18 @@ function extractArweaveTxId(base64Data) {
|
|
|
4206
4281
|
}
|
|
4207
4282
|
return legacy;
|
|
4208
4283
|
}
|
|
4209
|
-
if (
|
|
4210
|
-
const detail =
|
|
4284
|
+
if (http4.status < 200 || http4.status >= 300) {
|
|
4285
|
+
const detail = http4.body ? ` - ${http4.body}` : "";
|
|
4211
4286
|
throw new Error(
|
|
4212
|
-
`Blob upload failed: DVM returned HTTP ${
|
|
4287
|
+
`Blob upload failed: DVM returned HTTP ${http4.status} ${http4.statusText}`.trimEnd() + detail
|
|
4213
4288
|
);
|
|
4214
4289
|
}
|
|
4215
4290
|
let parsed;
|
|
4216
4291
|
try {
|
|
4217
|
-
parsed = JSON.parse(
|
|
4292
|
+
parsed = JSON.parse(http4.body);
|
|
4218
4293
|
} catch {
|
|
4219
4294
|
throw new Error(
|
|
4220
|
-
`Blob upload response body was not valid JSON: "${
|
|
4295
|
+
`Blob upload response body was not valid JSON: "${http4.body}"`
|
|
4221
4296
|
);
|
|
4222
4297
|
}
|
|
4223
4298
|
const body = parsed;
|
|
@@ -4235,7 +4310,7 @@ function extractArweaveTxId(base64Data) {
|
|
|
4235
4310
|
}
|
|
4236
4311
|
}
|
|
4237
4312
|
throw new Error(
|
|
4238
|
-
`Blob upload response did not contain a valid Arweave tx ID: "${
|
|
4313
|
+
`Blob upload response did not contain a valid Arweave tx ID: "${http4.body}"`
|
|
4239
4314
|
);
|
|
4240
4315
|
}
|
|
4241
4316
|
|
|
@@ -4490,6 +4565,172 @@ function parseHttpResponse(bytes) {
|
|
|
4490
4565
|
);
|
|
4491
4566
|
}
|
|
4492
4567
|
|
|
4568
|
+
// src/swap/settle-received-claims.ts
|
|
4569
|
+
import {
|
|
4570
|
+
buildSettlementTx,
|
|
4571
|
+
SettlementTxError
|
|
4572
|
+
} from "@toon-protocol/sdk";
|
|
4573
|
+
import {
|
|
4574
|
+
createPublicClient as createPublicClient3,
|
|
4575
|
+
defineChain as defineChain3,
|
|
4576
|
+
fromRlp,
|
|
4577
|
+
http as http3
|
|
4578
|
+
} from "viem";
|
|
4579
|
+
function entryToAccumulatedClaim(entry) {
|
|
4580
|
+
return {
|
|
4581
|
+
packetIndex: 0,
|
|
4582
|
+
sourceAmount: 0n,
|
|
4583
|
+
targetAmount: entry.cumulativeAmount,
|
|
4584
|
+
claimBytes: entry.claimBytes,
|
|
4585
|
+
swapEphemeralPubkey: "",
|
|
4586
|
+
...entry.claimId !== void 0 ? { claimId: entry.claimId } : {},
|
|
4587
|
+
pair: entry.pair,
|
|
4588
|
+
receivedAt: entry.receivedAt,
|
|
4589
|
+
channelId: entry.channelId,
|
|
4590
|
+
nonce: entry.nonce.toString(),
|
|
4591
|
+
cumulativeAmount: entry.cumulativeAmount.toString(),
|
|
4592
|
+
recipient: entry.recipient,
|
|
4593
|
+
swapSignerAddress: entry.swapSignerAddress
|
|
4594
|
+
};
|
|
4595
|
+
}
|
|
4596
|
+
function parseEvmChainId2(chain) {
|
|
4597
|
+
const parts = chain.split(":");
|
|
4598
|
+
const raw = parts.length >= 3 ? parts[2] : parts[1];
|
|
4599
|
+
const chainId = Number.parseInt(raw ?? "", 10);
|
|
4600
|
+
return Number.isInteger(chainId) && chainId > 0 ? chainId : void 0;
|
|
4601
|
+
}
|
|
4602
|
+
function buildSwapSettlements(params) {
|
|
4603
|
+
return params.entries.map((entry) => {
|
|
4604
|
+
const base = { chain: entry.chain, channelId: entry.channelId };
|
|
4605
|
+
const signer = { address: entry.swapSignerAddress };
|
|
4606
|
+
const contract = params.tokenNetworks?.[entry.chain];
|
|
4607
|
+
if (entry.chain.startsWith("evm")) {
|
|
4608
|
+
const chainId = parseEvmChainId2(entry.chain);
|
|
4609
|
+
if (!contract || chainId === void 0) {
|
|
4610
|
+
return {
|
|
4611
|
+
...base,
|
|
4612
|
+
error: {
|
|
4613
|
+
code: "MISSING_CHAIN_CONFIG",
|
|
4614
|
+
message: `EVM settlement for ${entry.chain} needs tokenNetworks["${entry.chain}"] (TokenNetwork contract) and a numeric chain id in the chain key.`
|
|
4615
|
+
}
|
|
4616
|
+
};
|
|
4617
|
+
}
|
|
4618
|
+
signer.contractAddress = contract;
|
|
4619
|
+
signer.chainId = chainId;
|
|
4620
|
+
} else if (entry.chain.startsWith("solana")) {
|
|
4621
|
+
if (!contract) {
|
|
4622
|
+
return {
|
|
4623
|
+
...base,
|
|
4624
|
+
error: {
|
|
4625
|
+
code: "MISSING_CHAIN_CONFIG",
|
|
4626
|
+
message: `Solana settlement for ${entry.chain} needs tokenNetworks["${entry.chain}"] (programId).`
|
|
4627
|
+
}
|
|
4628
|
+
};
|
|
4629
|
+
}
|
|
4630
|
+
signer.programId = contract;
|
|
4631
|
+
}
|
|
4632
|
+
try {
|
|
4633
|
+
const result = buildSettlementTx({
|
|
4634
|
+
claims: [entryToAccumulatedClaim(entry)],
|
|
4635
|
+
signers: { [entry.chain]: signer },
|
|
4636
|
+
recipients: { [entry.chain]: entry.recipient },
|
|
4637
|
+
...params.minaSignerClient ? { minaSignerClient: params.minaSignerClient } : {}
|
|
4638
|
+
});
|
|
4639
|
+
const firstRejected = result.rejected[0];
|
|
4640
|
+
if (firstRejected) {
|
|
4641
|
+
return {
|
|
4642
|
+
...base,
|
|
4643
|
+
error: {
|
|
4644
|
+
code: firstRejected.reason,
|
|
4645
|
+
message: firstRejected.details ?? `stored claim for ${entry.chain}/${entry.channelId} failed settlement re-verification`
|
|
4646
|
+
}
|
|
4647
|
+
};
|
|
4648
|
+
}
|
|
4649
|
+
const bundle = result.bundles[0];
|
|
4650
|
+
if (!bundle) {
|
|
4651
|
+
return {
|
|
4652
|
+
...base,
|
|
4653
|
+
error: {
|
|
4654
|
+
code: "NO_BUNDLE",
|
|
4655
|
+
message: `buildSettlementTx produced no bundle for ${entry.chain}/${entry.channelId}`
|
|
4656
|
+
}
|
|
4657
|
+
};
|
|
4658
|
+
}
|
|
4659
|
+
return { ...base, bundle };
|
|
4660
|
+
} catch (err) {
|
|
4661
|
+
const code = err instanceof SettlementTxError ? err.code : "BUILD_FAILED";
|
|
4662
|
+
return {
|
|
4663
|
+
...base,
|
|
4664
|
+
error: {
|
|
4665
|
+
code,
|
|
4666
|
+
message: err instanceof Error ? err.message : String(err)
|
|
4667
|
+
}
|
|
4668
|
+
};
|
|
4669
|
+
}
|
|
4670
|
+
});
|
|
4671
|
+
}
|
|
4672
|
+
function decodeEvmSettlementTx(bundle) {
|
|
4673
|
+
const fields = fromRlp(bundle.unsignedTxBytes, "hex");
|
|
4674
|
+
if (!Array.isArray(fields) || fields.length !== 9) {
|
|
4675
|
+
throw new Error(
|
|
4676
|
+
`settlement bundle RLP is not a 9-field unsigned EVM tx (got ${Array.isArray(fields) ? fields.length : typeof fields})`
|
|
4677
|
+
);
|
|
4678
|
+
}
|
|
4679
|
+
const to = fields[3];
|
|
4680
|
+
const data = fields[5];
|
|
4681
|
+
const chainIdHex = fields[6];
|
|
4682
|
+
const chainId = Number.parseInt(chainIdHex === "0x" ? "0x0" : chainIdHex, 16);
|
|
4683
|
+
if (typeof to !== "string" || to.length !== 42) {
|
|
4684
|
+
throw new Error(`settlement tx "to" is not a 20-byte address: ${String(to)}`);
|
|
4685
|
+
}
|
|
4686
|
+
return { to, data: data === "0x" ? "0x" : data, chainId };
|
|
4687
|
+
}
|
|
4688
|
+
async function submitEvmSettlement(bundle, params) {
|
|
4689
|
+
if (bundle.chainKind !== "evm") {
|
|
4690
|
+
throw new Error(
|
|
4691
|
+
`submitEvmSettlement only submits evm bundles (got ${bundle.chainKind} for ${bundle.chain})`
|
|
4692
|
+
);
|
|
4693
|
+
}
|
|
4694
|
+
const { to, data, chainId } = decodeEvmSettlementTx(bundle);
|
|
4695
|
+
const chain = defineChain3({
|
|
4696
|
+
id: chainId,
|
|
4697
|
+
name: bundle.chain,
|
|
4698
|
+
nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
|
|
4699
|
+
rpcUrls: { default: { http: [params.rpcUrl] } }
|
|
4700
|
+
});
|
|
4701
|
+
const publicClient = createPublicClient3({ chain, transport: http3(params.rpcUrl) });
|
|
4702
|
+
const [nonce, gasPrice, gas] = await Promise.all([
|
|
4703
|
+
publicClient.getTransactionCount({
|
|
4704
|
+
address: params.account.address,
|
|
4705
|
+
blockTag: "pending"
|
|
4706
|
+
}),
|
|
4707
|
+
publicClient.getGasPrice(),
|
|
4708
|
+
publicClient.estimateGas({ account: params.account.address, to, data })
|
|
4709
|
+
]);
|
|
4710
|
+
const signed = await params.account.signTransaction({
|
|
4711
|
+
type: "legacy",
|
|
4712
|
+
chainId,
|
|
4713
|
+
nonce,
|
|
4714
|
+
gasPrice,
|
|
4715
|
+
gas,
|
|
4716
|
+
to,
|
|
4717
|
+
value: 0n,
|
|
4718
|
+
data
|
|
4719
|
+
});
|
|
4720
|
+
const txHash = await publicClient.sendRawTransaction({
|
|
4721
|
+
serializedTransaction: signed
|
|
4722
|
+
});
|
|
4723
|
+
try {
|
|
4724
|
+
const receipt = await publicClient.waitForTransactionReceipt({
|
|
4725
|
+
hash: txHash,
|
|
4726
|
+
timeout: params.timeoutMs ?? 6e4
|
|
4727
|
+
});
|
|
4728
|
+
return { txHash, status: receipt.status };
|
|
4729
|
+
} catch {
|
|
4730
|
+
return { txHash };
|
|
4731
|
+
}
|
|
4732
|
+
}
|
|
4733
|
+
|
|
4493
4734
|
// src/ToonClient.ts
|
|
4494
4735
|
var ToonClient = class {
|
|
4495
4736
|
config;
|
|
@@ -4897,6 +5138,14 @@ var ToonClient = class {
|
|
|
4897
5138
|
* matching `destination`,
|
|
4898
5139
|
* (c) neither -> throw MISSING_CLAIM.
|
|
4899
5140
|
*
|
|
5141
|
+
* A caller may supply a sender-chosen 32-byte `executionCondition`
|
|
5142
|
+
* (`C = sha256(P)`, one fresh preimage per packet — toon-client#350,
|
|
5143
|
+
* rolling-swap spec §3 R1/R2) and an explicit `expiresAt` (R7). Both are
|
|
5144
|
+
* set on the wire by either transport (HTTP `POST /ilp` and BTP), and on
|
|
5145
|
+
* FULFILL the transport verifies `sha256(fulfillment) == condition` —
|
|
5146
|
+
* a mismatch comes back as `accepted: false` (code F99), never a silent
|
|
5147
|
+
* accept. Omitting them keeps today's zero-condition legacy packet.
|
|
5148
|
+
*
|
|
4900
5149
|
* @throws {ToonClientError} INVALID_STATE / NO_ILP_TRANSPORT / MISSING_CLAIM
|
|
4901
5150
|
*/
|
|
4902
5151
|
async sendSwapPacket(params) {
|
|
@@ -4917,7 +5166,9 @@ var ToonClient = class {
|
|
|
4917
5166
|
destination: params.destination,
|
|
4918
5167
|
amount: String(params.amount),
|
|
4919
5168
|
data: toBase64(params.toonData),
|
|
4920
|
-
timeout: params.timeout ?? 3e4
|
|
5169
|
+
timeout: params.timeout ?? 3e4,
|
|
5170
|
+
...params.executionCondition ? { executionCondition: params.executionCondition } : {},
|
|
5171
|
+
...params.expiresAt ? { expiresAt: params.expiresAt } : {}
|
|
4921
5172
|
},
|
|
4922
5173
|
claimMessage
|
|
4923
5174
|
);
|
|
@@ -5196,6 +5447,38 @@ var ToonClient = class {
|
|
|
5196
5447
|
this.channelManager.setChannelSettled(channelId, nowSec);
|
|
5197
5448
|
return { channelId, ...r.txHash ? { txHash: r.txHash } : {} };
|
|
5198
5449
|
}
|
|
5450
|
+
/**
|
|
5451
|
+
* Submit a receive-side swap settlement bundle on-chain (toon-client#352).
|
|
5452
|
+
* The bundle comes from the sdk's `buildSettlementTx` over persisted,
|
|
5453
|
+
* verified chain-B claims (`buildSwapSettlements`); this signs it with the
|
|
5454
|
+
* client's EVM account (the claim recipient) and broadcasts it.
|
|
5455
|
+
*
|
|
5456
|
+
* Env-gated seam: EVM only, and only when `chainRpcUrls[bundle.chain]` is
|
|
5457
|
+
* configured — otherwise this throws a clear config error and callers
|
|
5458
|
+
* surface a built-not-submitted result. Solana submission and the Mina
|
|
5459
|
+
* receive-side co-sign path are explicit follow-ups (see
|
|
5460
|
+
* swap/settle-received-claims.ts module doc).
|
|
5461
|
+
*/
|
|
5462
|
+
async settleSwapBundle(bundle) {
|
|
5463
|
+
if (bundle.chainKind !== "evm") {
|
|
5464
|
+
throw new Error(
|
|
5465
|
+
`Swap settlement submission for ${bundle.chainKind} (${bundle.chain}) is not wired yet \u2014 EVM only today.`
|
|
5466
|
+
);
|
|
5467
|
+
}
|
|
5468
|
+
const rpcUrl = this.config.chainRpcUrls?.[bundle.chain];
|
|
5469
|
+
if (!rpcUrl) {
|
|
5470
|
+
throw new Error(
|
|
5471
|
+
`No RPC URL configured for chain "${bundle.chain}" \u2014 add it to chainRpcUrls to enable swap settlement submission.`
|
|
5472
|
+
);
|
|
5473
|
+
}
|
|
5474
|
+
if (!this.evmSigner) {
|
|
5475
|
+
throw new Error("EVM signer not configured (no evmPrivateKey/mnemonic).");
|
|
5476
|
+
}
|
|
5477
|
+
return submitEvmSettlement(bundle, {
|
|
5478
|
+
rpcUrl,
|
|
5479
|
+
account: this.evmSigner.account
|
|
5480
|
+
});
|
|
5481
|
+
}
|
|
5199
5482
|
/** Where a tracked channel sits in the withdraw journey. */
|
|
5200
5483
|
getChannelCloseState(channelId) {
|
|
5201
5484
|
if (!this.channelManager) throw new Error("ChannelManager not initialized");
|
|
@@ -5794,6 +6077,238 @@ var HttpConnectorAdmin = class {
|
|
|
5794
6077
|
}
|
|
5795
6078
|
};
|
|
5796
6079
|
|
|
6080
|
+
// src/channel/ReceivedClaimStore.ts
|
|
6081
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync } from "fs";
|
|
6082
|
+
import { dirname } from "path";
|
|
6083
|
+
function key(chain, channelId) {
|
|
6084
|
+
return `${chain}|${channelId}`;
|
|
6085
|
+
}
|
|
6086
|
+
var JsonFileReceivedClaimStore = class {
|
|
6087
|
+
filePath;
|
|
6088
|
+
constructor(filePath) {
|
|
6089
|
+
this.filePath = filePath;
|
|
6090
|
+
}
|
|
6091
|
+
save(entry) {
|
|
6092
|
+
const data = this.readFile();
|
|
6093
|
+
data[key(entry.chain, entry.channelId)] = {
|
|
6094
|
+
chain: entry.chain,
|
|
6095
|
+
channelId: entry.channelId,
|
|
6096
|
+
nonce: entry.nonce.toString(),
|
|
6097
|
+
cumulativeAmount: entry.cumulativeAmount.toString(),
|
|
6098
|
+
recipient: entry.recipient,
|
|
6099
|
+
swapSignerAddress: entry.swapSignerAddress,
|
|
6100
|
+
claimBytes: Buffer.from(entry.claimBytes).toString("base64"),
|
|
6101
|
+
...entry.claimId !== void 0 ? { claimId: entry.claimId } : {},
|
|
6102
|
+
pair: entry.pair,
|
|
6103
|
+
receivedAt: entry.receivedAt,
|
|
6104
|
+
updatedAt: entry.updatedAt,
|
|
6105
|
+
...entry.settledAt !== void 0 ? { settledAt: entry.settledAt } : {},
|
|
6106
|
+
...entry.settledNonce !== void 0 ? { settledNonce: entry.settledNonce.toString() } : {},
|
|
6107
|
+
...entry.settleTxHash !== void 0 ? { settleTxHash: entry.settleTxHash } : {}
|
|
6108
|
+
};
|
|
6109
|
+
this.writeFile(data);
|
|
6110
|
+
}
|
|
6111
|
+
load(chain, channelId) {
|
|
6112
|
+
const entry = this.readFile()[key(chain, channelId)];
|
|
6113
|
+
return entry ? fromJson(entry) : void 0;
|
|
6114
|
+
}
|
|
6115
|
+
list() {
|
|
6116
|
+
return Object.values(this.readFile()).map(fromJson);
|
|
6117
|
+
}
|
|
6118
|
+
delete(chain, channelId) {
|
|
6119
|
+
const data = this.readFile();
|
|
6120
|
+
const { [key(chain, channelId)]: _, ...rest } = data;
|
|
6121
|
+
this.writeFile(rest);
|
|
6122
|
+
}
|
|
6123
|
+
readFile() {
|
|
6124
|
+
if (!existsSync2(this.filePath)) {
|
|
6125
|
+
return {};
|
|
6126
|
+
}
|
|
6127
|
+
const raw = readFileSync2(this.filePath, "utf-8");
|
|
6128
|
+
return JSON.parse(raw);
|
|
6129
|
+
}
|
|
6130
|
+
writeFile(data) {
|
|
6131
|
+
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
6132
|
+
writeFileSync2(this.filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
6133
|
+
}
|
|
6134
|
+
};
|
|
6135
|
+
var InMemoryReceivedClaimStore = class {
|
|
6136
|
+
entries = /* @__PURE__ */ new Map();
|
|
6137
|
+
save(entry) {
|
|
6138
|
+
this.entries.set(key(entry.chain, entry.channelId), { ...entry });
|
|
6139
|
+
}
|
|
6140
|
+
load(chain, channelId) {
|
|
6141
|
+
const entry = this.entries.get(key(chain, channelId));
|
|
6142
|
+
return entry ? { ...entry } : void 0;
|
|
6143
|
+
}
|
|
6144
|
+
list() {
|
|
6145
|
+
return [...this.entries.values()].map((e) => ({ ...e }));
|
|
6146
|
+
}
|
|
6147
|
+
delete(chain, channelId) {
|
|
6148
|
+
this.entries.delete(key(chain, channelId));
|
|
6149
|
+
}
|
|
6150
|
+
};
|
|
6151
|
+
function fromJson(entry) {
|
|
6152
|
+
return {
|
|
6153
|
+
chain: entry.chain,
|
|
6154
|
+
channelId: entry.channelId,
|
|
6155
|
+
nonce: BigInt(entry.nonce),
|
|
6156
|
+
cumulativeAmount: BigInt(entry.cumulativeAmount),
|
|
6157
|
+
recipient: entry.recipient,
|
|
6158
|
+
swapSignerAddress: entry.swapSignerAddress,
|
|
6159
|
+
claimBytes: new Uint8Array(Buffer.from(entry.claimBytes, "base64")),
|
|
6160
|
+
...entry.claimId !== void 0 ? { claimId: entry.claimId } : {},
|
|
6161
|
+
pair: entry.pair,
|
|
6162
|
+
receivedAt: entry.receivedAt,
|
|
6163
|
+
updatedAt: entry.updatedAt,
|
|
6164
|
+
...entry.settledAt !== void 0 ? { settledAt: entry.settledAt } : {},
|
|
6165
|
+
...entry.settledNonce !== void 0 ? { settledNonce: BigInt(entry.settledNonce) } : {},
|
|
6166
|
+
...entry.settleTxHash !== void 0 ? { settleTxHash: entry.settleTxHash } : {}
|
|
6167
|
+
};
|
|
6168
|
+
}
|
|
6169
|
+
|
|
6170
|
+
// src/swap/received-claims.ts
|
|
6171
|
+
import {
|
|
6172
|
+
verifyAccumulatedClaim
|
|
6173
|
+
} from "@toon-protocol/sdk";
|
|
6174
|
+
function hasSettlementMetadata(claim) {
|
|
6175
|
+
return claim.channelId !== void 0 && claim.nonce !== void 0 && claim.cumulativeAmount !== void 0 && claim.recipient !== void 0 && claim.swapSignerAddress !== void 0;
|
|
6176
|
+
}
|
|
6177
|
+
function sameAddress(chain, a, b) {
|
|
6178
|
+
return chain.startsWith("evm") ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
6179
|
+
}
|
|
6180
|
+
function signatureRejectionCode(reason) {
|
|
6181
|
+
if (reason.startsWith("SIGNER_MISMATCH")) return "SIGNER_MISMATCH";
|
|
6182
|
+
if (reason.startsWith("MINA_VERIFICATION_UNSUPPORTED"))
|
|
6183
|
+
return "MINA_VERIFICATION_UNSUPPORTED";
|
|
6184
|
+
if (reason.startsWith("UNSUPPORTED_CHAIN")) return "UNSUPPORTED_CHAIN";
|
|
6185
|
+
return "SIGNATURE_INVALID";
|
|
6186
|
+
}
|
|
6187
|
+
function ingestReceivedClaims(params) {
|
|
6188
|
+
const now = params.now ?? Date.now;
|
|
6189
|
+
const verified = [];
|
|
6190
|
+
const rejected = [];
|
|
6191
|
+
const legacy = [];
|
|
6192
|
+
let valueReceived = 0n;
|
|
6193
|
+
const watermarks = /* @__PURE__ */ new Map();
|
|
6194
|
+
const reject = (claim, code, message) => {
|
|
6195
|
+
rejected.push({ claim, code, message });
|
|
6196
|
+
};
|
|
6197
|
+
for (const claim of params.claims) {
|
|
6198
|
+
if (!hasSettlementMetadata(claim)) {
|
|
6199
|
+
legacy.push(claim);
|
|
6200
|
+
continue;
|
|
6201
|
+
}
|
|
6202
|
+
const chain = claim.pair.to.chain;
|
|
6203
|
+
if (chain !== params.expectedChain) {
|
|
6204
|
+
reject(
|
|
6205
|
+
claim,
|
|
6206
|
+
"CHAIN_MISMATCH",
|
|
6207
|
+
`claim settles on "${chain}", session expected "${params.expectedChain}"`
|
|
6208
|
+
);
|
|
6209
|
+
continue;
|
|
6210
|
+
}
|
|
6211
|
+
if (!sameAddress(chain, claim.recipient, params.chainRecipient)) {
|
|
6212
|
+
reject(
|
|
6213
|
+
claim,
|
|
6214
|
+
"RECIPIENT_MISMATCH",
|
|
6215
|
+
`claim recipient "${claim.recipient}" is not the session chainRecipient "${params.chainRecipient}"`
|
|
6216
|
+
);
|
|
6217
|
+
continue;
|
|
6218
|
+
}
|
|
6219
|
+
if (params.expectedSignerAddress !== void 0 && !sameAddress(chain, claim.swapSignerAddress, params.expectedSignerAddress)) {
|
|
6220
|
+
reject(
|
|
6221
|
+
claim,
|
|
6222
|
+
"SWAP_SIGNER_MISMATCH",
|
|
6223
|
+
`claim swapSignerAddress "${claim.swapSignerAddress}" does not match the maker's advertised signer "${params.expectedSignerAddress}"`
|
|
6224
|
+
);
|
|
6225
|
+
continue;
|
|
6226
|
+
}
|
|
6227
|
+
const expectedSigner = params.expectedSignerAddress ?? claim.swapSignerAddress;
|
|
6228
|
+
const sig = verifyAccumulatedClaim(
|
|
6229
|
+
claim,
|
|
6230
|
+
{ address: expectedSigner },
|
|
6231
|
+
params.minaSignerClient
|
|
6232
|
+
);
|
|
6233
|
+
if (!sig.valid) {
|
|
6234
|
+
reject(claim, signatureRejectionCode(sig.reason), sig.reason);
|
|
6235
|
+
continue;
|
|
6236
|
+
}
|
|
6237
|
+
let nonce;
|
|
6238
|
+
let cumulativeAmount;
|
|
6239
|
+
try {
|
|
6240
|
+
nonce = BigInt(claim.nonce);
|
|
6241
|
+
cumulativeAmount = BigInt(claim.cumulativeAmount);
|
|
6242
|
+
} catch {
|
|
6243
|
+
reject(
|
|
6244
|
+
claim,
|
|
6245
|
+
"MALFORMED_METADATA",
|
|
6246
|
+
`nonce "${claim.nonce}" / cumulativeAmount "${claim.cumulativeAmount}" are not decimal integers`
|
|
6247
|
+
);
|
|
6248
|
+
continue;
|
|
6249
|
+
}
|
|
6250
|
+
const wmKey = `${chain}|${claim.channelId}`;
|
|
6251
|
+
const watermark = watermarks.get(wmKey) ?? params.store.load(chain, claim.channelId);
|
|
6252
|
+
if (watermark) {
|
|
6253
|
+
if (!sameAddress(chain, watermark.swapSignerAddress, expectedSigner)) {
|
|
6254
|
+
reject(
|
|
6255
|
+
claim,
|
|
6256
|
+
"SWAP_SIGNER_MISMATCH",
|
|
6257
|
+
`channel ${claim.channelId} watermark was signed by "${watermark.swapSignerAddress}"; a claim signed by "${expectedSigner}" may not rotate it`
|
|
6258
|
+
);
|
|
6259
|
+
continue;
|
|
6260
|
+
}
|
|
6261
|
+
if (nonce <= watermark.nonce) {
|
|
6262
|
+
reject(
|
|
6263
|
+
claim,
|
|
6264
|
+
"NON_MONOTONIC_NONCE",
|
|
6265
|
+
`nonce ${nonce} does not advance the persisted watermark nonce ${watermark.nonce} for channel ${claim.channelId}`
|
|
6266
|
+
);
|
|
6267
|
+
continue;
|
|
6268
|
+
}
|
|
6269
|
+
if (cumulativeAmount <= watermark.cumulativeAmount) {
|
|
6270
|
+
reject(
|
|
6271
|
+
claim,
|
|
6272
|
+
"NON_MONOTONIC_CUMULATIVE",
|
|
6273
|
+
`cumulativeAmount ${cumulativeAmount} does not advance the persisted watermark ${watermark.cumulativeAmount} for channel ${claim.channelId}`
|
|
6274
|
+
);
|
|
6275
|
+
continue;
|
|
6276
|
+
}
|
|
6277
|
+
}
|
|
6278
|
+
const advance = cumulativeAmount - (watermark?.cumulativeAmount ?? 0n);
|
|
6279
|
+
if (advance < claim.targetAmount) {
|
|
6280
|
+
reject(
|
|
6281
|
+
claim,
|
|
6282
|
+
"CUMULATIVE_SHORTFALL",
|
|
6283
|
+
`cumulative advance ${advance} is less than the packet's targetAmount ${claim.targetAmount} for channel ${claim.channelId}`
|
|
6284
|
+
);
|
|
6285
|
+
continue;
|
|
6286
|
+
}
|
|
6287
|
+
const entry = {
|
|
6288
|
+
chain,
|
|
6289
|
+
channelId: claim.channelId,
|
|
6290
|
+
nonce,
|
|
6291
|
+
cumulativeAmount,
|
|
6292
|
+
recipient: claim.recipient,
|
|
6293
|
+
swapSignerAddress: expectedSigner,
|
|
6294
|
+
claimBytes: claim.claimBytes,
|
|
6295
|
+
...claim.claimId !== void 0 ? { claimId: claim.claimId } : {},
|
|
6296
|
+
pair: claim.pair,
|
|
6297
|
+
receivedAt: claim.receivedAt,
|
|
6298
|
+
updatedAt: now(),
|
|
6299
|
+
// Settlement bookkeeping survives watermark advances.
|
|
6300
|
+
...watermark?.settledAt !== void 0 ? { settledAt: watermark.settledAt } : {},
|
|
6301
|
+
...watermark?.settledNonce !== void 0 ? { settledNonce: watermark.settledNonce } : {},
|
|
6302
|
+
...watermark?.settleTxHash !== void 0 ? { settleTxHash: watermark.settleTxHash } : {}
|
|
6303
|
+
};
|
|
6304
|
+
params.store.save(entry);
|
|
6305
|
+
watermarks.set(wmKey, entry);
|
|
6306
|
+
verified.push({ claim, watermarkAdvance: advance });
|
|
6307
|
+
valueReceived += advance;
|
|
6308
|
+
}
|
|
6309
|
+
return { verified, rejected, legacy, valueReceived };
|
|
6310
|
+
}
|
|
6311
|
+
|
|
5797
6312
|
// src/faucet.ts
|
|
5798
6313
|
function defaultFaucetTimeout(chain) {
|
|
5799
6314
|
return chain === "mina" ? 12e4 : 3e4;
|
|
@@ -6771,9 +7286,9 @@ import {
|
|
|
6771
7286
|
scryptSync,
|
|
6772
7287
|
createCipheriv,
|
|
6773
7288
|
createDecipheriv,
|
|
6774
|
-
randomBytes
|
|
7289
|
+
randomBytes as randomBytes2
|
|
6775
7290
|
} from "crypto";
|
|
6776
|
-
import { writeFileSync as
|
|
7291
|
+
import { writeFileSync as writeFileSync3, readFileSync as readFileSync3 } from "fs";
|
|
6777
7292
|
var SCRYPT_N = 2 ** 17;
|
|
6778
7293
|
var SCRYPT_R = 8;
|
|
6779
7294
|
var SCRYPT_P = 1;
|
|
@@ -6798,16 +7313,16 @@ function encryptMnemonic2(mnemonic, password) {
|
|
|
6798
7313
|
if (typeof password !== "string" || password.length === 0) {
|
|
6799
7314
|
throw new Error("encryptMnemonic: password must be a non-empty string");
|
|
6800
7315
|
}
|
|
6801
|
-
const salt =
|
|
6802
|
-
const iv =
|
|
6803
|
-
const
|
|
7316
|
+
const salt = randomBytes2(SALT_LEN);
|
|
7317
|
+
const iv = randomBytes2(IV_LEN);
|
|
7318
|
+
const key2 = scryptSync(password, salt, SCRYPT_KEY_LEN, {
|
|
6804
7319
|
N: SCRYPT_N,
|
|
6805
7320
|
r: SCRYPT_R,
|
|
6806
7321
|
p: SCRYPT_P,
|
|
6807
7322
|
maxmem: SCRYPT_MAXMEM
|
|
6808
7323
|
});
|
|
6809
7324
|
try {
|
|
6810
|
-
const cipher = createCipheriv("aes-256-gcm",
|
|
7325
|
+
const cipher = createCipheriv("aes-256-gcm", key2, iv, {
|
|
6811
7326
|
authTagLength: AUTH_TAG_LEN
|
|
6812
7327
|
});
|
|
6813
7328
|
const ciphertext = Buffer.concat([
|
|
@@ -6823,7 +7338,7 @@ function encryptMnemonic2(mnemonic, password) {
|
|
|
6823
7338
|
version: 1
|
|
6824
7339
|
};
|
|
6825
7340
|
} finally {
|
|
6826
|
-
|
|
7341
|
+
key2.fill(0);
|
|
6827
7342
|
}
|
|
6828
7343
|
}
|
|
6829
7344
|
function decryptMnemonic2(encrypted, password) {
|
|
@@ -6838,14 +7353,14 @@ function decryptMnemonic2(encrypted, password) {
|
|
|
6838
7353
|
const iv = Buffer.from(encrypted.iv, "base64");
|
|
6839
7354
|
const ciphertext = Buffer.from(encrypted.ciphertext, "base64");
|
|
6840
7355
|
const tag = Buffer.from(encrypted.tag, "base64");
|
|
6841
|
-
const
|
|
7356
|
+
const key2 = scryptSync(password, salt, SCRYPT_KEY_LEN, {
|
|
6842
7357
|
N: SCRYPT_N,
|
|
6843
7358
|
r: SCRYPT_R,
|
|
6844
7359
|
p: SCRYPT_P,
|
|
6845
7360
|
maxmem: SCRYPT_MAXMEM
|
|
6846
7361
|
});
|
|
6847
7362
|
try {
|
|
6848
|
-
const decipher = createDecipheriv("aes-256-gcm",
|
|
7363
|
+
const decipher = createDecipheriv("aes-256-gcm", key2, iv, {
|
|
6849
7364
|
authTagLength: AUTH_TAG_LEN
|
|
6850
7365
|
});
|
|
6851
7366
|
decipher.setAuthTag(tag);
|
|
@@ -6861,7 +7376,7 @@ function decryptMnemonic2(encrypted, password) {
|
|
|
6861
7376
|
);
|
|
6862
7377
|
}
|
|
6863
7378
|
} finally {
|
|
6864
|
-
|
|
7379
|
+
key2.fill(0);
|
|
6865
7380
|
}
|
|
6866
7381
|
}
|
|
6867
7382
|
function generateKeystore(path, password) {
|
|
@@ -6884,7 +7399,7 @@ function importKeystore(path, mnemonic, password) {
|
|
|
6884
7399
|
}
|
|
6885
7400
|
function loadKeystore(path, password) {
|
|
6886
7401
|
assertNode();
|
|
6887
|
-
const raw =
|
|
7402
|
+
const raw = readFileSync3(path, "utf8");
|
|
6888
7403
|
let parsed;
|
|
6889
7404
|
try {
|
|
6890
7405
|
parsed = JSON.parse(raw);
|
|
@@ -6895,17 +7410,20 @@ function loadKeystore(path, password) {
|
|
|
6895
7410
|
}
|
|
6896
7411
|
function writeKeystoreFile(path, keystore) {
|
|
6897
7412
|
assertNode();
|
|
6898
|
-
|
|
7413
|
+
writeFileSync3(path, JSON.stringify(keystore, null, 2), {
|
|
6899
7414
|
encoding: "utf8",
|
|
6900
7415
|
mode: 384
|
|
6901
7416
|
});
|
|
6902
7417
|
}
|
|
6903
7418
|
export {
|
|
6904
7419
|
BtpRuntimeClient,
|
|
7420
|
+
CONDITION_LENGTH,
|
|
6905
7421
|
ChannelFundingError,
|
|
6906
7422
|
ChannelManager,
|
|
6907
7423
|
ConnectorError,
|
|
6908
7424
|
EvmSigner,
|
|
7425
|
+
FULFILLMENT_MISMATCH_CODE,
|
|
7426
|
+
FULFILLMENT_MISMATCH_MESSAGE,
|
|
6909
7427
|
GenerativeFallbackRenderer,
|
|
6910
7428
|
Http402Client,
|
|
6911
7429
|
HttpConnectorAdmin,
|
|
@@ -6914,6 +7432,8 @@ export {
|
|
|
6914
7432
|
ILP_CLAIM_HEADER,
|
|
6915
7433
|
ILP_CLAIM_WRAPPED_HEADER,
|
|
6916
7434
|
ILP_PEER_ID_HEADER,
|
|
7435
|
+
InMemoryReceivedClaimStore,
|
|
7436
|
+
JsonFileReceivedClaimStore,
|
|
6917
7437
|
KeyManager,
|
|
6918
7438
|
KindRegistry,
|
|
6919
7439
|
MIME_A2UI,
|
|
@@ -6930,14 +7450,17 @@ export {
|
|
|
6930
7450
|
ValidationError,
|
|
6931
7451
|
applyDefaults,
|
|
6932
7452
|
applyNetworkPresets,
|
|
7453
|
+
assertValidCondition,
|
|
6933
7454
|
buildBackupEvent,
|
|
6934
7455
|
buildBackupFilter,
|
|
6935
7456
|
buildConsentRequest,
|
|
6936
7457
|
buildRendererEventTemplate,
|
|
6937
7458
|
buildSettlementInfo,
|
|
6938
7459
|
buildStoreWriteEnvelope,
|
|
7460
|
+
buildSwapSettlements,
|
|
6939
7461
|
buildUiCoordinate,
|
|
6940
7462
|
classifyIntent,
|
|
7463
|
+
decodeEvmSettlementTx,
|
|
6941
7464
|
decryptMnemonic2 as decryptMnemonic,
|
|
6942
7465
|
defaultFaucetTimeout,
|
|
6943
7466
|
deriveFromNsec,
|
|
@@ -6945,8 +7468,10 @@ export {
|
|
|
6945
7468
|
deriveNostrKeyFromMnemonic,
|
|
6946
7469
|
deterministicGenerator,
|
|
6947
7470
|
encryptMnemonic2 as encryptMnemonic,
|
|
7471
|
+
entryToAccumulatedClaim,
|
|
6948
7472
|
extractArweaveTxId,
|
|
6949
7473
|
extractUiResource,
|
|
7474
|
+
fulfillmentMatchesCondition,
|
|
6950
7475
|
fundWallet,
|
|
6951
7476
|
generateKeystore,
|
|
6952
7477
|
generateMnemonic,
|
|
@@ -6954,13 +7479,18 @@ export {
|
|
|
6954
7479
|
getNetworkStatus,
|
|
6955
7480
|
getUiCoordinate,
|
|
6956
7481
|
guardedRenderDispatch,
|
|
7482
|
+
hasSettlementMetadata,
|
|
6957
7483
|
httpEndpointToBtpUrl,
|
|
6958
7484
|
importKeystore,
|
|
7485
|
+
ingestReceivedClaims,
|
|
6959
7486
|
isInsufficientGasError,
|
|
6960
7487
|
isPrfSupported,
|
|
6961
7488
|
isTrustDowngrade,
|
|
7489
|
+
isZeroCondition,
|
|
6962
7490
|
loadKeystore,
|
|
7491
|
+
mintExecutionCondition,
|
|
6963
7492
|
parseBackupPayload,
|
|
7493
|
+
parseEvmChainId2 as parseEvmChainId,
|
|
6964
7494
|
parseFulfillHttp,
|
|
6965
7495
|
parseFulfillHttpBytes,
|
|
6966
7496
|
parseHttpResponse,
|
|
@@ -6986,6 +7516,7 @@ export {
|
|
|
6986
7516
|
selectIlpTransport,
|
|
6987
7517
|
selectLatestAddressable,
|
|
6988
7518
|
serializeHttpRequest,
|
|
7519
|
+
submitEvmSettlement,
|
|
6989
7520
|
validateConfig,
|
|
6990
7521
|
validateMnemonic,
|
|
6991
7522
|
verifyRendererTrust,
|