@toon-protocol/client 0.16.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.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _toon_protocol_core from '@toon-protocol/core';
2
- import { IlpPeerInfo, IlpClient, IlpSendResult, NetworkFamilyStatus, ConnectorAdminClient, ConnectorChannelClient, OpenChannelParams, OpenChannelResult, ChannelState } from '@toon-protocol/core';
2
+ import { IlpPeerInfo, IlpSendResult, IlpClient, NetworkFamilyStatus, ConnectorAdminClient, ConnectorChannelClient, OpenChannelParams, OpenChannelResult, ChannelState } from '@toon-protocol/core';
3
3
  export { UI_RENDERER_KIND, UI_TAG, UiCoordinate, buildUiCoordinate, getUiCoordinate, parseUiCoordinate, selectLatestAddressable } from '@toon-protocol/core';
4
4
  import { NostrEvent, EventTemplate } from 'nostr-tools/pure';
5
5
  import { PrivateKeyAccount } from 'viem/accounts';
@@ -640,6 +640,62 @@ declare function requestBlobStorage(client: ToonClient, secretKey: Uint8Array, p
640
640
  */
641
641
  declare function extractArweaveTxId(base64Data: string): string;
642
642
 
643
+ /**
644
+ * Shared sender-side plumbing for sender-chosen execution conditions
645
+ * (toon-client#350; contract: connector `docs/local-delivery-fulfillment-contract.md`,
646
+ * connector#309; spec: toon-meta `docs/rolling-swap.md` §3).
647
+ *
648
+ * Both ILP transports (HTTP `POST /ilp` and BTP) take the SAME extended send
649
+ * params and map FULFILL/REJECT responses through the SAME verifier so the
650
+ * two paths cannot drift:
651
+ *
652
+ * - absent/all-zero `executionCondition` → legacy class: today's behavior,
653
+ * byte-for-byte (zero condition on the wire, no FULFILL verification);
654
+ * - non-zero `executionCondition` → sender-chosen: the condition goes on
655
+ * the PREPARE verbatim and the FULFILL's 32-byte fulfillment MUST hash
656
+ * back to it — a missing/malformed/mismatching preimage is surfaced as a
657
+ * FAILED result (never a silent accept, never retried).
658
+ */
659
+
660
+ /**
661
+ * Send parameters accepted by both ILP transports. Extends the
662
+ * `@toon-protocol/core` `IlpClient` param shape with the sender-chosen
663
+ * condition and an explicit expiry — plain `IlpClient` callers keep working
664
+ * unchanged (both extras are optional; omitting them is the legacy path).
665
+ */
666
+ interface IlpSendParams {
667
+ destination: string;
668
+ amount: string;
669
+ /** Base64 ILP `data` payload. */
670
+ data: string;
671
+ /** Transport timeout in ms; also the default expiry window. */
672
+ timeout?: number;
673
+ /**
674
+ * Sender-chosen 32-byte execution condition `C = sha256(P)` (spec R2).
675
+ * Absent or all-zero = legacy unverified packet (default — ordinary
676
+ * publish/upload writes MUST keep this default).
677
+ */
678
+ executionCondition?: Uint8Array;
679
+ /**
680
+ * Explicit PREPARE `expiresAt` (spec R7). Defaults to `now + timeout`,
681
+ * preserving pre-#350 behavior.
682
+ */
683
+ expiresAt?: Date;
684
+ }
685
+ /**
686
+ * `IlpSendResult` plus the FULFILL preimage when a sender-chosen condition
687
+ * was verified. Only populated on the sender-chosen path so legacy result
688
+ * objects stay byte-identical.
689
+ */
690
+ interface IlpSendResultWithFulfillment extends IlpSendResult {
691
+ /** Base64 32-byte fulfillment preimage (verified: sha256 == condition). */
692
+ fulfillment?: string;
693
+ }
694
+ /** ILP code used for a client-side fulfillment-verification failure. */
695
+ declare const FULFILLMENT_MISMATCH_CODE = "F99";
696
+ /** Message for a client-side fulfillment-verification failure. */
697
+ declare const FULFILLMENT_MISMATCH_MESSAGE: string;
698
+
643
699
  interface BtpRuntimeClientConfig {
644
700
  btpUrl: string;
645
701
  peerId: string;
@@ -676,23 +732,22 @@ declare class BtpRuntimeClient implements IlpClient {
676
732
  /**
677
733
  * Sends an ILP packet via BTP with auto-reconnect on connection errors.
678
734
  * Satisfies IlpClient interface.
735
+ *
736
+ * `params` may carry a sender-chosen `executionCondition` and an explicit
737
+ * `expiresAt` (toon-client#350); omitting both is the legacy zero-condition
738
+ * path, unchanged. With a non-zero condition the FULFILL preimage is
739
+ * verified (`sha256(fulfillment) == condition`) and a mismatch is surfaced
740
+ * as a failed result — see `mapIlpResponse`.
679
741
  */
680
- sendIlpPacket(params: {
681
- destination: string;
682
- amount: string;
683
- data: string;
684
- timeout?: number;
685
- }): Promise<IlpSendResult>;
742
+ sendIlpPacket(params: IlpSendParams): Promise<IlpSendResult>;
686
743
  /**
687
744
  * Sends a balance proof claim via BTP protocol data, then sends an ILP packet.
688
745
  * Auto-reconnects on connection errors.
746
+ *
747
+ * Sender-chosen `executionCondition` / explicit `expiresAt` semantics are
748
+ * identical to {@link sendIlpPacket}.
689
749
  */
690
- sendIlpPacketWithClaim(params: {
691
- destination: string;
692
- amount: string;
693
- data: string;
694
- timeout?: number;
695
- }, claim: Record<string, unknown>): Promise<IlpSendResult>;
750
+ sendIlpPacketWithClaim(params: IlpSendParams, claim: Record<string, unknown>): Promise<IlpSendResult>;
696
751
  /**
697
752
  * Send a standalone `payment-channel-claim` BTP MESSAGE (no ILP packet
698
753
  * attached). The connector's ClaimReceiver consumes this fire-and-forget
@@ -701,6 +756,14 @@ declare class BtpRuntimeClient implements IlpClient {
701
756
  */
702
757
  sendClaimMessage(claim: Record<string, unknown>): Promise<void>;
703
758
  private _sendClaimMessageOnce;
759
+ /**
760
+ * Build the ILP PREPARE for a send, applying the sender-chosen condition /
761
+ * explicit expiry when provided (toon-client#350) and validating that a
762
+ * non-zero condition is exactly 32 bytes (the OER serializer would
763
+ * otherwise silently zero-fill it, downgrading the packet to the legacy
764
+ * unverified class).
765
+ */
766
+ private buildPrepare;
704
767
  /**
705
768
  * Single-attempt ILP packet send. Reconnects if not connected.
706
769
  */
@@ -792,25 +855,24 @@ declare class HttpIlpClient implements IlpClient {
792
855
  * Send an ILP PREPARE via `POST /ilp` WITHOUT a claim. The connector accepts
793
856
  * this only on free/zero-amount routes; paid writes must use
794
857
  * {@link sendIlpPacketWithClaim}. Satisfies the IlpClient interface.
858
+ *
859
+ * `params` may carry a sender-chosen `executionCondition` and an explicit
860
+ * `expiresAt` (toon-client#350); omitting both is the legacy zero-condition
861
+ * path, unchanged. With a non-zero condition the FULFILL preimage is
862
+ * verified (`sha256(fulfillment) == condition`) and a mismatch is surfaced
863
+ * as a failed result — see {@link mapIlpResponse}.
795
864
  */
796
- sendIlpPacket(params: {
797
- destination: string;
798
- amount: string;
799
- data: string;
800
- timeout?: number;
801
- }): Promise<IlpSendResult>;
865
+ sendIlpPacket(params: IlpSendParams): Promise<IlpSendResult>;
802
866
  /**
803
867
  * Send an ILP PREPARE via `POST /ilp` with the payment-channel claim attached
804
868
  * as the `ILP-Payment-Channel-Claim` header. `claim` is the SAME JSON object
805
869
  * the BTP path attaches as the `payment-channel-claim` protocolData entry —
806
870
  * we base64(JSON.stringify(claim)) it, byte-for-byte identical to BTP.
871
+ *
872
+ * Sender-chosen `executionCondition` / explicit `expiresAt` semantics are
873
+ * identical to {@link sendIlpPacket}.
807
874
  */
808
- sendIlpPacketWithClaim(params: {
809
- destination: string;
810
- amount: string;
811
- data: string;
812
- timeout?: number;
813
- }, claim: unknown): Promise<IlpSendResult>;
875
+ sendIlpPacketWithClaim(params: IlpSendParams, claim: unknown): Promise<IlpSendResult>;
814
876
  /**
815
877
  * Upgrade to a duplex BTP session over the SAME endpoint.
816
878
  *
@@ -838,6 +900,10 @@ declare class HttpIlpClient implements IlpClient {
838
900
  * Map a `200 OK` body (OER FULFILL/REJECT) to an IlpSendResult; map a non-2xx
839
901
  * to a transport error. Per the wire contract, ILP-level rejects arrive as a
840
902
  * 200 + REJECT body — only HTTP non-2xx means a transport-layer failure.
903
+ *
904
+ * When `sentCondition` is non-zero the FULFILL preimage is verified against
905
+ * it; a mismatch yields `accepted: false` (shared `mapIlpResponse` logic,
906
+ * identical to the BTP path).
841
907
  */
842
908
  private mapResponse;
843
909
  private mapTransportError;
@@ -1254,6 +1320,14 @@ declare class ToonClient {
1254
1320
  * matching `destination`,
1255
1321
  * (c) neither -> throw MISSING_CLAIM.
1256
1322
  *
1323
+ * A caller may supply a sender-chosen 32-byte `executionCondition`
1324
+ * (`C = sha256(P)`, one fresh preimage per packet — toon-client#350,
1325
+ * rolling-swap spec §3 R1/R2) and an explicit `expiresAt` (R7). Both are
1326
+ * set on the wire by either transport (HTTP `POST /ilp` and BTP), and on
1327
+ * FULFILL the transport verifies `sha256(fulfillment) == condition` —
1328
+ * a mismatch comes back as `accepted: false` (code F99), never a silent
1329
+ * accept. Omitting them keeps today's zero-condition legacy packet.
1330
+ *
1257
1331
  * @throws {ToonClientError} INVALID_STATE / NO_ILP_TRANSPORT / MISSING_CLAIM
1258
1332
  */
1259
1333
  sendSwapPacket(params: {
@@ -1262,6 +1336,10 @@ declare class ToonClient {
1262
1336
  toonData: Uint8Array;
1263
1337
  timeout?: number;
1264
1338
  claim?: SignedBalanceProof;
1339
+ /** Sender-chosen 32-byte execution condition; absent/zero = legacy. */
1340
+ executionCondition?: Uint8Array;
1341
+ /** Explicit ILP expiry; defaults to `now + timeout` in the transport. */
1342
+ expiresAt?: Date;
1265
1343
  }): Promise<IlpSendResult>;
1266
1344
  /**
1267
1345
  * Build a BTP claim message from a pre-signed balance proof using the
@@ -2809,6 +2887,56 @@ declare function parseFulfillHttpBytes(bytes: Uint8Array): ParsedFulfillHttp;
2809
2887
  */
2810
2888
  declare function parseFulfillHttp(base64Data: string): ParsedFulfillHttp;
2811
2889
 
2890
+ /**
2891
+ * Sender-chosen ILP execution conditions (toon-client#350, rolling-swap
2892
+ * prerequisite — toon-meta#145 §3 R1/R2).
2893
+ *
2894
+ * Contract (normative: connector `docs/local-delivery-fulfillment-contract.md`,
2895
+ * connector#309):
2896
+ * - An ABSENT or ALL-ZERO (32×0x00) `executionCondition` is the LEGACY
2897
+ * class: no hashlock, no verification — today's publish/upload writes.
2898
+ * - Any NON-ZERO condition is SENDER-CHOSEN: the sender mints a fresh
2899
+ * random 32-byte preimage `P` per packet, sets `C = sha256(P)` on the
2900
+ * PREPARE, and on FULFILL MUST verify that the returned fulfillment
2901
+ * hashes back to `C` — a missing/malformed/mismatching fulfillment is a
2902
+ * hard failure (the packet is counted failed, never silently accepted).
2903
+ *
2904
+ * Isomorphic: @noble/hashes only (no Buffer, no node:crypto).
2905
+ */
2906
+ /** Exact byte length of an ILP execution condition / fulfillment preimage. */
2907
+ declare const CONDITION_LENGTH = 32;
2908
+ /** A freshly minted per-packet hashlock: `condition = sha256(preimage)`. */
2909
+ interface ExecutionConditionPair {
2910
+ /** Random 32-byte preimage `P` — reveal is the commit act; never reuse. */
2911
+ preimage: Uint8Array;
2912
+ /** `C = sha256(P)` — goes on the PREPARE's `executionCondition`. */
2913
+ condition: Uint8Array;
2914
+ }
2915
+ /**
2916
+ * Mint a fresh random 32-byte preimage and its sha256 condition (spec R1).
2917
+ * Mint one PER PACKET: a reused preimage lets any observer of packet *i*
2918
+ * fulfill packet *i+1* without the sender's consent.
2919
+ */
2920
+ declare function mintExecutionCondition(): ExecutionConditionPair;
2921
+ /**
2922
+ * True when `condition` selects the LEGACY (unverified) class: absent or
2923
+ * all-zero. The OER wire treats absent and 32×0x00 identically.
2924
+ */
2925
+ declare function isZeroCondition(condition: Uint8Array | undefined): boolean;
2926
+ /**
2927
+ * Validate a caller-supplied execution condition before it goes on the wire.
2928
+ * @throws {Error} When the condition is not exactly 32 bytes — the OER
2929
+ * serializer would otherwise silently zero-fill it, downgrading a
2930
+ * sender-chosen packet to the legacy unverified class.
2931
+ */
2932
+ declare function assertValidCondition(condition: Uint8Array): void;
2933
+ /**
2934
+ * True iff `fulfillment` is exactly 32 bytes and `sha256(fulfillment)`
2935
+ * equals `condition` — the sender-side FULFILL check (spec R6's mirror).
2936
+ * Fail-closed: an undefined/short/long fulfillment never matches.
2937
+ */
2938
+ declare function fulfillmentMatchesCondition(fulfillment: Uint8Array | undefined, condition: Uint8Array): boolean;
2939
+
2812
2940
  /**
2813
2941
  * Settlement info produced by buildSettlementInfo().
2814
2942
  * Extends the core SettlementConfig shape with ilpAddress for client use.
@@ -3326,4 +3454,4 @@ declare function loadKeystore(path: string, password: string): string;
3326
3454
  */
3327
3455
  declare function writeKeystoreFile(path: string, keystore: EncryptedKeystore): void;
3328
3456
 
3329
- export { type BackupPayload, type BalanceProofParams, BtpRuntimeClient, type BtpRuntimeClientConfig, type ChainMetadata, type ChainSigner, ChannelFundingError, ChannelManager, type ClaimMessage, type ClaimResolver, ConnectorError, type DiscoveredIlpPeer, type EVMClaimMessage, type EncryptedKeystore, EvmSigner, type FaucetChain, type FundWalletOptions, type FundWalletResult, type H402FetchOptions, Http402Client, type Http402ClientConfig, HttpConnectorAdmin, type HttpConnectorAdminConfig, HttpIlpClient, type HttpIlpClientConfig, type HttpIlpClientFactory, HttpRuntimeClient, type HttpRuntimeClientConfig, ILP_CLAIM_HEADER, ILP_CLAIM_WRAPPED_HEADER, ILP_PEER_ID_HEADER, type IlpTransportChoice, KeyManager, type KeyManagerConfig, type MinaClaimMessage, type MinaDepositReader, MinaSigner, type MinaSignerOptions, NetworkError, OnChainChannelClient, type OnChainChannelClientConfig, type ParsedFulfillHttp, type ParsedX402Challenge, type PasskeyInfo, type PublishEventResult, type RequestBlobStorageParams, type RequestBlobStorageResult, type RetryOptions, type SelectIlpTransportOptions, type SignedBalanceProof, type SolanaChannelClientOptions, type SolanaClaimMessage, SolanaSigner, type ToonChannelAccept, ToonClient, type ToonClientConfig, ToonClientError, type ToonIdentity, type ToonSigners, type ToonStartResult, ValidationError, type VaultData, type WalletBalance, type WalletBalanceSources, type WalletChainBalances, type WalletTokenAmount, applyDefaults, applyNetworkPresets, buildBackupEvent, buildBackupFilter, buildSettlementInfo, buildStoreWriteEnvelope, decryptMnemonic, defaultFaucetTimeout, deriveFromNsec, deriveFullIdentity, deriveNostrKeyFromMnemonic, encryptMnemonic, extractArweaveTxId, fundWallet, generateKeystore, generateMnemonic, generateRandomIdentity, getNetworkStatus, httpEndpointToBtpUrl, importKeystore, isInsufficientGasError, isPrfSupported, loadKeystore, parseBackupPayload, parseFulfillHttp, parseFulfillHttpBytes, parseHttpResponse, parseX402Body, parseX402Challenge, proxyIlpEndpoint, readDiscoveredIlpPeer, readEvmNativeBalance, readEvmTokenBalance, readMinaBalance, readMinaDepositTotal, readSolanaNativeBalance, readSolanaTokenBalance, readWalletBalances, requestBlobStorage, selectIlpTransport, serializeHttpRequest, validateConfig, validateMnemonic, withRetry, writeKeystoreFile };
3457
+ export { type BackupPayload, type BalanceProofParams, BtpRuntimeClient, type BtpRuntimeClientConfig, CONDITION_LENGTH, type ChainMetadata, type ChainSigner, ChannelFundingError, ChannelManager, type ClaimMessage, type ClaimResolver, ConnectorError, type DiscoveredIlpPeer, type EVMClaimMessage, type EncryptedKeystore, EvmSigner, type ExecutionConditionPair, FULFILLMENT_MISMATCH_CODE, FULFILLMENT_MISMATCH_MESSAGE, type FaucetChain, type FundWalletOptions, type FundWalletResult, type H402FetchOptions, Http402Client, type Http402ClientConfig, HttpConnectorAdmin, type HttpConnectorAdminConfig, HttpIlpClient, type HttpIlpClientConfig, type HttpIlpClientFactory, HttpRuntimeClient, type HttpRuntimeClientConfig, ILP_CLAIM_HEADER, ILP_CLAIM_WRAPPED_HEADER, ILP_PEER_ID_HEADER, type IlpSendParams, type IlpSendResultWithFulfillment, type IlpTransportChoice, KeyManager, type KeyManagerConfig, type MinaClaimMessage, type MinaDepositReader, MinaSigner, type MinaSignerOptions, NetworkError, OnChainChannelClient, type OnChainChannelClientConfig, type ParsedFulfillHttp, type ParsedX402Challenge, type PasskeyInfo, type PublishEventResult, type RequestBlobStorageParams, type RequestBlobStorageResult, type RetryOptions, type SelectIlpTransportOptions, type SignedBalanceProof, type SolanaChannelClientOptions, type SolanaClaimMessage, SolanaSigner, type ToonChannelAccept, ToonClient, type ToonClientConfig, ToonClientError, type ToonIdentity, type ToonSigners, type ToonStartResult, ValidationError, type VaultData, type WalletBalance, type WalletBalanceSources, type WalletChainBalances, type WalletTokenAmount, applyDefaults, applyNetworkPresets, assertValidCondition, buildBackupEvent, buildBackupFilter, buildSettlementInfo, buildStoreWriteEnvelope, decryptMnemonic, defaultFaucetTimeout, deriveFromNsec, deriveFullIdentity, deriveNostrKeyFromMnemonic, encryptMnemonic, extractArweaveTxId, fulfillmentMatchesCondition, fundWallet, generateKeystore, generateMnemonic, generateRandomIdentity, getNetworkStatus, httpEndpointToBtpUrl, importKeystore, isInsufficientGasError, isPrfSupported, isZeroCondition, loadKeystore, mintExecutionCondition, parseBackupPayload, parseFulfillHttp, parseFulfillHttpBytes, parseHttpResponse, parseX402Body, parseX402Challenge, proxyIlpEndpoint, readDiscoveredIlpPeer, readEvmNativeBalance, readEvmTokenBalance, readMinaBalance, readMinaDepositTotal, readSolanaNativeBalance, readSolanaTokenBalance, readWalletBalances, requestBlobStorage, selectIlpTransport, serializeHttpRequest, validateConfig, validateMnemonic, withRetry, writeKeystoreFile };
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;
@@ -4897,6 +4968,14 @@ var ToonClient = class {
4897
4968
  * matching `destination`,
4898
4969
  * (c) neither -> throw MISSING_CLAIM.
4899
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
+ *
4900
4979
  * @throws {ToonClientError} INVALID_STATE / NO_ILP_TRANSPORT / MISSING_CLAIM
4901
4980
  */
4902
4981
  async sendSwapPacket(params) {
@@ -4917,7 +4996,9 @@ var ToonClient = class {
4917
4996
  destination: params.destination,
4918
4997
  amount: String(params.amount),
4919
4998
  data: toBase64(params.toonData),
4920
- timeout: params.timeout ?? 3e4
4999
+ timeout: params.timeout ?? 3e4,
5000
+ ...params.executionCondition ? { executionCondition: params.executionCondition } : {},
5001
+ ...params.expiresAt ? { expiresAt: params.expiresAt } : {}
4921
5002
  },
4922
5003
  claimMessage
4923
5004
  );
@@ -6771,7 +6852,7 @@ import {
6771
6852
  scryptSync,
6772
6853
  createCipheriv,
6773
6854
  createDecipheriv,
6774
- randomBytes
6855
+ randomBytes as randomBytes2
6775
6856
  } from "crypto";
6776
6857
  import { writeFileSync as writeFileSync2, readFileSync as readFileSync2 } from "fs";
6777
6858
  var SCRYPT_N = 2 ** 17;
@@ -6798,8 +6879,8 @@ function encryptMnemonic2(mnemonic, password) {
6798
6879
  if (typeof password !== "string" || password.length === 0) {
6799
6880
  throw new Error("encryptMnemonic: password must be a non-empty string");
6800
6881
  }
6801
- const salt = randomBytes(SALT_LEN);
6802
- const iv = randomBytes(IV_LEN);
6882
+ const salt = randomBytes2(SALT_LEN);
6883
+ const iv = randomBytes2(IV_LEN);
6803
6884
  const key = scryptSync(password, salt, SCRYPT_KEY_LEN, {
6804
6885
  N: SCRYPT_N,
6805
6886
  r: SCRYPT_R,
@@ -6902,10 +6983,13 @@ function writeKeystoreFile(path, keystore) {
6902
6983
  }
6903
6984
  export {
6904
6985
  BtpRuntimeClient,
6986
+ CONDITION_LENGTH,
6905
6987
  ChannelFundingError,
6906
6988
  ChannelManager,
6907
6989
  ConnectorError,
6908
6990
  EvmSigner,
6991
+ FULFILLMENT_MISMATCH_CODE,
6992
+ FULFILLMENT_MISMATCH_MESSAGE,
6909
6993
  GenerativeFallbackRenderer,
6910
6994
  Http402Client,
6911
6995
  HttpConnectorAdmin,
@@ -6930,6 +7014,7 @@ export {
6930
7014
  ValidationError,
6931
7015
  applyDefaults,
6932
7016
  applyNetworkPresets,
7017
+ assertValidCondition,
6933
7018
  buildBackupEvent,
6934
7019
  buildBackupFilter,
6935
7020
  buildConsentRequest,
@@ -6947,6 +7032,7 @@ export {
6947
7032
  encryptMnemonic2 as encryptMnemonic,
6948
7033
  extractArweaveTxId,
6949
7034
  extractUiResource,
7035
+ fulfillmentMatchesCondition,
6950
7036
  fundWallet,
6951
7037
  generateKeystore,
6952
7038
  generateMnemonic,
@@ -6959,7 +7045,9 @@ export {
6959
7045
  isInsufficientGasError,
6960
7046
  isPrfSupported,
6961
7047
  isTrustDowngrade,
7048
+ isZeroCondition,
6962
7049
  loadKeystore,
7050
+ mintExecutionCondition,
6963
7051
  parseBackupPayload,
6964
7052
  parseFulfillHttp,
6965
7053
  parseFulfillHttpBytes,