@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.
- package/dist/{chunk-Z6S56VPV.js → chunk-RAZRWSJF.js} +1139 -225
- package/dist/chunk-RAZRWSJF.js.map +1 -0
- package/dist/index.d.ts +50 -6
- package/dist/index.js +385 -1
- package/dist/index.js.map +1 -1
- package/dist/swap-PFQTJZA7.d.ts +1244 -0
- package/dist/swap.d.ts +1 -1
- package/dist/swap.js +21 -1
- package/package.json +2 -2
- package/dist/chunk-Z6S56VPV.js.map +0 -1
- package/dist/swap-DF4ghKio.d.ts +0 -484
|
@@ -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 };
|