@toon-protocol/client 0.15.0 → 0.17.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.js CHANGED
@@ -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,70 @@ 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
+ var FULFILLMENT_MISMATCH_CODE = "F99";
1313
+ var FULFILLMENT_MISMATCH_MESSAGE = "FULFILL fulfillment does not match execution condition (sha256(fulfillment) != executionCondition) \u2014 packet counted failed";
1314
+ function mapIlpResponse(packet, sentCondition) {
1315
+ if (packet.type === ILPPacketType.FULFILL) {
1316
+ const dataField = packet.data.length > 0 ? { data: toBase64(packet.data) } : {};
1317
+ if (sentCondition === void 0 || isZeroCondition(sentCondition)) {
1318
+ return { accepted: true, ...dataField };
1319
+ }
1320
+ if (!fulfillmentMatchesCondition(packet.fulfillment, sentCondition)) {
1321
+ return {
1322
+ accepted: false,
1323
+ code: FULFILLMENT_MISMATCH_CODE,
1324
+ message: FULFILLMENT_MISMATCH_MESSAGE,
1325
+ ...dataField
1326
+ };
1327
+ }
1328
+ return {
1329
+ accepted: true,
1330
+ fulfillment: toBase64(packet.fulfillment),
1331
+ ...dataField
1332
+ };
1333
+ }
1334
+ return {
1335
+ accepted: false,
1336
+ code: packet.code,
1337
+ message: packet.message,
1338
+ ...packet.data.length > 0 ? { data: toBase64(packet.data) } : {}
1339
+ };
1340
+ }
1341
+
1272
1342
  // src/adapters/BtpRuntimeClient.ts
1273
1343
  function isConnectionError(error) {
1274
1344
  const msg = error.message.toLowerCase();
@@ -1324,6 +1394,12 @@ var BtpRuntimeClient = class {
1324
1394
  /**
1325
1395
  * Sends an ILP packet via BTP with auto-reconnect on connection errors.
1326
1396
  * Satisfies IlpClient interface.
1397
+ *
1398
+ * `params` may carry a sender-chosen `executionCondition` and an explicit
1399
+ * `expiresAt` (toon-client#350); omitting both is the legacy zero-condition
1400
+ * path, unchanged. With a non-zero condition the FULFILL preimage is
1401
+ * verified (`sha256(fulfillment) == condition`) and a mismatch is surfaced
1402
+ * as a failed result — see `mapIlpResponse`.
1327
1403
  */
1328
1404
  async sendIlpPacket(params) {
1329
1405
  return withRetry(() => this._sendIlpPacketOnce(params), {
@@ -1339,6 +1415,9 @@ var BtpRuntimeClient = class {
1339
1415
  /**
1340
1416
  * Sends a balance proof claim via BTP protocol data, then sends an ILP packet.
1341
1417
  * Auto-reconnects on connection errors.
1418
+ *
1419
+ * Sender-chosen `executionCondition` / explicit `expiresAt` semantics are
1420
+ * identical to {@link sendIlpPacket}.
1342
1421
  */
1343
1422
  async sendIlpPacketWithClaim(params, claim) {
1344
1423
  return withRetry(() => this._sendIlpPacketWithClaimOnce(params, claim), {
@@ -1381,6 +1460,30 @@ var BtpRuntimeClient = class {
1381
1460
  encodeUtf8(JSON.stringify(claim))
1382
1461
  );
1383
1462
  }
1463
+ /**
1464
+ * Build the ILP PREPARE for a send, applying the sender-chosen condition /
1465
+ * explicit expiry when provided (toon-client#350) and validating that a
1466
+ * non-zero condition is exactly 32 bytes (the OER serializer would
1467
+ * otherwise silently zero-fill it, downgrading the packet to the legacy
1468
+ * unverified class).
1469
+ */
1470
+ buildPrepare(params) {
1471
+ const condition = params.executionCondition;
1472
+ if (condition !== void 0 && !isZeroCondition(condition)) {
1473
+ assertValidCondition(condition);
1474
+ }
1475
+ return {
1476
+ prepare: {
1477
+ type: 12,
1478
+ amount: BigInt(params.amount),
1479
+ destination: params.destination,
1480
+ executionCondition: condition ?? new Uint8Array(32),
1481
+ expiresAt: params.expiresAt ?? new Date(Date.now() + (params.timeout ?? 3e4)),
1482
+ data: fromBase64(params.data)
1483
+ },
1484
+ condition
1485
+ };
1486
+ }
1384
1487
  /**
1385
1488
  * Single-attempt ILP packet send. Reconnects if not connected.
1386
1489
  */
@@ -1388,26 +1491,9 @@ var BtpRuntimeClient = class {
1388
1491
  if (!this._isConnected) {
1389
1492
  await this.reconnect();
1390
1493
  }
1391
- const response = await this.btpClient.sendPacket({
1392
- type: 12,
1393
- amount: BigInt(params.amount),
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
- };
1494
+ const { prepare, condition } = this.buildPrepare(params);
1495
+ const response = await this.btpClient.sendPacket(prepare);
1496
+ return mapIlpResponse(response, condition);
1411
1497
  }
1412
1498
  /**
1413
1499
  * Single-attempt claim + ILP packet send. Reconnects if not connected.
@@ -1426,29 +1512,9 @@ var BtpRuntimeClient = class {
1426
1512
  data: encodeUtf8(JSON.stringify(claim))
1427
1513
  }
1428
1514
  ];
1429
- const response = await this.btpClient.sendPacket(
1430
- {
1431
- type: 12,
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
- };
1515
+ const { prepare, condition } = this.buildPrepare(params);
1516
+ const response = await this.btpClient.sendPacket(prepare, protocolData);
1517
+ return mapIlpResponse(response, condition);
1452
1518
  }
1453
1519
  };
1454
1520
 
@@ -1480,6 +1546,12 @@ var HttpIlpClient = class {
1480
1546
  * Send an ILP PREPARE via `POST /ilp` WITHOUT a claim. The connector accepts
1481
1547
  * this only on free/zero-amount routes; paid writes must use
1482
1548
  * {@link sendIlpPacketWithClaim}. Satisfies the IlpClient interface.
1549
+ *
1550
+ * `params` may carry a sender-chosen `executionCondition` and an explicit
1551
+ * `expiresAt` (toon-client#350); omitting both is the legacy zero-condition
1552
+ * path, unchanged. With a non-zero condition the FULFILL preimage is
1553
+ * verified (`sha256(fulfillment) == condition`) and a mismatch is surfaced
1554
+ * as a failed result — see {@link mapIlpResponse}.
1483
1555
  */
1484
1556
  async sendIlpPacket(params) {
1485
1557
  return withRetry(() => this.postPrepare(params), {
@@ -1494,6 +1566,9 @@ var HttpIlpClient = class {
1494
1566
  * as the `ILP-Payment-Channel-Claim` header. `claim` is the SAME JSON object
1495
1567
  * the BTP path attaches as the `payment-channel-claim` protocolData entry —
1496
1568
  * we base64(JSON.stringify(claim)) it, byte-for-byte identical to BTP.
1569
+ *
1570
+ * Sender-chosen `executionCondition` / explicit `expiresAt` semantics are
1571
+ * identical to {@link sendIlpPacket}.
1497
1572
  */
1498
1573
  async sendIlpPacketWithClaim(params, claim) {
1499
1574
  return withRetry(() => this.postPrepare(params, claim), {
@@ -1546,12 +1621,16 @@ var HttpIlpClient = class {
1546
1621
  */
1547
1622
  async postPrepare(params, claim) {
1548
1623
  const requestTimeout = params.timeout ?? this.timeout;
1624
+ const condition = params.executionCondition;
1625
+ if (condition !== void 0 && !isZeroCondition(condition)) {
1626
+ assertValidCondition(condition);
1627
+ }
1549
1628
  const prepare = serializeIlpPrepare({
1550
1629
  type: ILPPacketType.PREPARE,
1551
1630
  amount: BigInt(params.amount),
1552
1631
  destination: params.destination,
1553
- executionCondition: new Uint8Array(32),
1554
- expiresAt: new Date(Date.now() + requestTimeout),
1632
+ executionCondition: condition ?? new Uint8Array(32),
1633
+ expiresAt: params.expiresAt ?? new Date(Date.now() + requestTimeout),
1555
1634
  data: fromBase64(params.data)
1556
1635
  });
1557
1636
  const headers = {
@@ -1574,7 +1653,7 @@ var HttpIlpClient = class {
1574
1653
  signal: controller.signal
1575
1654
  });
1576
1655
  clearTimeout(timeoutId);
1577
- return await this.mapResponse(response);
1656
+ return await this.mapResponse(response, condition);
1578
1657
  } catch (error) {
1579
1658
  clearTimeout(timeoutId);
1580
1659
  throw this.mapTransportError(error, requestTimeout);
@@ -1584,26 +1663,18 @@ var HttpIlpClient = class {
1584
1663
  * Map a `200 OK` body (OER FULFILL/REJECT) to an IlpSendResult; map a non-2xx
1585
1664
  * to a transport error. Per the wire contract, ILP-level rejects arrive as a
1586
1665
  * 200 + REJECT body — only HTTP non-2xx means a transport-layer failure.
1666
+ *
1667
+ * When `sentCondition` is non-zero the FULFILL preimage is verified against
1668
+ * it; a mismatch yields `accepted: false` (shared `mapIlpResponse` logic,
1669
+ * identical to the BTP path).
1587
1670
  */
1588
- async mapResponse(response) {
1671
+ async mapResponse(response, sentCondition) {
1589
1672
  if (response.ok) {
1590
1673
  const buf = new Uint8Array(await response.arrayBuffer());
1591
1674
  if (buf.length === 0) {
1592
1675
  throw new ConnectorError("Empty 200 body from /ilp (expected OER ILP response)");
1593
1676
  }
1594
- const ilp = deserializeIlpPacket(buf);
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
- };
1677
+ return mapIlpResponse(deserializeIlpPacket(buf), sentCondition);
1607
1678
  }
1608
1679
  const body = await response.text().catch(() => "");
1609
1680
  const detail = body ? `: ${body}` : "";
@@ -1692,7 +1763,7 @@ import { base58Encode as base58Encode2 } from "@toon-protocol/core";
1692
1763
 
1693
1764
  // src/channel/solana-payment-channel.ts
1694
1765
  import { ed25519 } from "@noble/curves/ed25519.js";
1695
- import { sha256 } from "@noble/hashes/sha2.js";
1766
+ import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
1696
1767
  import { base58Encode, base58Decode } from "@toon-protocol/core";
1697
1768
  var TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
1698
1769
  var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
@@ -1781,7 +1852,7 @@ function findProgramAddress(seeds, programId) {
1781
1852
  input.set(programId, offset);
1782
1853
  offset += programId.length;
1783
1854
  input.set(PDA_MARKER, offset);
1784
- const hash = sha256(input);
1855
+ const hash = sha2562(input);
1785
1856
  if (!isOnCurve(hash)) return { pda: hash, bump };
1786
1857
  }
1787
1858
  throw new Error("Could not find a viable PDA bump seed");
@@ -3234,7 +3305,7 @@ var SolanaSigner = class {
3234
3305
 
3235
3306
  // src/signing/mina-signer.ts
3236
3307
  import { hexToMinaBase58PrivateKey as hexToMinaBase58PrivateKey3 } from "@toon-protocol/core";
3237
- import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
3308
+ import { sha256 as sha2563 } from "@noble/hashes/sha2.js";
3238
3309
  import { bytesToHex } from "@noble/hashes/utils.js";
3239
3310
 
3240
3311
  // src/channel/mina-payment-channel.ts
@@ -3365,7 +3436,7 @@ var DEFAULT_MINA_TOKEN_ID = "MINA";
3365
3436
  var MINA_CLAIM_NETWORK = "devnet";
3366
3437
  function deriveMinaSalt(zkAppAddress, nonce) {
3367
3438
  const digestHex = bytesToHex(
3368
- sha2562(new TextEncoder().encode(`mina-pc-salt:${zkAppAddress}:${nonce}`))
3439
+ sha2563(new TextEncoder().encode(`mina-pc-salt:${zkAppAddress}:${nonce}`))
3369
3440
  );
3370
3441
  const salt = BigInt("0x" + digestHex.slice(0, 60));
3371
3442
  return salt === 0n ? 1n : salt;
@@ -3957,6 +4028,38 @@ async function readEvmTokenBalance(opts) {
3957
4028
  if (decimals !== void 0) out.assetScale = Number(decimals);
3958
4029
  return out;
3959
4030
  }
4031
+ async function readEvmNativeBalance(opts) {
4032
+ const chainId = parseEvmChainId(opts.chainKey);
4033
+ const client = createPublicClient2({
4034
+ transport: http2(opts.rpcUrl),
4035
+ chain: defineChain2({
4036
+ id: chainId,
4037
+ name: opts.chainKey,
4038
+ nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
4039
+ rpcUrls: { default: { http: [opts.rpcUrl] } }
4040
+ })
4041
+ });
4042
+ const wei = await client.getBalance({ address: opts.owner });
4043
+ return { symbol: "ETH", amount: wei.toString(), decimals: 18 };
4044
+ }
4045
+ async function readSolanaNativeBalance(opts) {
4046
+ const fetchImpl = opts.fetchImpl ?? fetch;
4047
+ const res = await fetchImpl(opts.rpcUrl, {
4048
+ method: "POST",
4049
+ headers: { "content-type": "application/json" },
4050
+ body: JSON.stringify({
4051
+ jsonrpc: "2.0",
4052
+ id: 1,
4053
+ method: "getBalance",
4054
+ params: [opts.owner, { commitment: "confirmed" }]
4055
+ })
4056
+ });
4057
+ if (!res.ok) throw new Error(`Solana RPC request failed: HTTP ${res.status}`);
4058
+ const json = await res.json();
4059
+ if (json.error) throw new Error(`Solana RPC error: ${json.error.message ?? "unknown"}`);
4060
+ const lamports = BigInt(json.result?.value ?? 0);
4061
+ return { symbol: "SOL", amount: lamports.toString(), decimals: 9 };
4062
+ }
3960
4063
  async function readSolanaTokenBalance(opts) {
3961
4064
  const fetchImpl = opts.fetchImpl ?? fetch;
3962
4065
  const res = await fetchImpl(opts.rpcUrl, {
@@ -4004,6 +4107,95 @@ async function readMinaBalance(opts) {
4004
4107
  assetScale: 9
4005
4108
  };
4006
4109
  }
4110
+ var errText = (e) => e instanceof Error ? e.message : String(e);
4111
+ function foldNative(out, settled, errors) {
4112
+ if (settled.status === "fulfilled") out.native = settled.value;
4113
+ else errors.push(errText(settled.reason));
4114
+ }
4115
+ function foldToken(out, settled, tokenAddress, errors) {
4116
+ if (settled.status === "rejected") {
4117
+ errors.push(errText(settled.reason));
4118
+ return;
4119
+ }
4120
+ const bal = settled.value;
4121
+ if (!bal) return;
4122
+ out.tokens.push({
4123
+ symbol: bal.asset,
4124
+ amount: bal.amount,
4125
+ decimals: bal.assetScale,
4126
+ ...tokenAddress ? { address: tokenAddress } : {}
4127
+ });
4128
+ }
4129
+ function finalizeChain(out, errors) {
4130
+ if (errors.length > 0) out.error = errors[0];
4131
+ if (out.native === void 0 && out.tokens.length === 0) out.unreadable = true;
4132
+ }
4133
+ async function readWalletBalances(sources) {
4134
+ const { fetchImpl } = sources;
4135
+ const tasks = [];
4136
+ if (sources.evm) {
4137
+ const { chainKey, rpcUrl, owner, tokenAddress } = sources.evm;
4138
+ tasks.push(
4139
+ (async () => {
4140
+ const out = { chain: "evm", chainKey, address: owner, tokens: [] };
4141
+ const errors = [];
4142
+ const [nativeR, tokenR] = await Promise.allSettled([
4143
+ readEvmNativeBalance({ rpcUrl, chainKey, owner }),
4144
+ tokenAddress ? readEvmTokenBalance({ rpcUrl, chainKey, tokenAddress, owner }) : Promise.resolve(void 0)
4145
+ ]);
4146
+ foldNative(out, nativeR, errors);
4147
+ foldToken(out, tokenR, tokenAddress, errors);
4148
+ finalizeChain(out, errors);
4149
+ return out;
4150
+ })()
4151
+ );
4152
+ }
4153
+ if (sources.solana) {
4154
+ const { chainKey = "solana", rpcUrl, owner, tokenMint } = sources.solana;
4155
+ tasks.push(
4156
+ (async () => {
4157
+ const out = { chain: "solana", chainKey, address: owner, tokens: [] };
4158
+ const errors = [];
4159
+ const [nativeR, tokenR] = await Promise.allSettled([
4160
+ readSolanaNativeBalance({ rpcUrl, owner, fetchImpl }),
4161
+ tokenMint ? readSolanaTokenBalance({ rpcUrl, mint: tokenMint, owner, fetchImpl }) : Promise.resolve(void 0)
4162
+ ]);
4163
+ foldNative(out, nativeR, errors);
4164
+ if (tokenR.status === "fulfilled" && tokenR.value && tokenR.value.asset === void 0) {
4165
+ tokenR.value.asset = "USDC";
4166
+ }
4167
+ foldToken(out, tokenR, tokenMint, errors);
4168
+ finalizeChain(out, errors);
4169
+ return out;
4170
+ })()
4171
+ );
4172
+ }
4173
+ if (sources.mina) {
4174
+ const { chainKey = "mina", graphqlUrl, owner } = sources.mina;
4175
+ tasks.push(
4176
+ (async () => {
4177
+ const out = { chain: "mina", chainKey, address: owner, tokens: [] };
4178
+ const errors = [];
4179
+ const nativeR = await Promise.allSettled([readMinaBalance({ graphqlUrl, owner, fetchImpl })]);
4180
+ foldNative(
4181
+ out,
4182
+ nativeR[0].status === "fulfilled" ? {
4183
+ status: "fulfilled",
4184
+ value: {
4185
+ symbol: nativeR[0].value.asset,
4186
+ amount: nativeR[0].value.amount,
4187
+ decimals: nativeR[0].value.assetScale
4188
+ }
4189
+ } : nativeR[0],
4190
+ errors
4191
+ );
4192
+ finalizeChain(out, errors);
4193
+ return out;
4194
+ })()
4195
+ );
4196
+ }
4197
+ return Promise.all(tasks);
4198
+ }
4007
4199
 
4008
4200
  // src/blob-storage.ts
4009
4201
  import { buildBlobStorageRequest } from "@toon-protocol/core";
@@ -4776,6 +4968,14 @@ var ToonClient = class {
4776
4968
  * matching `destination`,
4777
4969
  * (c) neither -> throw MISSING_CLAIM.
4778
4970
  *
4971
+ * A caller may supply a sender-chosen 32-byte `executionCondition`
4972
+ * (`C = sha256(P)`, one fresh preimage per packet — toon-client#350,
4973
+ * rolling-swap spec §3 R1/R2) and an explicit `expiresAt` (R7). Both are
4974
+ * set on the wire by either transport (HTTP `POST /ilp` and BTP), and on
4975
+ * FULFILL the transport verifies `sha256(fulfillment) == condition` —
4976
+ * a mismatch comes back as `accepted: false` (code F99), never a silent
4977
+ * accept. Omitting them keeps today's zero-condition legacy packet.
4978
+ *
4779
4979
  * @throws {ToonClientError} INVALID_STATE / NO_ILP_TRANSPORT / MISSING_CLAIM
4780
4980
  */
4781
4981
  async sendSwapPacket(params) {
@@ -4796,7 +4996,9 @@ var ToonClient = class {
4796
4996
  destination: params.destination,
4797
4997
  amount: String(params.amount),
4798
4998
  data: toBase64(params.toonData),
4799
- timeout: params.timeout ?? 3e4
4999
+ timeout: params.timeout ?? 3e4,
5000
+ ...params.executionCondition ? { executionCondition: params.executionCondition } : {},
5001
+ ...params.expiresAt ? { expiresAt: params.expiresAt } : {}
4800
5002
  },
4801
5003
  claimMessage
4802
5004
  );
@@ -5154,6 +5356,74 @@ var ToonClient = class {
5154
5356
  }
5155
5357
  return out;
5156
5358
  }
5359
+ /**
5360
+ * The FULL multi-chain wallet view (#299): for every chain the identity is
5361
+ * configured for, the native coin (ETH / SOL / MINA) AND every configured
5362
+ * token (USDC), grouped per chain with the identity's address on that chain.
5363
+ * A superset of {@link getBalances} — which stays scoped to the channel's
5364
+ * settlement token — kept as a separate reader so channel-settlement callers
5365
+ * are unaffected.
5366
+ *
5367
+ * FREE: read-only RPC, no signing, no payment. Works on an UNSTARTED client:
5368
+ * the Solana/Mina addresses (which the signers only register during
5369
+ * `start()`) are derived on demand from the retained mnemonic — the SAME keys
5370
+ * `start()` would register and that `rig fund` prints — so all configured
5371
+ * chains appear even before a start. Best-effort per chain: an unreachable
5372
+ * RPC yields `{ unreadable: true }` for that chain, never failing the others.
5373
+ */
5374
+ async getWalletBalances() {
5375
+ const sources = {};
5376
+ let derived;
5377
+ let derivedTried = false;
5378
+ const ensureDerived = async () => {
5379
+ if (derivedTried) return derived;
5380
+ derivedTried = true;
5381
+ if (this.config.mnemonic) {
5382
+ derived = await deriveFullIdentity(
5383
+ this.config.mnemonic,
5384
+ this.config.mnemonicAccountIndex ?? 0
5385
+ );
5386
+ }
5387
+ return derived;
5388
+ };
5389
+ const evmAddress = this.getEvmAddress();
5390
+ const rpcUrls = this.config.chainRpcUrls;
5391
+ const tokens = this.config.preferredTokens;
5392
+ if (evmAddress && rpcUrls) {
5393
+ const usableEvm = (c) => c.startsWith("evm") && Boolean(rpcUrls[c]);
5394
+ const settlementKeys = Object.keys(this.config.settlementAddresses ?? {});
5395
+ const chainKeys = this.config.supportedChains ?? Object.keys(rpcUrls);
5396
+ const chainKey = settlementKeys.find(usableEvm) ?? chainKeys.find(usableEvm);
5397
+ if (chainKey && rpcUrls[chainKey]) {
5398
+ sources.evm = {
5399
+ chainKey,
5400
+ rpcUrl: rpcUrls[chainKey],
5401
+ owner: evmAddress,
5402
+ ...tokens?.[chainKey] ? { tokenAddress: tokens[chainKey] } : {}
5403
+ };
5404
+ }
5405
+ }
5406
+ const sol = this.config.solanaChannel;
5407
+ if (sol?.rpcUrl) {
5408
+ const solAddress = this.getSolanaAddress() ?? (await ensureDerived())?.solana.publicKey;
5409
+ if (solAddress) {
5410
+ sources.solana = {
5411
+ chainKey: "solana",
5412
+ rpcUrl: sol.rpcUrl,
5413
+ owner: solAddress,
5414
+ ...sol.tokenMint ? { tokenMint: sol.tokenMint } : {}
5415
+ };
5416
+ }
5417
+ }
5418
+ const mina = this.config.minaChannel;
5419
+ if (mina?.graphqlUrl) {
5420
+ const minaAddress = this.getMinaAddress() ?? (await ensureDerived())?.mina.publicKey;
5421
+ if (minaAddress) {
5422
+ sources.mina = { chainKey: "mina", graphqlUrl: mina.graphqlUrl, owner: minaAddress };
5423
+ }
5424
+ }
5425
+ return readWalletBalances(sources);
5426
+ }
5157
5427
  /**
5158
5428
  * Resolves an ILP destination address to a peer ID.
5159
5429
  * Convention: destination "g.toon.peer1" → peerId "peer1" (last segment).
@@ -6582,7 +6852,7 @@ import {
6582
6852
  scryptSync,
6583
6853
  createCipheriv,
6584
6854
  createDecipheriv,
6585
- randomBytes
6855
+ randomBytes as randomBytes2
6586
6856
  } from "crypto";
6587
6857
  import { writeFileSync as writeFileSync2, readFileSync as readFileSync2 } from "fs";
6588
6858
  var SCRYPT_N = 2 ** 17;
@@ -6609,8 +6879,8 @@ function encryptMnemonic2(mnemonic, password) {
6609
6879
  if (typeof password !== "string" || password.length === 0) {
6610
6880
  throw new Error("encryptMnemonic: password must be a non-empty string");
6611
6881
  }
6612
- const salt = randomBytes(SALT_LEN);
6613
- const iv = randomBytes(IV_LEN);
6882
+ const salt = randomBytes2(SALT_LEN);
6883
+ const iv = randomBytes2(IV_LEN);
6614
6884
  const key = scryptSync(password, salt, SCRYPT_KEY_LEN, {
6615
6885
  N: SCRYPT_N,
6616
6886
  r: SCRYPT_R,
@@ -6713,10 +6983,13 @@ function writeKeystoreFile(path, keystore) {
6713
6983
  }
6714
6984
  export {
6715
6985
  BtpRuntimeClient,
6986
+ CONDITION_LENGTH,
6716
6987
  ChannelFundingError,
6717
6988
  ChannelManager,
6718
6989
  ConnectorError,
6719
6990
  EvmSigner,
6991
+ FULFILLMENT_MISMATCH_CODE,
6992
+ FULFILLMENT_MISMATCH_MESSAGE,
6720
6993
  GenerativeFallbackRenderer,
6721
6994
  Http402Client,
6722
6995
  HttpConnectorAdmin,
@@ -6741,6 +7014,7 @@ export {
6741
7014
  ValidationError,
6742
7015
  applyDefaults,
6743
7016
  applyNetworkPresets,
7017
+ assertValidCondition,
6744
7018
  buildBackupEvent,
6745
7019
  buildBackupFilter,
6746
7020
  buildConsentRequest,
@@ -6758,6 +7032,7 @@ export {
6758
7032
  encryptMnemonic2 as encryptMnemonic,
6759
7033
  extractArweaveTxId,
6760
7034
  extractUiResource,
7035
+ fulfillmentMatchesCondition,
6761
7036
  fundWallet,
6762
7037
  generateKeystore,
6763
7038
  generateMnemonic,
@@ -6770,7 +7045,9 @@ export {
6770
7045
  isInsufficientGasError,
6771
7046
  isPrfSupported,
6772
7047
  isTrustDowngrade,
7048
+ isZeroCondition,
6773
7049
  loadKeystore,
7050
+ mintExecutionCondition,
6774
7051
  parseBackupPayload,
6775
7052
  parseFulfillHttp,
6776
7053
  parseFulfillHttpBytes,
@@ -6781,7 +7058,13 @@ export {
6781
7058
  proxyIlpEndpoint,
6782
7059
  publishBackCoordinate,
6783
7060
  readDiscoveredIlpPeer,
7061
+ readEvmNativeBalance,
7062
+ readEvmTokenBalance,
7063
+ readMinaBalance,
6784
7064
  readMinaDepositTotal,
7065
+ readSolanaNativeBalance,
7066
+ readSolanaTokenBalance,
7067
+ readWalletBalances,
6785
7068
  renderDeterministicHtml,
6786
7069
  renderDispatch,
6787
7070
  requestBlobStorage,