@toon-protocol/sdk 2.0.1 → 2.2.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.
@@ -0,0 +1,1244 @@
1
+ import { NostrEvent, UnsignedEvent } from 'nostr-tools/pure';
2
+ import { IlpPreparePacket, SwapPair, ToonError } from '@toon-protocol/core';
3
+
4
+ /**
5
+ * NIP-59 Gift Wrap Integration for ILP Packets (Story 12.2)
6
+ *
7
+ * Provides privacy-preserving encoding/decoding of swap packets using the
8
+ * NIP-59 three-layer gift wrap construction (rumor -> seal -> gift wrap).
9
+ * Intermediary peers routing ILP packets see only opaque TOON-encoded binary
10
+ * in the data field -- they cannot determine the event kind, sender identity,
11
+ * or swap intent. Only the destination Swap can unwrap and process.
12
+ *
13
+ * Also provides NIP-44 encryption/decryption for FULFILL claim return path
14
+ * (D12-008), using ephemeral keys so intermediaries on the return path see
15
+ * only opaque ciphertext.
16
+ *
17
+ * @module
18
+ */
19
+
20
+ /** Parameters for {@link wrapSwapPacket}. */
21
+ interface WrapSwapPacketParams {
22
+ /** Unsigned inner event containing swap metadata. */
23
+ rumor: UnsignedEvent;
24
+ /** Sender's secp256k1 secret key. */
25
+ senderSecretKey: Uint8Array;
26
+ /** Swap's compressed hex pubkey (64 chars). */
27
+ recipientPubkey: string;
28
+ }
29
+ /** Result of {@link wrapSwapPacket}. */
30
+ interface WrapSwapPacketResult {
31
+ /** Fully-formed kind:1059 gift wrap event signed by a fresh ephemeral key. */
32
+ giftWrap: NostrEvent;
33
+ /** The ephemeral pubkey used for the outer gift wrap layer. */
34
+ ephemeralPubkey: string;
35
+ }
36
+ /** Parameters for {@link unwrapSwapPacket}. */
37
+ interface UnwrapSwapPacketParams {
38
+ /** A kind:1059 gift wrap event. */
39
+ giftWrap: NostrEvent;
40
+ /** Recipient's (Swap's) secret key. */
41
+ recipientSecretKey: Uint8Array;
42
+ }
43
+ /** Result of {@link unwrapSwapPacket}. */
44
+ interface UnwrapSwapPacketResult {
45
+ /** The decrypted inner rumor (unsigned). */
46
+ rumor: UnsignedEvent;
47
+ /** The sender's real pubkey (extracted from the seal layer). */
48
+ senderPubkey: string;
49
+ }
50
+ /** Parameters for {@link wrapSwapPacketToToon}. */
51
+ interface WrapSwapPacketToToonParams {
52
+ /** Unsigned inner event containing swap metadata. */
53
+ rumor: UnsignedEvent;
54
+ /** Sender's secp256k1 secret key. */
55
+ senderSecretKey: Uint8Array;
56
+ /** Swap's compressed hex pubkey (64 chars). */
57
+ recipientPubkey: string;
58
+ /** ILP destination address. */
59
+ destination: string;
60
+ /** Payment amount in ILP units (bigint). */
61
+ amount: bigint;
62
+ /**
63
+ * Per-packet expiry. Propagated onto the produced PREPARE as an ISO 8601
64
+ * `expiresAt` string (rolling-swap R7 leg ordering). When omitted, the
65
+ * transport applies its default (timeout-derived, ~30s).
66
+ */
67
+ expiresAt?: Date;
68
+ }
69
+ /** Result of {@link wrapSwapPacketToToon}. */
70
+ interface WrapSwapPacketToToonResult {
71
+ /** Ready-to-send ILP PREPARE packet with TOON-encoded gift wrap as data. */
72
+ ilpPrepare: IlpPreparePacket;
73
+ /** The ephemeral pubkey used for the outer gift wrap layer. */
74
+ ephemeralPubkey: string;
75
+ }
76
+ /** Parameters for {@link unwrapSwapPacketFromToon}. */
77
+ interface UnwrapSwapPacketFromToonParams {
78
+ /** The data field from an incoming ILP PREPARE (TOON-encoded gift wrap). */
79
+ toonData: Uint8Array;
80
+ /** Recipient's (Swap's) secret key. */
81
+ recipientSecretKey: Uint8Array;
82
+ }
83
+ /** Parameters for {@link encryptFulfillClaim}. */
84
+ interface EncryptFulfillClaimParams {
85
+ /** The signed claim bytes to encrypt. */
86
+ claimData: Uint8Array;
87
+ /** The original sender's pubkey (recovered from unwrap). */
88
+ senderPubkey: string;
89
+ }
90
+ /** Result of {@link encryptFulfillClaim}. */
91
+ interface EncryptFulfillClaimResult {
92
+ /** NIP-44 encrypted claim bytes. */
93
+ ciphertext: Uint8Array;
94
+ /** The Swap's ephemeral pubkey (included in FULFILL so sender can decrypt). */
95
+ ephemeralPubkey: string;
96
+ }
97
+ /** Parameters for {@link decryptFulfillClaim}. */
98
+ interface DecryptFulfillClaimParams {
99
+ /** The encrypted claim bytes from the FULFILL data field. */
100
+ ciphertext: Uint8Array;
101
+ /** The Swap's ephemeral pubkey from the FULFILL data field. */
102
+ ephemeralPubkey: string;
103
+ /** The sender's (recipient of FULFILL) secret key. */
104
+ recipientSecretKey: Uint8Array;
105
+ }
106
+ /**
107
+ * NIP-59 three-layer gift wrap a swap packet.
108
+ *
109
+ * Constructs a kind:1059 gift wrap event using a fresh ephemeral keypair.
110
+ * The rumor is sealed with the sender's real key, then wrapped with the
111
+ * ephemeral key. Intermediaries see only the ephemeral pubkey.
112
+ *
113
+ * Each invocation generates a fresh ephemeral keypair for forward secrecy
114
+ * and message unlinkability (risk R-006).
115
+ *
116
+ * @throws {GiftWrapError} If the wrap operation fails (invalid keys, crypto failure).
117
+ */
118
+ declare function wrapSwapPacket(params: WrapSwapPacketParams): WrapSwapPacketResult;
119
+ /**
120
+ * Unwrap a NIP-59 gift-wrapped swap packet, recovering the inner rumor
121
+ * and the sender's real pubkey.
122
+ *
123
+ * Performs two-layer decryption: gift wrap -> seal -> rumor.
124
+ * The sender pubkey is extracted from the seal's pubkey field.
125
+ *
126
+ * @throws {GiftWrapError} If kind is not 1059, decryption fails, or structure is malformed.
127
+ */
128
+ declare function unwrapSwapPacket(params: UnwrapSwapPacketParams): UnwrapSwapPacketResult;
129
+ /**
130
+ * Convenience: gift wrap a swap packet, encode to TOON binary, and build
131
+ * an ILP PREPARE packet.
132
+ *
133
+ * This is the path that `streamSwap()` (Story 12.5) will use in its
134
+ * per-packet loop.
135
+ */
136
+ declare function wrapSwapPacketToToon(params: WrapSwapPacketToToonParams): WrapSwapPacketToToonResult;
137
+ /**
138
+ * Convenience: decode TOON binary from an incoming ILP PREPARE data field
139
+ * and unwrap the gift-wrapped swap packet.
140
+ *
141
+ * This is the path that the Swap handler (Story 12.3) will use to process
142
+ * incoming swap packets.
143
+ */
144
+ declare function unwrapSwapPacketFromToon(params: UnwrapSwapPacketFromToonParams): UnwrapSwapPacketResult;
145
+ /**
146
+ * Encrypt a FULFILL claim for the return path (D12-008).
147
+ *
148
+ * Generates a fresh ephemeral keypair and NIP-44 encrypts the claim data.
149
+ * The ephemeral pubkey is included alongside the ciphertext so the sender
150
+ * can decrypt. The ephemeral privkey is discarded after encryption.
151
+ *
152
+ * @throws {GiftWrapError} If claimData is empty or encryption fails.
153
+ */
154
+ declare function encryptFulfillClaim(params: EncryptFulfillClaimParams): EncryptFulfillClaimResult;
155
+ /**
156
+ * Decrypt a FULFILL claim from the return path.
157
+ *
158
+ * Uses the sender's secret key and the Swap's ephemeral pubkey (from the
159
+ * FULFILL data field) to NIP-44 decrypt the claim bytes.
160
+ *
161
+ * @throws {GiftWrapError} If decryption fails (wrong key, malformed ciphertext).
162
+ */
163
+ declare function decryptFulfillClaim(params: DecryptFulfillClaimParams): Uint8Array;
164
+
165
+ /**
166
+ * rfc-0039-style stream receipts (issue #84, rolling-swap spec §7.2).
167
+ *
168
+ * Per fulfilled packet the maker attaches a compact SIGNED receipt to the
169
+ * FULFILL accept-metadata: a monotone cumulative proof of asset-B delivered
170
+ * in the session. The receipt chain is the sender's portable audit/dispute
171
+ * artifact today (it makes the maker-debt bound provable to a third party)
172
+ * and the natural input to a future escrow layer. Receipts are
173
+ * session-scoped via `streamNonce`; the latest receipt supersedes all
174
+ * earlier ones, mirroring the highest-nonce rule for claims.
175
+ *
176
+ * ## Deviations from Interledger rfc-0039 (documented per spec §7.2 — the
177
+ * TOON-native equivalent deliberately does not import the STREAM framing):
178
+ *
179
+ * - **Signature, not HMAC.** rfc-0039 authenticates receipts with
180
+ * HMAC-SHA256 over a pre-shared 32-byte Receipt Secret, so only a party
181
+ * holding the secret can verify. TOON receipts are BIP-340 Schnorr
182
+ * signatures by the maker's receipt key (default: the maker's Nostr
183
+ * identity key — the same `swapPubkey` the sender already discovered via
184
+ * kind:10032), making every receipt verifiable by ANY third party with
185
+ * the maker's pubkey. No Receipt Secret exists or is provisioned.
186
+ * - **Nonce provisioning.** rfc-0039's Verifier generates the Receipt Nonce
187
+ * and communicates it out-of-band (SPSP headers). Here the sender (who is
188
+ * its own verifier today) generates a 16-byte `streamNonce` per
189
+ * `streamSwap()` invocation and communicates it in-band as the rumor's
190
+ * `stream-nonce` tag. Legacy makers ignore the unknown tag.
191
+ * - **Encoding.** JSON object inside the accept-metadata dict rather than a
192
+ * CANONICAL-OER STREAM frame. A deterministic length-prefixed binary
193
+ * encoding ({@link encodeReceiptSigningPayload}) is used ONLY as the
194
+ * signing preimage.
195
+ * - **Fields.** `seq` (maker's per-session fulfill counter — enables hole
196
+ * detection, absent from rfc-0039), `rate` + `rateTimestamp` (the quote
197
+ * tape entry, attested under the maker signature) are added; the STREAM
198
+ * `Stream ID` is dropped (one logical stream per session — the
199
+ * `streamNonce` IS the stream identity); `Total Received` is a decimal
200
+ * string (bigint range) rather than UInt64.
201
+ *
202
+ * rfc-0039 semantics preserved: one receipt per fulfilled packet, each
203
+ * carrying a progressively higher cumulative total; latest supersedes;
204
+ * verification = authenticity check + monotonicity against previously seen
205
+ * receipts for the same nonce.
206
+ *
207
+ * @module
208
+ */
209
+ /** Wire version of the TOON stream receipt. */
210
+ declare const STREAM_RECEIPT_VERSION: 1;
211
+ /** Domain-separation tag prefixed to the receipt signing payload. */
212
+ declare const STREAM_RECEIPT_SIGNING_TAG = "toon.stream-receipt.v1";
213
+ /** The signed fields of a {@link StreamReceipt} (everything except `sig`). */
214
+ interface StreamReceiptFields {
215
+ /** Receipt wire version — always {@link STREAM_RECEIPT_VERSION}. */
216
+ v: typeof STREAM_RECEIPT_VERSION;
217
+ /**
218
+ * Session identifier: 32-char lowercase hex (16 bytes), generated by the
219
+ * sender per stream and echoed by the maker on every receipt.
220
+ */
221
+ streamNonce: string;
222
+ /**
223
+ * Maker's per-session fulfill counter, 1-based, strictly increasing by 1
224
+ * per fulfilled packet. Gaps observed by the sender indicate receipts it
225
+ * never received (hole detection).
226
+ */
227
+ seq: number;
228
+ /**
229
+ * Running total of asset B delivered in this session (target micro-units,
230
+ * decimal string). Monotone non-decreasing in `seq`; the latest receipt
231
+ * supersedes all earlier ones.
232
+ */
233
+ cumulativeDelivered: string;
234
+ /** The maker's quote `R_i` applied to the packet this receipt rode on (decimal string). */
235
+ rate: string;
236
+ /** Unix ms timestamp when the maker's rate source produced {@link rate}. */
237
+ rateTimestamp: number;
238
+ }
239
+ /**
240
+ * A signed stream receipt as carried on the FULFILL accept-metadata under
241
+ * the `receipt` key (rolling-swap spec §7.2).
242
+ */
243
+ interface StreamReceipt extends StreamReceiptFields {
244
+ /**
245
+ * 128-char lowercase hex BIP-340 Schnorr signature by the maker's receipt
246
+ * key over `sha256(encodeReceiptSigningPayload(fields))`.
247
+ */
248
+ sig: string;
249
+ }
250
+ /**
251
+ * Aggregate view of the receipts harvested from one stream — the serialized
252
+ * audit/dispute artifact's in-memory shape. Returned on
253
+ * `StreamSwapResult.receipts` (present on abort too, covering what filled).
254
+ */
255
+ interface StreamReceiptChain {
256
+ /** The sender-generated session nonce receipts were verified against. */
257
+ streamNonce: string;
258
+ /** All verified receipts, sorted by ascending `seq`. */
259
+ receipts: StreamReceipt[];
260
+ /** Highest-`seq` receipt — supersedes all others (rfc-0039 latest-wins). */
261
+ latest?: StreamReceipt;
262
+ /**
263
+ * `latest.cumulativeDelivered`, or `'0'` when no receipt was received —
264
+ * the signed total of asset B the maker attests to having delivered.
265
+ */
266
+ totalDelivered: string;
267
+ /**
268
+ * Missing `seq` values in `[1, latest.seq]`: fulfilled packets whose
269
+ * receipts the sender never received (lost FULFILLs, or maker skips).
270
+ */
271
+ holes: number[];
272
+ }
273
+ /** True iff `value` is a well-formed 32-char lowercase hex stream nonce. */
274
+ declare function isValidStreamNonce(value: unknown): value is string;
275
+ /**
276
+ * Structurally validate an untrusted metadata `receipt` value into a
277
+ * {@link StreamReceipt}. Checks shape ONLY — no signature or monotonicity
278
+ * verification (that is {@link verifyStreamReceipt} / {@link ReceiptChainTracker}).
279
+ *
280
+ * @throws {Error} with a human-readable reason when malformed. Callers
281
+ * (e.g. `decodeFulfillMetadata`) wrap into their own error taxonomy.
282
+ */
283
+ declare function parseStreamReceipt(value: unknown): StreamReceipt;
284
+ /**
285
+ * Deterministic binary encoding of the signed receipt fields — the signing
286
+ * preimage. Layout (all integers big-endian):
287
+ *
288
+ * ```
289
+ * len(tag):u32 || tag(utf8) || v:u8 || streamNonce(16 raw bytes)
290
+ * || seq:u64 || len:u32 || cumulativeDelivered(utf8)
291
+ * || len:u32 || rate(utf8) || rateTimestamp:u64
292
+ * ```
293
+ *
294
+ * Length prefixes remove concatenation ambiguity between the variable-width
295
+ * decimal strings (same rationale as the swap-handler replay packet-id hash).
296
+ */
297
+ declare function encodeReceiptSigningPayload(fields: StreamReceiptFields): Uint8Array;
298
+ /**
299
+ * Sign the receipt fields with the maker's receipt secret key (32-byte
300
+ * secp256k1 scalar — by default the maker's Nostr identity key). Returns
301
+ * the complete {@link StreamReceipt} with the BIP-340 Schnorr `sig` over
302
+ * `sha256(encodeReceiptSigningPayload(fields))`.
303
+ */
304
+ declare function signStreamReceipt(fields: StreamReceiptFields, secretKey: Uint8Array): StreamReceipt;
305
+ /**
306
+ * Verify a receipt's Schnorr signature against the maker's receipt pubkey
307
+ * (64-char lowercase hex x-only key — by default the maker's `swapPubkey`).
308
+ * Authenticity only; monotonicity/session checks live in
309
+ * {@link ReceiptChainTracker}.
310
+ */
311
+ declare function verifyStreamReceipt(receipt: StreamReceipt, makerPubkey: string): boolean;
312
+ /** Outcome of {@link ReceiptChainTracker.add}. */
313
+ type ReceiptAddResult = {
314
+ ok: true;
315
+ } | {
316
+ ok: false;
317
+ /** Machine-readable rejection reason. */
318
+ code: 'BAD_SIGNATURE' | 'WRONG_STREAM_NONCE' | 'DUPLICATE_SEQ' | 'NON_MONOTONIC';
319
+ message: string;
320
+ };
321
+ /**
322
+ * Accumulates and verifies the receipt chain for ONE stream session.
323
+ *
324
+ * Per added receipt it checks, in order:
325
+ * 1. `streamNonce` matches the session (a receipt for another session is
326
+ * worthless as proof for this one),
327
+ * 2. the Schnorr signature verifies against the maker's receipt pubkey,
328
+ * 3. `seq` is not a duplicate,
329
+ * 4. `cumulativeDelivered` is monotone in `seq` against every receipt seen
330
+ * so far: non-decreasing with ascending `seq` (rfc-0039 "progressively
331
+ * higher amount"; non-strict so a zero-target packet — possible under
332
+ * `applyRate` truncation — does not read as maker equivocation).
333
+ *
334
+ * Receipts may be added OUT OF ORDER (adaptive W>1 streams complete in
335
+ * arbitrary order); monotonicity is enforced against both the nearest lower
336
+ * and nearest higher `seq` neighbors.
337
+ */
338
+ declare class ReceiptChainTracker {
339
+ #private;
340
+ constructor(input: {
341
+ streamNonce: string;
342
+ makerPubkey: string;
343
+ });
344
+ get streamNonce(): string;
345
+ /** Number of verified receipts accumulated so far. */
346
+ get size(): number;
347
+ /**
348
+ * Verify + accumulate one receipt. On `{ok: false}` the receipt is NOT
349
+ * added and the chain is unchanged.
350
+ */
351
+ add(receipt: StreamReceipt): ReceiptAddResult;
352
+ /**
353
+ * Snapshot the accumulated chain: sorted receipts, the superseding latest,
354
+ * the signed total delivered, and any `seq` holes in `[1, latest.seq]`.
355
+ */
356
+ chain(): StreamReceiptChain;
357
+ }
358
+ /**
359
+ * Serialize a receipt chain into the portable audit/dispute artifact: a
360
+ * stable, versioned JSON document. Any third party holding the maker's
361
+ * receipt pubkey can re-verify every receipt in it offline (the enclosed
362
+ * receipts are self-contained signed statements).
363
+ */
364
+ declare function serializeReceiptChain(chain: StreamReceiptChain): string;
365
+ /** Per-session receipt issuance state kept by the maker. */
366
+ interface ReceiptSessionState {
367
+ /** Last issued seq (0 = none issued yet). */
368
+ seq: number;
369
+ /** Cumulative asset-B delivered in the session so far (target micro-units). */
370
+ cumulativeDelivered: bigint;
371
+ }
372
+ /**
373
+ * Minimal synchronous store contract for maker-side receipt session state,
374
+ * keyed by `streamNonce`.
375
+ *
376
+ * SYNCHRONOUS by design: the swap handler performs the read-increment-write
377
+ * inside one microtask so concurrent packets in the same session get
378
+ * distinct, gapless `seq` values and consistent cumulative totals. An
379
+ * operator-supplied store that persists (e.g. alongside the claim/channel
380
+ * store, so receipt state survives restarts like claims do) should
381
+ * write-through asynchronously in the background while serving reads from
382
+ * memory. The default is {@link BoundedReceiptSessions} (in-memory LRU).
383
+ */
384
+ interface ReceiptSessionStoreLike {
385
+ get(streamNonce: string): ReceiptSessionState | undefined;
386
+ set(streamNonce: string, state: ReceiptSessionState): unknown;
387
+ delete(streamNonce: string): boolean;
388
+ }
389
+ /**
390
+ * Default ceiling for the maker's receipt-session LRU. Same DoS rationale
391
+ * as `DEFAULT_SEEN_PACKET_IDS_CAP`: bounds memory against an attacker
392
+ * minting unlimited distinct stream nonces. An evicted (or maker-restarted)
393
+ * session restarts at seq 1 — the sender's tracker then rejects the
394
+ * duplicate seq loudly rather than accepting a forked chain.
395
+ */
396
+ declare const DEFAULT_RECEIPT_SESSIONS_CAP = 10000;
397
+ /**
398
+ * Bounded in-memory LRU {@link ReceiptSessionStoreLike}. Access-order
399
+ * eviction: active sessions stay pinned while stale ones age out.
400
+ */
401
+ declare class BoundedReceiptSessions implements ReceiptSessionStoreLike {
402
+ #private;
403
+ constructor(cap?: number);
404
+ get size(): number;
405
+ get cap(): number;
406
+ get(streamNonce: string): ReceiptSessionState | undefined;
407
+ set(streamNonce: string, state: ReceiptSessionState): this;
408
+ delete(streamNonce: string): boolean;
409
+ }
410
+ /**
411
+ * Maker-side: advance the session state for one fulfilled packet and issue
412
+ * the corresponding signed receipt. The read-increment-write against the
413
+ * store happens synchronously (no awaits) so concurrent handler invocations
414
+ * interleave safely.
415
+ *
416
+ * MUST only be called for a packet that is being ACCEPTED — a rejected
417
+ * packet gets no receipt and MUST NOT advance the session.
418
+ */
419
+ declare function issueSessionReceipt(input: {
420
+ sessions: ReceiptSessionStoreLike;
421
+ streamNonce: string;
422
+ /** Asset-B amount delivered by THIS packet (target micro-units). */
423
+ deliveredAmount: bigint;
424
+ /** The quote-tape rate applied to this packet. */
425
+ rate: string;
426
+ /** The quote-tape timestamp for {@link rate}. */
427
+ rateTimestamp: number;
428
+ /** Maker receipt secret key (32-byte secp256k1). */
429
+ secretKey: Uint8Array;
430
+ }): StreamReceipt;
431
+
432
+ /**
433
+ * Adaptive δ/W controller for rolling swaps (issue #83, rolling-swap spec §6).
434
+ *
435
+ * Two knobs, managed separately:
436
+ * - `δ` (delta) — packet size in source micro-units — bounds *per-packet
437
+ * pick-off risk*.
438
+ * - `W` (window) — max unfulfilled packets in flight — bounds *timing /
439
+ * liveness risk* and the worst-case unrecovered exposure `δ·W`.
440
+ *
441
+ * Normative behavior implemented here (spec §6):
442
+ * - **The cap.** `delta_cap = ε / (v·τ)`, recomputed per packet from measured
443
+ * state, and `δ ≤ delta_cap` always. δ and `delta_cap` are fractions of the
444
+ * session's remaining notional; `v·τ` is the expected fractional rate drift
445
+ * while one packet is in flight; ε is the per-packet slippage budget as a
446
+ * fraction of the maker's advertised half-spread (default `ε = 0.5 ×
447
+ * halfSpread`). ε is spread-denominated, never an absolute rate.
448
+ * - **Inputs are measured, not trusted.** `v` is an EWMA of
449
+ * `abs(R_i − R_{i−1})/R_{i−1}` per second read off the quote tape
450
+ * (`PacketProgress.rate` / `rateTimestamp`, issue #82); `τ` is an EWMA of
451
+ * observed round-trip times. Both update on every observed packet.
452
+ * - **Asymmetric adjustment, one knob per step.** On a shrink signal (a
453
+ * `stale_rate` reject, any other reject/verification failure, or realized
454
+ * per-packet slip `> ε`): multiplicative — `δ ← max(δ_min, δ/2)`; if the
455
+ * signal was a timeout/expiry, `W ← max(1, ⌈W/2⌉)` instead. On a clean
456
+ * streak of `K = 16` consecutive fulfills: additive — `δ ← min(caps, δ +
457
+ * δ_0)` or `W ← min(W_max, W + 1)`, alternating, never both in one step.
458
+ * - **Cold start ramps.** With no persisted state for the tuple:
459
+ * `δ_0 = min(delta_cap, notional/256, maker maxAmount)`, `W_0 = 1`. Until
460
+ * the first shrink signal ever observed for the tuple, the δ widen step is
461
+ * multiplicative (`δ ← min(caps, 2δ)` per clean streak) — slow-start —
462
+ * dropping to additive permanently after the first loss event.
463
+ * - **State is per-(chain, maker, pair) and persisted** via a pluggable
464
+ * {@link SwapControllerStateStore} (the SDK is isomorphic; the JSON-file
465
+ * implementation is Node-only and lazily imports `node:fs`).
466
+ *
467
+ * INVARIANT (spec §5): the controller is an *efficiency* mechanism only. It
468
+ * never sees, computes, or relaxes the `minExchangeRate` floor — the floor
469
+ * check in `stream-swap.ts` runs before the controller observes anything and
470
+ * consults nothing but the floor itself. A calm (or adversarially painted)
471
+ * tape can widen δ up to the caps, but can never worsen the sender's declared
472
+ * worst case. NOTE: the adversarial-quote-tape question (toon-meta#146 — a
473
+ * maker quoting calm to coax δ wide, then gapping one large packet) is open;
474
+ * its resolution may tighten the cap logic here. The floor plus the absolute
475
+ * `maxPacketAmount` cap are the uncompromising backstops in the meantime.
476
+ *
477
+ * @module
478
+ */
479
+
480
+ /**
481
+ * Error thrown for invalid adaptive-controller configuration or a
482
+ * persistence-layer failure surfaced through {@link SwapControllerStateStore}.
483
+ */
484
+ declare class SwapControllerError extends ToonError {
485
+ constructor(message: string, cause?: Error);
486
+ }
487
+ /**
488
+ * Persisted per-(chain, maker, pair) controller state (spec §6 value shape
489
+ * `{delta, W, vEwma, tauEwma, cleanStreak, everShrunk, updatedAt}` plus the
490
+ * additive `lastWidened` alternation marker).
491
+ *
492
+ * JSON-serializable by construction: `delta` is a decimal string because
493
+ * `bigint` does not survive `JSON.stringify`.
494
+ */
495
+ interface SwapControllerState {
496
+ /** Schema version for forward migration. Currently `1`. */
497
+ v: 1;
498
+ /**
499
+ * Current ramp value of δ in source micro-units (decimal string).
500
+ * `'0'` means "not yet initialized" — the first `nextDelta()` call seeds it
501
+ * with the cold-start `δ_0`.
502
+ */
503
+ delta: string;
504
+ /** In-flight window W: max unfulfilled packets outstanding. Always ≥ 1. */
505
+ W: number;
506
+ /** EWMA of `abs(ΔR)/R` per second, measured from the quote tape. */
507
+ vEwma: number;
508
+ /** EWMA of observed packet round-trip time, in seconds. */
509
+ tauEwma: number;
510
+ /** Consecutive clean fulfills since the last knob adjustment. */
511
+ cleanStreak: number;
512
+ /**
513
+ * True once ANY shrink signal has ever been observed for this tuple.
514
+ * Ends the multiplicative slow-start widen ramp permanently (spec §6).
515
+ */
516
+ everShrunk: boolean;
517
+ /** Which knob the most recent widen step adjusted (alternation marker). */
518
+ lastWidened: 'delta' | 'window';
519
+ /** Unix ms of the last state mutation. */
520
+ updatedAt: number;
521
+ }
522
+ /** Runtime shape check for a (possibly foreign) persisted state blob. */
523
+ declare function isSwapControllerState(value: unknown): value is SwapControllerState;
524
+ /**
525
+ * Canonical persistence key for a controller tuple (spec §6):
526
+ * `${chain}:${makerPubkey}:${from}:${to}` with `from`/`to` as
527
+ * `assetCode@chain` so cross-chain pairs sharing an asset code stay distinct.
528
+ *
529
+ * The leading `chain` segment is the SOURCE chain — per-tuple state is how
530
+ * the same code runs fast on Base and cautious on Mina.
531
+ */
532
+ declare function swapControllerStateKey(params: {
533
+ makerPubkey: string;
534
+ pair: SwapPair;
535
+ }): string;
536
+ /**
537
+ * Pluggable persistence seam for controller state, keyed by
538
+ * {@link swapControllerStateKey}. The SDK is isomorphic, so the interface is
539
+ * environment-neutral (same pattern as `WorkflowEventStore`); pick
540
+ * {@link JsonFileSwapControllerStateStore} on Node or supply your own
541
+ * (e.g. IndexedDB / daemon-side store in toon-client).
542
+ */
543
+ interface SwapControllerStateStore {
544
+ load(key: string): Promise<SwapControllerState | undefined>;
545
+ save(key: string, state: SwapControllerState): Promise<void>;
546
+ }
547
+ /** Volatile in-memory store — the default when no store is configured. */
548
+ declare class InMemorySwapControllerStateStore implements SwapControllerStateStore {
549
+ private readonly states;
550
+ load(key: string): Promise<SwapControllerState | undefined>;
551
+ save(key: string, state: SwapControllerState): Promise<void>;
552
+ }
553
+ /**
554
+ * Node-only JSON-file store (the toon-client `JsonFileChannelStore` pattern —
555
+ * controller state persists beside the channel store). One JSON file holds a
556
+ * `{ [key]: SwapControllerState }` map; writes are atomic
557
+ * (temp-file + rename). `node:fs` / `node:path` are imported lazily so
558
+ * bundling this module for the browser stays safe as long as the class is
559
+ * not instantiated there.
560
+ */
561
+ declare class JsonFileSwapControllerStateStore implements SwapControllerStateStore {
562
+ private readonly filePath;
563
+ constructor(filePath: string);
564
+ private readAll;
565
+ load(key: string): Promise<SwapControllerState | undefined>;
566
+ save(key: string, state: SwapControllerState): Promise<void>;
567
+ }
568
+ /**
569
+ * Resolution class of one packet, as measured by the sender (spec §6):
570
+ *
571
+ * - `'fulfill'` — packet fulfilled and its claim accepted. Clean unless the
572
+ * realized slip exceeds ε (computed internally from `rate` +
573
+ * `sourceAmount` + `targetAmount`), in which case it is a shrink signal.
574
+ * - `'reject-stale'` — maker staleness reject (`T99 stale_rate`, spec §4).
575
+ * Shrink signal → δ halves.
576
+ * - `'reject'` — any other reject or verification failure (R5-class: floor
577
+ * breach, recipient substitution, bad claim). Shrink signal → δ halves.
578
+ * - `'timeout'` — packet expiry / transport timeout. Shrink signal → W
579
+ * halves (timing risk, not pricing risk).
580
+ * - `'error'` — transport or decode error. Shrink signal → δ halves.
581
+ */
582
+ type PacketResolution = 'fulfill' | 'reject-stale' | 'reject' | 'timeout' | 'error';
583
+ /** One per-packet observation fed to {@link AdaptiveDeltaController.observe}. */
584
+ interface PacketObservation {
585
+ /** Resolution class for this packet (see {@link PacketResolution}). */
586
+ resolution: PacketResolution;
587
+ /** Measured round-trip time for this packet in ms (send → resolve). */
588
+ rttMs?: number;
589
+ /** Quote-tape rate `R_i` for this packet (decimal string, issue #82). */
590
+ rate?: string;
591
+ /** Unix ms when the maker's rate source produced {@link rate}. */
592
+ rateTimestamp?: number;
593
+ /** Source amount sent for this packet (micro-units). Used for slip. */
594
+ sourceAmount?: bigint;
595
+ /** Delivered target amount (micro-units). Used for slip. */
596
+ targetAmount?: bigint;
597
+ /**
598
+ * Remaining session notional AFTER this packet (source micro-units).
599
+ * When provided, widen steps are additionally clamped to the current
600
+ * `delta_cap` fraction of it (spec §6 `δ ← min(delta_cap, δ + δ_0)`).
601
+ * `nextDelta()` re-enforces the cap at issuance regardless.
602
+ */
603
+ remaining?: bigint;
604
+ }
605
+ /**
606
+ * The seam `streamSwap` consumes (kept structural so alternative controller
607
+ * implementations — or a test double — can be plugged in).
608
+ */
609
+ interface StreamSwapAdaptiveController {
610
+ /**
611
+ * Decide the size of the next packet, in source micro-units, given the
612
+ * session's remaining notional. MUST return a value in `[1, remaining]`
613
+ * (callers clamp defensively). Enforces `δ ≤ delta_cap` at issuance.
614
+ */
615
+ nextDelta(remaining: bigint): bigint;
616
+ /** Current in-flight window W (max unfulfilled packets outstanding, ≥ 1). */
617
+ readonly window: number;
618
+ /** Feed one packet observation. May persist state (hence async). */
619
+ observe(observation: PacketObservation): void | Promise<void>;
620
+ }
621
+ /** Configuration for {@link AdaptiveDeltaController.create}. */
622
+ interface AdaptiveDeltaControllerConfig {
623
+ /** Maker's 64-char hex pubkey — part of the persistence key. */
624
+ makerPubkey: string;
625
+ /** The pair being executed — chain + asset parts of the persistence key. */
626
+ pair: SwapPair;
627
+ /**
628
+ * Maker's advertised two-sided spread as a fraction (e.g. `0.004` = 40 bps).
629
+ * ε is denominated off this — NEVER an absolute rate (spec §6): the
630
+ * half-spread self-calibrates ε per chain and per maker. Today the spread
631
+ * is caller-supplied (from the maker's board / RFQ response once toon#145's
632
+ * RFQ lands); there is deliberately no default — an invented spread would
633
+ * rot and silently mis-size ε.
634
+ */
635
+ advertisedSpread: number;
636
+ /**
637
+ * ε as a fraction of the advertised HALF-spread. Default `0.5`
638
+ * (spec §6 default `ε = 0.5 × halfSpread`).
639
+ */
640
+ epsilonHalfSpreadFraction?: number;
641
+ /**
642
+ * Absolute per-packet ceiling in source micro-units (the maker's advertised
643
+ * `maxAmount`). Applied to δ at issuance AND to every widen step — the
644
+ * uncompromising absolute cap alongside the measured `v·τ` bound.
645
+ */
646
+ maxPacketAmount?: bigint;
647
+ /** Floor on δ in source micro-units (`δ_min`). Default `1n`. */
648
+ minPacketAmount?: bigint;
649
+ /** Ceiling on W (`W_max`). Default `8`. */
650
+ maxWindow?: number;
651
+ /** Clean-fulfill streak length K per widen step. Default `16` (spec §6). */
652
+ cleanStreakLength?: number;
653
+ /**
654
+ * Cold-start divisor: `δ_0 = notional / coldStartDivisor` (further clamped
655
+ * by `delta_cap` and `maxPacketAmount`). Default `256` (spec §6).
656
+ */
657
+ coldStartDivisor?: number;
658
+ /** EWMA smoothing factor α for both `v` and `τ`, in (0, 1]. Default `0.2`. */
659
+ ewmaAlpha?: number;
660
+ /**
661
+ * Pluggable persistence. Default: a fresh volatile
662
+ * {@link InMemorySwapControllerStateStore} (state survives the session
663
+ * only). Pass {@link JsonFileSwapControllerStateStore} (Node) or a custom
664
+ * store to persist ramp/trust across swaps.
665
+ */
666
+ store?: SwapControllerStateStore;
667
+ /** Injectable clock for deterministic tests. Default `Date.now`. */
668
+ now?: () => number;
669
+ }
670
+ /**
671
+ * Per-(chain, maker, pair) adaptive δ/W controller (spec §6). Construct via
672
+ * {@link AdaptiveDeltaController.create} (async: loads persisted state).
673
+ *
674
+ * Concurrency note: one instance drives one swap session. Running two
675
+ * concurrent sessions against the same tuple is last-write-wins on the
676
+ * persisted state (same as concurrent channel use).
677
+ */
678
+ declare class AdaptiveDeltaController implements StreamSwapAdaptiveController {
679
+ /** Persistence key for this tuple (see {@link swapControllerStateKey}). */
680
+ readonly key: string;
681
+ private readonly store;
682
+ private readonly pair;
683
+ private readonly epsilon;
684
+ private readonly maxPacketAmount;
685
+ private readonly minPacketAmount;
686
+ private readonly maxWindow;
687
+ private readonly cleanStreakLength;
688
+ private readonly coldStartDivisor;
689
+ private readonly alpha;
690
+ private readonly now;
691
+ private readonly stateInternal;
692
+ /** δ in-memory as bigint (mirrors `stateInternal.delta`). */
693
+ private delta;
694
+ /** Cold-start δ_0 — also the additive widen increment. Set on first nextDelta(). */
695
+ private delta0;
696
+ /** Previous tape entry for the per-second volatility measurement. */
697
+ private lastRate;
698
+ private lastRateTimestamp;
699
+ private constructor();
700
+ /**
701
+ * Create a controller for one swap session: loads the persisted state for
702
+ * the tuple from the store, or starts cold (`δ` unset until the first
703
+ * `nextDelta()`, `W = 1`).
704
+ */
705
+ static create(config: AdaptiveDeltaControllerConfig): Promise<AdaptiveDeltaController>;
706
+ /** Defensive snapshot of the current controller state. */
707
+ get state(): SwapControllerState;
708
+ /** Current in-flight window W. */
709
+ get window(): number;
710
+ /**
711
+ * Current `delta_cap = ε/(v·τ)` as a fraction of remaining notional.
712
+ * `Infinity` while `v·τ` has no measurement yet (cold tape) — the
713
+ * cold-start `δ_0` and absolute caps bound δ in that regime.
714
+ */
715
+ get deltaCapFraction(): number;
716
+ /** `delta_cap` in absolute source micro-units for a given remaining notional. */
717
+ private deltaCapAbsolute;
718
+ /**
719
+ * Decide the next packet size (source micro-units) for a session with
720
+ * `remaining` notional left. Enforces, in order: the measured
721
+ * `delta_cap = ε/(v·τ)` bound, the absolute `maxPacketAmount` cap, the
722
+ * remaining notional, and the `minPacketAmount` floor. Seeds the
723
+ * cold-start `δ_0 = min(delta_cap, notional/256, maxPacketAmount)` on
724
+ * first call.
725
+ */
726
+ nextDelta(remaining: bigint): bigint;
727
+ /**
728
+ * Feed one packet observation: updates the measured EWMAs (`v`, `τ`),
729
+ * applies at most ONE knob adjustment (asymmetric: multiplicative shrink /
730
+ * additive widen), and persists the state.
731
+ */
732
+ observe(observation: PacketObservation): Promise<void>;
733
+ /**
734
+ * Realized per-packet slip as a fraction: shortfall of the delivered
735
+ * target amount vs the amount implied by the packet's own tape rate `R_i`
736
+ * (spec §7.1 `Δcumulative` vs `⌊δ · R_i⌋` cross-check). `0` when the
737
+ * inputs are unavailable or delivery met the tape.
738
+ */
739
+ private realizedSlip;
740
+ }
741
+
742
+ /**
743
+ * Sender-side `streamSwap()` API (Story 12.5).
744
+ *
745
+ * Drives the sender side of the Token Swap Primitive: chunks a total source
746
+ * amount into N sender-chosen packets, wraps each with `wrapSwapPacketToToon()`
747
+ * using a fresh ephemeral gift-wrap key per packet (D12-003 / risk R-006),
748
+ * sends each via `ToonClient.sendSwapPacket(...)`, decrypts each FULFILL's
749
+ * NIP-44-encrypted claim with `decryptFulfillClaim()`, accumulates the
750
+ * decrypted claims into an ordered array, invokes a per-packet rate monitoring
751
+ * callback, and supports pause / resume / stop / AbortSignal so the sender
752
+ * can abort when rate drifts past tolerance.
753
+ *
754
+ * Composition story: consumes Stories 12.1 (SwapPair type), 12.2 (gift wrap
755
+ * primitives), 12.3 (handler wire contract). Produces `AccumulatedClaim[]`
756
+ * consumed by Story 12.6 (`buildSettlementTx()`).
757
+ *
758
+ * Design decisions:
759
+ * - `streamSwap()` does NOT throw on mid-stream failure. Caller gets
760
+ * `StreamSwapResult` with `state` and `abortReason`. Only construction-time
761
+ * validation throws synchronously.
762
+ * - `streamSwap()` does NOT retry individual packets. BTP-level connection
763
+ * retries are handled inside `BtpRuntimeClient`; application-layer retry
764
+ * (e.g., T04 insufficient inventory) is a future-story concern.
765
+ * - Rate deviation math stays BigInt throughout. The `effectiveRate` on
766
+ * `PacketProgress` is a display-only `number` (Epic 11 retro MAX_SAFE_INTEGER
767
+ * guard).
768
+ *
769
+ * @module
770
+ */
771
+
772
+ /** Minimal `IlpSendResult` shape — mirrors `packages/client/src/types.ts`. */
773
+ interface IlpSendResultLike {
774
+ accepted: boolean;
775
+ data?: string;
776
+ code?: string;
777
+ message?: string;
778
+ }
779
+ /**
780
+ * Minimal `SignedBalanceProof` structural type.
781
+ *
782
+ * We avoid importing from `@toon-protocol/client` so the SDK package does not
783
+ * gain a runtime dep on the client package. The sender layer only forwards
784
+ * this opaquely to `ToonClient.sendSwapPacket`.
785
+ */
786
+ type SignedBalanceProofLike = any;
787
+ /**
788
+ * The subset of `ToonClient` that `streamSwap()` requires. This narrow shape
789
+ * exists so that consumers can pass a real `ToonClient` OR a mock without
790
+ * importing the whole class.
791
+ */
792
+ interface StreamSwapClient {
793
+ sendSwapPacket(params: {
794
+ destination: string;
795
+ amount: bigint;
796
+ toonData: Uint8Array;
797
+ timeout?: number;
798
+ claim?: SignedBalanceProofLike;
799
+ /**
800
+ * Explicit per-packet PREPARE expiry. When set, the transport MUST use
801
+ * exactly this expiry on the wire instead of deriving one from the
802
+ * request timeout. Optional so existing `ToonClient` implementations
803
+ * remain structurally compatible (they ignore the extra field until
804
+ * the client-transport work lands).
805
+ */
806
+ expiresAt?: Date;
807
+ }): Promise<IlpSendResultLike>;
808
+ getPublicKey(): string;
809
+ }
810
+ /**
811
+ * Parameters for {@link streamSwap} / {@link streamSwapControlled}.
812
+ *
813
+ * @stable — downstream Stories 12.6 and 12.8 depend on this shape.
814
+ */
815
+ interface StreamSwapParams {
816
+ /** SDK client with BTP wiring (see {@link StreamSwapClient}). */
817
+ client: StreamSwapClient;
818
+ /** Swap's 64-char lowercase hex pubkey (recipient of gift wrap). */
819
+ swapPubkey: string;
820
+ /** Swap's ILP destination address (e.g., 'g.toon.swap1'). */
821
+ swapIlpAddress: string;
822
+ /** The `SwapPair` being executed (from kind:10032 discovery, Story 12.1). */
823
+ pair: SwapPair;
824
+ /** Sender's 32-byte secp256k1 secret key. Used for seal signing AND FULFILL decryption. */
825
+ senderSecretKey: Uint8Array;
826
+ /**
827
+ * Sender's chain-specific payout address for `pair.to.chain` (Story 12.9 AC-4).
828
+ *
829
+ * REQUIRED. The Nostr `senderPubkey` (identity layer) cannot be used as the
830
+ * on-chain `recipient` in a balance-proof because they are cryptographically
831
+ * independent keys (D12-011). For `evm:*` chains this must be a 20-byte
832
+ * lowercased `0x`-prefixed hex address; for `solana:*` a base58 pubkey that
833
+ * decodes to 32 bytes; for `mina:*` a base58 public-key string. The value
834
+ * is validated at `streamSwapControlled()` entry via `validateChainAddress`
835
+ * and echoed on every rumor as the `chain-recipient` tag (AC-6).
836
+ */
837
+ chainRecipient: string;
838
+ /** Total source-asset amount to swap (source micro-units). */
839
+ totalAmount: bigint;
840
+ /** Even-split packet count. EXACTLY ONE of this, `packetAmounts`, or `controller` is required. */
841
+ packetCount?: number;
842
+ /** Explicit per-packet amounts. EXACTLY ONE of this, `packetCount`, or `controller` is required. */
843
+ packetAmounts?: readonly bigint[];
844
+ /**
845
+ * Adaptive δ/W controller (issue #83, rolling-swap spec §6). EXACTLY ONE
846
+ * of this, `packetCount`, or `packetAmounts` is required.
847
+ *
848
+ * When set, packetization is DYNAMIC: the static even split is replaced by
849
+ * per-packet sizing from `controller.nextDelta(remaining)` (δ, capped by
850
+ * the measured `ε/(v·τ)` bound), and up to `controller.window` (W) packets
851
+ * are kept in flight concurrently. After every packet resolves, the
852
+ * controller receives a {@link PacketObservation} (realized rate, tape
853
+ * entry, RTT, resolution class) so δ and W adapt across the stream —
854
+ * multiplicative shrink on stale/slip/reject, additive widen on clean
855
+ * streaks.
856
+ *
857
+ * Adaptive-mode contract differences (each N/A to the legacy paths):
858
+ * - `PacketProgress.total` is the number of packets scheduled SO FAR (the
859
+ * final count is unknown upfront).
860
+ * - With `W > 1`, `onPacket` fires in packet COMPLETION order, not strict
861
+ * index order.
862
+ * - On a mid-stream halt (floor breach, stop, abort), already-sent
863
+ * in-flight packets are drained and their claims harvested before the
864
+ * result resolves.
865
+ *
866
+ * INVARIANT: the controller only tightens/loosens δ and W. It is consulted
867
+ * strictly AFTER the `minExchangeRate` floor check and can never relax it.
868
+ */
869
+ controller?: StreamSwapAdaptiveController;
870
+ /** Source-asset balance proof claim. Required unless ChannelManager is wired. */
871
+ claim?: SignedBalanceProofLike;
872
+ /** Rate monitoring callback (fires after each accepted FULFILL). */
873
+ onPacket?: RateMonitorCallback;
874
+ /** Rate deviation threshold (decimal, e.g., 0.02 = 2%). */
875
+ rateDeviationThreshold?: number;
876
+ /**
877
+ * Hard floor on the per-packet exchange rate (issue #82, rfc-0029
878
+ * `minExchangeRate` semantics; rolling-swap spec §5). Decimal string in
879
+ * `SwapPair.rate` format (target whole-units per source whole-unit),
880
+ * strictly positive.
881
+ *
882
+ * When set:
883
+ * - The quote tape (`rate` + `rateTimestamp` on each FULFILL's metadata)
884
+ * becomes REQUIRED: a fulfilled packet whose metadata is missing the
885
+ * tape is a loud per-packet `FULFILL_DECODE_FAILED` error, never a
886
+ * silent drop.
887
+ * - Each fulfilled packet is checked BEFORE its claim is decrypted or
888
+ * accumulated: if the maker's tape rate `R_i` is below the floor, OR
889
+ * the delivered `targetAmount` is below `applyRate(sourceAmount,
890
+ * minExchangeRate)`, the packet is recorded as a rejection with code
891
+ * `BELOW_FLOOR` and the stream halts with `abortReason: 'below-floor'`.
892
+ * A violating packet NEVER accumulates into `claims[]`.
893
+ *
894
+ * This is the safety mechanism, deliberately independent of (and never
895
+ * relaxed by) the soft `rateDeviationThreshold` monitor, the `onPacket`
896
+ * callback, or any future adaptive-controller signal: a calm tape must
897
+ * not be able to talk the sender into a worse worst case.
898
+ *
899
+ * When omitted, behavior is unchanged (legacy back-compat): the tape is
900
+ * optional and only the soft deviation monitor applies.
901
+ */
902
+ minExchangeRate?: string;
903
+ /**
904
+ * Maker receipt verification key (issue #84, rfc-0039 stream receipts;
905
+ * rolling-swap spec §7.2): 64-char lowercase hex x-only pubkey each
906
+ * per-fulfill receipt's BIP-340 signature is verified against. Defaults
907
+ * to `swapPubkey` (the maker identity key — the swap handler's default
908
+ * receipt signer). Set when the maker provisioned a dedicated receipt key
909
+ * (`CreateSwapHandlerConfig.receiptSecretKey`, e.g. the swap#47 coupled
910
+ * engine's chain-B claim signer key).
911
+ */
912
+ receiptPubkey?: string;
913
+ /**
914
+ * When true, every fulfilled packet MUST carry a verifiable receipt: a
915
+ * receipt-less FULFILL is recorded as a `RECEIPT_MISSING` rejection and
916
+ * the stream halts with `abortReason: 'receipt-invalid'` (a maker that
917
+ * doesn't attest deliveries cannot be audited, so keep the exposure at
918
+ * zero). When omitted/false, receipt-less fulfills from legacy makers
919
+ * degrade gracefully: the claim still accumulates, `result.receipts`
920
+ * is simply empty. A receipt that is PRESENT but fails verification
921
+ * (tampered, wrong key, wrong session, non-monotone, duplicate seq) is
922
+ * ALWAYS a loud `RECEIPT_INVALID` rejection + halt, regardless of this
923
+ * flag — the packet's claim is never accumulated.
924
+ */
925
+ requireReceipts?: boolean;
926
+ /** Per-packet timeout in ms. Default 30000. */
927
+ packetTimeoutMs?: number;
928
+ /**
929
+ * Per-packet PREPARE expiry window in ms. When set, each packet is sent
930
+ * with `expiresAt = now + packetExpiryMs` (computed at send time) so a
931
+ * stalled packet expires deterministically and releases its in-flight
932
+ * slot (rolling-swap R7). When omitted, behavior is unchanged: the
933
+ * transport derives the expiry from its timeout (back-compat default).
934
+ */
935
+ packetExpiryMs?: number;
936
+ /** Optional AbortSignal. */
937
+ signal?: AbortSignal;
938
+ /**
939
+ * Optional pino-compatible logger. Defaults to a no-op.
940
+ *
941
+ * Note: `streamSwap` calls each method with a single structured-event object
942
+ * (e.g., `logger.warn({ event: 'stream_swap.packet_rejected', code, ... })`).
943
+ * Pino accepts this form (the object is logged as top-level fields and the
944
+ * message is left empty); other loggers that expect `(msg, meta)` should
945
+ * wrap accordingly.
946
+ */
947
+ logger?: {
948
+ debug: (...a: unknown[]) => void;
949
+ info: (...a: unknown[]) => void;
950
+ warn: (...a: unknown[]) => void;
951
+ error: (...a: unknown[]) => void;
952
+ };
953
+ }
954
+ /**
955
+ * Rate monitoring callback. Fires after each successful FULFILL decryption,
956
+ * before the next packet is sent. If the callback throws or rejects, the
957
+ * stream is treated as stopped.
958
+ */
959
+ type RateMonitorCallback = (progress: PacketProgress) => void | Promise<void>;
960
+ /**
961
+ * Progress payload delivered to {@link RateMonitorCallback}. Frozen to
962
+ * prevent callback mutation.
963
+ */
964
+ interface PacketProgress {
965
+ /** 0-indexed packet number within this streamSwap invocation. */
966
+ index: number;
967
+ /**
968
+ * Total number of packets scheduled. In adaptive mode (`controller` set)
969
+ * packet sizing is dynamic, so this is the number scheduled SO FAR.
970
+ */
971
+ total: number;
972
+ /** Source-asset amount sent for this packet (micro-units). */
973
+ sourceAmount: bigint;
974
+ /** Target-asset claim amount derived for this packet (micro-units). */
975
+ targetAmount: bigint;
976
+ /** Rate advertised on the `SwapPair` at swap start (decimal string). */
977
+ advertisedRate: string;
978
+ /** Effective rate for this packet as JS Number (target / source in whole units). Display-only. */
979
+ effectiveRate: number;
980
+ /** Absolute deviation from advertisedRate as a decimal (e.g., 0.0125 = 1.25%). */
981
+ rateDeviation: number;
982
+ /** Cumulative source sent across accepted packets so far (including this one). */
983
+ cumulativeSource: bigint;
984
+ /** Cumulative target received so far (including this one). */
985
+ cumulativeTarget: bigint;
986
+ /**
987
+ * Quote-tape entry (issue #82, rolling-swap spec §7.1): the maker's fresh
988
+ * rate `R_i` actually applied to THIS packet, as reported on the FULFILL
989
+ * metadata. Present iff the maker emitted the tape. The sequence of these
990
+ * values across packets, in `index` order, IS the price tape the adaptive
991
+ * controller (toon#83) consumes.
992
+ */
993
+ rate?: string;
994
+ /** Unix ms timestamp when the maker's rate source produced {@link rate}. Present iff `rate` is. */
995
+ rateTimestamp?: number;
996
+ /**
997
+ * Verified per-fulfill stream receipt (issue #84, rolling-swap spec §7.2).
998
+ * Present iff the maker emitted a receipt AND it verified (signature,
999
+ * session nonce, monotonicity) — an invalid receipt halts the stream
1000
+ * before the callback ever fires for that packet.
1001
+ */
1002
+ receipt?: StreamReceipt;
1003
+ /** Controller state at callback time. */
1004
+ state: 'running' | 'paused' | 'stopped';
1005
+ }
1006
+ /**
1007
+ * An accumulated claim successfully harvested from a single packet.
1008
+ *
1009
+ * @stable — Story 12.6 (`buildSettlementTx()`) depends on this shape.
1010
+ * Breaking changes require a coordinated migration.
1011
+ *
1012
+ * Story 12.6 ADDITIVE extension: the settlement-context fields
1013
+ * `channelId`, `nonce`, `cumulativeAmount`, `recipient`, and
1014
+ * `swapSignerAddress` are marked optional (`?:`) for one story-cycle of
1015
+ * backward compat but are REQUIRED in practice: Story 12.6's
1016
+ * `buildSettlementTx()` throws `MISSING_SETTLEMENT_METADATA` when any of
1017
+ * these are absent.
1018
+ */
1019
+ interface AccumulatedClaim {
1020
+ /** 0-indexed position in the swap's packet stream. */
1021
+ packetIndex: number;
1022
+ /** Source-asset amount sent for this packet (micro-units). */
1023
+ sourceAmount: bigint;
1024
+ /**
1025
+ * Target-asset amount claimed (micro-units).
1026
+ *
1027
+ * **Source of truth caveat:** This is the expected target amount computed
1028
+ * by `applyRate(pair.rate)`. The actual signed-claim amount lives inside
1029
+ * `claimBytes`; Story 12.6 is responsible for parsing `claimBytes` per
1030
+ * chain and verifying the on-wire signed amount equals this expected amount.
1031
+ */
1032
+ targetAmount: bigint;
1033
+ /** Decrypted signed claim bytes. Chain-specific encoding per Story 12.4. */
1034
+ claimBytes: Uint8Array;
1035
+ /** Swap's ephemeral pubkey from the FULFILL (64-char lowercase hex). */
1036
+ swapEphemeralPubkey: string;
1037
+ /** Optional Swap-side claim ID (passed through from handler metadata). */
1038
+ claimId?: string;
1039
+ /** Swap pair this claim was priced against (copy of `pair` for settlement-time routing). */
1040
+ pair: SwapPair;
1041
+ /** Unix ms timestamp when this claim was accepted. */
1042
+ receivedAt: number;
1043
+ /** Channel identifier on the target chain (lowercase hex with 0x prefix for EVM; base58 for Solana). */
1044
+ channelId?: string;
1045
+ /** Balance-proof nonce (decimal string). Monotonically increasing within a channel. */
1046
+ nonce?: string;
1047
+ /** Cumulative transferred amount on the channel (target micro-units, decimal string). */
1048
+ cumulativeAmount?: string;
1049
+ /** Recipient address (the sender's target-asset address). */
1050
+ recipient?: string;
1051
+ /** Swap's on-chain signer address. */
1052
+ swapSignerAddress?: string;
1053
+ /** Maker's fresh rate `R_i` applied to this packet (decimal string), from the FULFILL quote tape. */
1054
+ rate?: string;
1055
+ /** Unix ms timestamp when the maker's rate source produced `rate`. Present iff `rate` is. */
1056
+ rateTimestamp?: number;
1057
+ /**
1058
+ * The VERIFIED signed receipt that rode on this packet's FULFILL
1059
+ * (issue #84, rolling-swap spec §7.2) — receipts persist wherever the
1060
+ * claim does. Present iff the maker emitted receipts for this session.
1061
+ */
1062
+ receipt?: StreamReceipt;
1063
+ }
1064
+ /**
1065
+ * Aggregate result of a `streamSwap()` invocation.
1066
+ *
1067
+ * @stable — Stories 12.6 and 12.8 depend on this shape.
1068
+ */
1069
+ interface StreamSwapResult {
1070
+ state: 'completed' | 'failed' | 'stopped';
1071
+ claims: AccumulatedClaim[];
1072
+ rejections: {
1073
+ packetIndex: number;
1074
+ sourceAmount: bigint;
1075
+ code: string;
1076
+ message: string;
1077
+ }[];
1078
+ errors: {
1079
+ packetIndex: number;
1080
+ cause: Error;
1081
+ }[];
1082
+ abortReason: 'complete' | 'aborted' | 'stopped' | 'callback-stop' | 'callback-throw' | 'rate-deviation' | 'below-floor' | 'receipt-invalid' | 'all-rejected';
1083
+ cumulativeSource: bigint;
1084
+ cumulativeTarget: bigint;
1085
+ packetsSent: number;
1086
+ packetsScheduled: number;
1087
+ /**
1088
+ * The verified receipt chain for this stream (issue #84, rolling-swap
1089
+ * spec §7.2): every receipt that verified against the maker receipt key,
1090
+ * sorted by `seq`, plus the superseding `latest`, the signed
1091
+ * `totalDelivered`, and any `seq` holes. ALWAYS present — on abort it
1092
+ * covers exactly what filled before the halt; against a legacy maker
1093
+ * (no receipt support) it is simply empty. Feed to
1094
+ * `serializeReceiptChain()` for the portable audit/dispute artifact.
1095
+ */
1096
+ receipts: StreamReceiptChain;
1097
+ }
1098
+ /** External controller returned by {@link streamSwapControlled}. */
1099
+ interface StreamSwapController {
1100
+ pause(): void;
1101
+ resume(): void;
1102
+ stop(): void;
1103
+ readonly state: 'running' | 'paused' | 'stopped' | 'completed' | 'failed';
1104
+ }
1105
+ /** Length-split `total` into `count` bigints. Remainder absorbed on last element. */
1106
+ declare function chunkAmount(total: bigint, count: number): bigint[];
1107
+ /** Build the kind:20032 unsigned "swap rumor" event per AC-4.
1108
+ *
1109
+ * Story 12.9 AC-1/AC-6: the returned rumor MUST include a `chain-recipient`
1110
+ * tag carrying the sender-supplied chain-format payout address for
1111
+ * `pair.to.chain`. The value is echoed verbatim per packet — no case-folding
1112
+ * or transformation beyond what the sender-side `validateChainAddress`
1113
+ * accepts.
1114
+ *
1115
+ * Issue #84 (rfc-0039 stream receipts): when `streamNonce` is provided, the
1116
+ * rumor also carries a `stream-nonce` tag — the sender-generated session
1117
+ * identifier a receipt-capable maker echoes on every per-fulfill receipt.
1118
+ * This is the TOON analogue of rfc-0039's Verifier→Receiver Receipt-Nonce
1119
+ * provisioning (in-band, per stream). Legacy makers ignore the unknown tag.
1120
+ */
1121
+ declare function buildSwapRumor(input: {
1122
+ senderPubkey: string;
1123
+ pair: SwapPair;
1124
+ sourceAmount: bigint;
1125
+ packetIndex: number;
1126
+ totalPackets: number;
1127
+ nonce: Uint8Array;
1128
+ createdAt: number;
1129
+ chainRecipient: string;
1130
+ /** 32-char lowercase hex session nonce (issue #84 stream receipts). */
1131
+ streamNonce?: string;
1132
+ }): UnsignedEvent;
1133
+ /**
1134
+ * Decode the FULFILL response `data` (base64-encoded JSON metadata) into
1135
+ * the `{ claim, ephemeralPubkey, claimId?, targetAmount?, ...settlement }`
1136
+ * shape.
1137
+ *
1138
+ * Story 12.6 extension: also parses the settlement-context fields
1139
+ * (`channelId`, `nonce`, `cumulativeAmount`, `recipient`, `swapSignerAddress`).
1140
+ * These are OPTIONAL and best-effort (#153): each is threaded through only
1141
+ * when it is a well-formed string for the target chain; an absent or malformed
1142
+ * settlement field is silently dropped rather than failing the whole decode,
1143
+ * so a fulfilled swap still surfaces its signed `claim` + `ephemeralPubkey`.
1144
+ * Only `claim` and `ephemeralPubkey` are strictly required.
1145
+ *
1146
+ * Issue #82 extension (quote tape, rolling-swap spec §7.1): parses the
1147
+ * per-packet quote-tape fields `rate` (decimal string, the maker's fresh
1148
+ * `R_i`) and `rateTimestamp` (unix ms). Unlike the best-effort settlement
1149
+ * fields, a MALFORMED tape is always a loud `FULFILL_DECODE_FAILED` — a
1150
+ * present-but-garbled `rate`/`rateTimestamp`, or one field without the
1151
+ * other, fails the decode rather than silently dropping, so a rolling
1152
+ * sender can never run blind on a corrupt tape. A wholly ABSENT tape is
1153
+ * permitted for legacy makers unless `opts.requireQuoteTape` is set (which
1154
+ * `runLoop` does whenever `minExchangeRate` is armed).
1155
+ *
1156
+ * Issue #84 extension (rfc-0039 stream receipts, spec §7.2): parses the
1157
+ * optional `receipt` object into a structurally-validated {@link StreamReceipt}.
1158
+ * Like the tape, a PRESENT-but-malformed receipt is a loud
1159
+ * `FULFILL_DECODE_FAILED` (a garbled proof must never be silently dropped —
1160
+ * the sender would keep streaming while its audit artifact rots); a wholly
1161
+ * absent receipt is legacy-maker territory and tolerated here (the
1162
+ * `requireReceipts` policy is enforced by the caller, which has the halt
1163
+ * machinery). Signature/monotonicity verification is NOT done here — that is
1164
+ * `processAcceptedPacket`'s ReceiptChainTracker.
1165
+ *
1166
+ * @param chain Optional `pair.to.chain` string for per-chain format validation
1167
+ * of channelId / recipient / swapSignerAddress. When omitted, format checks
1168
+ * fall back to a length-only sanity check.
1169
+ * @param opts.requireQuoteTape When true, a missing `rate`/`rateTimestamp`
1170
+ * pair is a `FULFILL_DECODE_FAILED` error instead of being tolerated.
1171
+ */
1172
+ declare function decodeFulfillMetadata(data: string | undefined, chain?: string, opts?: {
1173
+ requireQuoteTape?: boolean;
1174
+ }): {
1175
+ claim: string;
1176
+ ephemeralPubkey: string;
1177
+ claimId?: string;
1178
+ /** Optional Swap-reported actual target amount (decimal string). Used for rate deviation when present. */
1179
+ targetAmount?: string;
1180
+ /** Quote-tape rate `R_i` (decimal string). Present iff the maker emitted the tape. */
1181
+ rate?: string;
1182
+ /** Quote-tape timestamp (unix ms). Present iff `rate` is. */
1183
+ rateTimestamp?: number;
1184
+ /** Per-fulfill stream receipt (issue #84) — structurally validated, NOT yet signature-verified. */
1185
+ receipt?: StreamReceipt;
1186
+ channelId?: string;
1187
+ nonce?: string;
1188
+ cumulativeAmount?: string;
1189
+ recipient?: string;
1190
+ swapSignerAddress?: string;
1191
+ };
1192
+ /**
1193
+ * Compare two decimal-string rates (RATE_REGEX format) exactly, in BigInt —
1194
+ * no float coercion. Returns -1 if `a < b`, 0 if equal, 1 if `a > b`.
1195
+ *
1196
+ * Used by the issue #82 `minExchangeRate` floor to compare the maker's tape
1197
+ * rate `R_i` against the floor without precision loss on long fractions.
1198
+ */
1199
+ declare function compareDecimalRates(a: string, b: string): -1 | 0 | 1;
1200
+ /**
1201
+ * Drive a multi-packet swap against a Swap and return a `StreamSwapResult`.
1202
+ *
1203
+ * `streamSwap()` does NOT throw on mid-stream failure — inspect the result's
1204
+ * `state`, `abortReason`, `rejections[]`, and `errors[]` to diagnose.
1205
+ *
1206
+ * @example
1207
+ * ```ts
1208
+ * // Discover the SwapPair from the Swap's kind:10032 peer-info event (Story 12.1).
1209
+ * const result = await streamSwap({
1210
+ * client: toonClient,
1211
+ * swapPubkey: swap.pubkey,
1212
+ * swapIlpAddress: 'g.toon.swap1',
1213
+ * pair,
1214
+ * senderSecretKey,
1215
+ * totalAmount: 1_000_000n,
1216
+ * packetCount: 10,
1217
+ * onPacket: (p) => console.log('packet', p.index, 'rate', p.effectiveRate),
1218
+ * rateDeviationThreshold: 0.02,
1219
+ * });
1220
+ * // Feed result.claims into buildSettlementTx() from Story 12.6.
1221
+ * ```
1222
+ *
1223
+ * @throws {StreamSwapError} Only for construction-time validation failures
1224
+ * (INVALID_AMOUNT, INVALID_CHUNKING, INVALID_PAIR, INVALID_STATE).
1225
+ */
1226
+ declare function streamSwap(params: StreamSwapParams): Promise<StreamSwapResult>;
1227
+ /**
1228
+ * Two-form variant of {@link streamSwap} that additionally returns a
1229
+ * {@link StreamSwapController} with `pause()` / `resume()` / `stop()`.
1230
+ *
1231
+ * @throws {StreamSwapError} Synchronously for construction-time validation.
1232
+ */
1233
+ declare function streamSwapControlled(params: StreamSwapParams): {
1234
+ result: Promise<StreamSwapResult>;
1235
+ controller: StreamSwapController;
1236
+ };
1237
+ declare const __testing: {
1238
+ chunkAmount: typeof chunkAmount;
1239
+ decodeFulfillMetadata: typeof decodeFulfillMetadata;
1240
+ buildSwapRumor: typeof buildSwapRumor;
1241
+ compareDecimalRates: typeof compareDecimalRates;
1242
+ };
1243
+
1244
+ export { wrapSwapPacket as $, type AccumulatedClaim as A, BoundedReceiptSessions as B, decryptFulfillClaim as C, DEFAULT_RECEIPT_SESSIONS_CAP as D, type EncryptFulfillClaimParams as E, encodeReceiptSigningPayload as F, encryptFulfillClaim as G, isSwapControllerState as H, InMemorySwapControllerStateStore as I, JsonFileSwapControllerStateStore as J, isValidStreamNonce as K, issueSessionReceipt as L, parseStreamReceipt as M, serializeReceiptChain as N, signStreamReceipt as O, type PacketObservation as P, streamSwap as Q, type ReceiptSessionStoreLike as R, STREAM_RECEIPT_SIGNING_TAG as S, streamSwapControlled as T, type UnwrapSwapPacketFromToonParams as U, swapControllerStateKey as V, type WrapSwapPacketParams as W, unwrapSwapPacket as X, unwrapSwapPacketFromToon as Y, verifyStreamReceipt as Z, __testing as _, AdaptiveDeltaController as a, wrapSwapPacketToToon as a0, type StreamSwapClient as a1, type AdaptiveDeltaControllerConfig as b, type DecryptFulfillClaimParams as c, type EncryptFulfillClaimResult as d, type PacketProgress as e, type PacketResolution as f, type RateMonitorCallback as g, type ReceiptAddResult as h, ReceiptChainTracker as i, type ReceiptSessionState as j, STREAM_RECEIPT_VERSION as k, type StreamReceipt as l, type StreamReceiptChain as m, type StreamReceiptFields as n, type StreamSwapAdaptiveController as o, type StreamSwapController as p, type StreamSwapParams as q, type StreamSwapResult as r, SwapControllerError as s, type SwapControllerState as t, type SwapControllerStateStore as u, type UnwrapSwapPacketParams as v, type UnwrapSwapPacketResult as w, type WrapSwapPacketResult as x, type WrapSwapPacketToToonParams as y, type WrapSwapPacketToToonResult as z };