@toon-protocol/sdk 2.0.0 → 2.1.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-7LBYFU4L.js} +658 -204
- package/dist/chunk-7LBYFU4L.js.map +1 -0
- package/dist/index.d.ts +29 -6
- package/dist/index.js +361 -1
- package/dist/index.js.map +1 -1
- package/dist/swap-Oub-0vqU.d.ts +911 -0
- package/dist/swap.d.ts +1 -1
- package/dist/swap.js +1 -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,911 @@
|
|
|
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
|
+
* Adaptive δ/W controller for rolling swaps (issue #83, rolling-swap spec §6).
|
|
167
|
+
*
|
|
168
|
+
* Two knobs, managed separately:
|
|
169
|
+
* - `δ` (delta) — packet size in source micro-units — bounds *per-packet
|
|
170
|
+
* pick-off risk*.
|
|
171
|
+
* - `W` (window) — max unfulfilled packets in flight — bounds *timing /
|
|
172
|
+
* liveness risk* and the worst-case unrecovered exposure `δ·W`.
|
|
173
|
+
*
|
|
174
|
+
* Normative behavior implemented here (spec §6):
|
|
175
|
+
* - **The cap.** `delta_cap = ε / (v·τ)`, recomputed per packet from measured
|
|
176
|
+
* state, and `δ ≤ delta_cap` always. δ and `delta_cap` are fractions of the
|
|
177
|
+
* session's remaining notional; `v·τ` is the expected fractional rate drift
|
|
178
|
+
* while one packet is in flight; ε is the per-packet slippage budget as a
|
|
179
|
+
* fraction of the maker's advertised half-spread (default `ε = 0.5 ×
|
|
180
|
+
* halfSpread`). ε is spread-denominated, never an absolute rate.
|
|
181
|
+
* - **Inputs are measured, not trusted.** `v` is an EWMA of
|
|
182
|
+
* `abs(R_i − R_{i−1})/R_{i−1}` per second read off the quote tape
|
|
183
|
+
* (`PacketProgress.rate` / `rateTimestamp`, issue #82); `τ` is an EWMA of
|
|
184
|
+
* observed round-trip times. Both update on every observed packet.
|
|
185
|
+
* - **Asymmetric adjustment, one knob per step.** On a shrink signal (a
|
|
186
|
+
* `stale_rate` reject, any other reject/verification failure, or realized
|
|
187
|
+
* per-packet slip `> ε`): multiplicative — `δ ← max(δ_min, δ/2)`; if the
|
|
188
|
+
* signal was a timeout/expiry, `W ← max(1, ⌈W/2⌉)` instead. On a clean
|
|
189
|
+
* streak of `K = 16` consecutive fulfills: additive — `δ ← min(caps, δ +
|
|
190
|
+
* δ_0)` or `W ← min(W_max, W + 1)`, alternating, never both in one step.
|
|
191
|
+
* - **Cold start ramps.** With no persisted state for the tuple:
|
|
192
|
+
* `δ_0 = min(delta_cap, notional/256, maker maxAmount)`, `W_0 = 1`. Until
|
|
193
|
+
* the first shrink signal ever observed for the tuple, the δ widen step is
|
|
194
|
+
* multiplicative (`δ ← min(caps, 2δ)` per clean streak) — slow-start —
|
|
195
|
+
* dropping to additive permanently after the first loss event.
|
|
196
|
+
* - **State is per-(chain, maker, pair) and persisted** via a pluggable
|
|
197
|
+
* {@link SwapControllerStateStore} (the SDK is isomorphic; the JSON-file
|
|
198
|
+
* implementation is Node-only and lazily imports `node:fs`).
|
|
199
|
+
*
|
|
200
|
+
* INVARIANT (spec §5): the controller is an *efficiency* mechanism only. It
|
|
201
|
+
* never sees, computes, or relaxes the `minExchangeRate` floor — the floor
|
|
202
|
+
* check in `stream-swap.ts` runs before the controller observes anything and
|
|
203
|
+
* consults nothing but the floor itself. A calm (or adversarially painted)
|
|
204
|
+
* tape can widen δ up to the caps, but can never worsen the sender's declared
|
|
205
|
+
* worst case. NOTE: the adversarial-quote-tape question (toon-meta#146 — a
|
|
206
|
+
* maker quoting calm to coax δ wide, then gapping one large packet) is open;
|
|
207
|
+
* its resolution may tighten the cap logic here. The floor plus the absolute
|
|
208
|
+
* `maxPacketAmount` cap are the uncompromising backstops in the meantime.
|
|
209
|
+
*
|
|
210
|
+
* @module
|
|
211
|
+
*/
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Error thrown for invalid adaptive-controller configuration or a
|
|
215
|
+
* persistence-layer failure surfaced through {@link SwapControllerStateStore}.
|
|
216
|
+
*/
|
|
217
|
+
declare class SwapControllerError extends ToonError {
|
|
218
|
+
constructor(message: string, cause?: Error);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Persisted per-(chain, maker, pair) controller state (spec §6 value shape
|
|
222
|
+
* `{delta, W, vEwma, tauEwma, cleanStreak, everShrunk, updatedAt}` plus the
|
|
223
|
+
* additive `lastWidened` alternation marker).
|
|
224
|
+
*
|
|
225
|
+
* JSON-serializable by construction: `delta` is a decimal string because
|
|
226
|
+
* `bigint` does not survive `JSON.stringify`.
|
|
227
|
+
*/
|
|
228
|
+
interface SwapControllerState {
|
|
229
|
+
/** Schema version for forward migration. Currently `1`. */
|
|
230
|
+
v: 1;
|
|
231
|
+
/**
|
|
232
|
+
* Current ramp value of δ in source micro-units (decimal string).
|
|
233
|
+
* `'0'` means "not yet initialized" — the first `nextDelta()` call seeds it
|
|
234
|
+
* with the cold-start `δ_0`.
|
|
235
|
+
*/
|
|
236
|
+
delta: string;
|
|
237
|
+
/** In-flight window W: max unfulfilled packets outstanding. Always ≥ 1. */
|
|
238
|
+
W: number;
|
|
239
|
+
/** EWMA of `abs(ΔR)/R` per second, measured from the quote tape. */
|
|
240
|
+
vEwma: number;
|
|
241
|
+
/** EWMA of observed packet round-trip time, in seconds. */
|
|
242
|
+
tauEwma: number;
|
|
243
|
+
/** Consecutive clean fulfills since the last knob adjustment. */
|
|
244
|
+
cleanStreak: number;
|
|
245
|
+
/**
|
|
246
|
+
* True once ANY shrink signal has ever been observed for this tuple.
|
|
247
|
+
* Ends the multiplicative slow-start widen ramp permanently (spec §6).
|
|
248
|
+
*/
|
|
249
|
+
everShrunk: boolean;
|
|
250
|
+
/** Which knob the most recent widen step adjusted (alternation marker). */
|
|
251
|
+
lastWidened: 'delta' | 'window';
|
|
252
|
+
/** Unix ms of the last state mutation. */
|
|
253
|
+
updatedAt: number;
|
|
254
|
+
}
|
|
255
|
+
/** Runtime shape check for a (possibly foreign) persisted state blob. */
|
|
256
|
+
declare function isSwapControllerState(value: unknown): value is SwapControllerState;
|
|
257
|
+
/**
|
|
258
|
+
* Canonical persistence key for a controller tuple (spec §6):
|
|
259
|
+
* `${chain}:${makerPubkey}:${from}:${to}` with `from`/`to` as
|
|
260
|
+
* `assetCode@chain` so cross-chain pairs sharing an asset code stay distinct.
|
|
261
|
+
*
|
|
262
|
+
* The leading `chain` segment is the SOURCE chain — per-tuple state is how
|
|
263
|
+
* the same code runs fast on Base and cautious on Mina.
|
|
264
|
+
*/
|
|
265
|
+
declare function swapControllerStateKey(params: {
|
|
266
|
+
makerPubkey: string;
|
|
267
|
+
pair: SwapPair;
|
|
268
|
+
}): string;
|
|
269
|
+
/**
|
|
270
|
+
* Pluggable persistence seam for controller state, keyed by
|
|
271
|
+
* {@link swapControllerStateKey}. The SDK is isomorphic, so the interface is
|
|
272
|
+
* environment-neutral (same pattern as `WorkflowEventStore`); pick
|
|
273
|
+
* {@link JsonFileSwapControllerStateStore} on Node or supply your own
|
|
274
|
+
* (e.g. IndexedDB / daemon-side store in toon-client).
|
|
275
|
+
*/
|
|
276
|
+
interface SwapControllerStateStore {
|
|
277
|
+
load(key: string): Promise<SwapControllerState | undefined>;
|
|
278
|
+
save(key: string, state: SwapControllerState): Promise<void>;
|
|
279
|
+
}
|
|
280
|
+
/** Volatile in-memory store — the default when no store is configured. */
|
|
281
|
+
declare class InMemorySwapControllerStateStore implements SwapControllerStateStore {
|
|
282
|
+
private readonly states;
|
|
283
|
+
load(key: string): Promise<SwapControllerState | undefined>;
|
|
284
|
+
save(key: string, state: SwapControllerState): Promise<void>;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Node-only JSON-file store (the toon-client `JsonFileChannelStore` pattern —
|
|
288
|
+
* controller state persists beside the channel store). One JSON file holds a
|
|
289
|
+
* `{ [key]: SwapControllerState }` map; writes are atomic
|
|
290
|
+
* (temp-file + rename). `node:fs` / `node:path` are imported lazily so
|
|
291
|
+
* bundling this module for the browser stays safe as long as the class is
|
|
292
|
+
* not instantiated there.
|
|
293
|
+
*/
|
|
294
|
+
declare class JsonFileSwapControllerStateStore implements SwapControllerStateStore {
|
|
295
|
+
private readonly filePath;
|
|
296
|
+
constructor(filePath: string);
|
|
297
|
+
private readAll;
|
|
298
|
+
load(key: string): Promise<SwapControllerState | undefined>;
|
|
299
|
+
save(key: string, state: SwapControllerState): Promise<void>;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Resolution class of one packet, as measured by the sender (spec §6):
|
|
303
|
+
*
|
|
304
|
+
* - `'fulfill'` — packet fulfilled and its claim accepted. Clean unless the
|
|
305
|
+
* realized slip exceeds ε (computed internally from `rate` +
|
|
306
|
+
* `sourceAmount` + `targetAmount`), in which case it is a shrink signal.
|
|
307
|
+
* - `'reject-stale'` — maker staleness reject (`T99 stale_rate`, spec §4).
|
|
308
|
+
* Shrink signal → δ halves.
|
|
309
|
+
* - `'reject'` — any other reject or verification failure (R5-class: floor
|
|
310
|
+
* breach, recipient substitution, bad claim). Shrink signal → δ halves.
|
|
311
|
+
* - `'timeout'` — packet expiry / transport timeout. Shrink signal → W
|
|
312
|
+
* halves (timing risk, not pricing risk).
|
|
313
|
+
* - `'error'` — transport or decode error. Shrink signal → δ halves.
|
|
314
|
+
*/
|
|
315
|
+
type PacketResolution = 'fulfill' | 'reject-stale' | 'reject' | 'timeout' | 'error';
|
|
316
|
+
/** One per-packet observation fed to {@link AdaptiveDeltaController.observe}. */
|
|
317
|
+
interface PacketObservation {
|
|
318
|
+
/** Resolution class for this packet (see {@link PacketResolution}). */
|
|
319
|
+
resolution: PacketResolution;
|
|
320
|
+
/** Measured round-trip time for this packet in ms (send → resolve). */
|
|
321
|
+
rttMs?: number;
|
|
322
|
+
/** Quote-tape rate `R_i` for this packet (decimal string, issue #82). */
|
|
323
|
+
rate?: string;
|
|
324
|
+
/** Unix ms when the maker's rate source produced {@link rate}. */
|
|
325
|
+
rateTimestamp?: number;
|
|
326
|
+
/** Source amount sent for this packet (micro-units). Used for slip. */
|
|
327
|
+
sourceAmount?: bigint;
|
|
328
|
+
/** Delivered target amount (micro-units). Used for slip. */
|
|
329
|
+
targetAmount?: bigint;
|
|
330
|
+
/**
|
|
331
|
+
* Remaining session notional AFTER this packet (source micro-units).
|
|
332
|
+
* When provided, widen steps are additionally clamped to the current
|
|
333
|
+
* `delta_cap` fraction of it (spec §6 `δ ← min(delta_cap, δ + δ_0)`).
|
|
334
|
+
* `nextDelta()` re-enforces the cap at issuance regardless.
|
|
335
|
+
*/
|
|
336
|
+
remaining?: bigint;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* The seam `streamSwap` consumes (kept structural so alternative controller
|
|
340
|
+
* implementations — or a test double — can be plugged in).
|
|
341
|
+
*/
|
|
342
|
+
interface StreamSwapAdaptiveController {
|
|
343
|
+
/**
|
|
344
|
+
* Decide the size of the next packet, in source micro-units, given the
|
|
345
|
+
* session's remaining notional. MUST return a value in `[1, remaining]`
|
|
346
|
+
* (callers clamp defensively). Enforces `δ ≤ delta_cap` at issuance.
|
|
347
|
+
*/
|
|
348
|
+
nextDelta(remaining: bigint): bigint;
|
|
349
|
+
/** Current in-flight window W (max unfulfilled packets outstanding, ≥ 1). */
|
|
350
|
+
readonly window: number;
|
|
351
|
+
/** Feed one packet observation. May persist state (hence async). */
|
|
352
|
+
observe(observation: PacketObservation): void | Promise<void>;
|
|
353
|
+
}
|
|
354
|
+
/** Configuration for {@link AdaptiveDeltaController.create}. */
|
|
355
|
+
interface AdaptiveDeltaControllerConfig {
|
|
356
|
+
/** Maker's 64-char hex pubkey — part of the persistence key. */
|
|
357
|
+
makerPubkey: string;
|
|
358
|
+
/** The pair being executed — chain + asset parts of the persistence key. */
|
|
359
|
+
pair: SwapPair;
|
|
360
|
+
/**
|
|
361
|
+
* Maker's advertised two-sided spread as a fraction (e.g. `0.004` = 40 bps).
|
|
362
|
+
* ε is denominated off this — NEVER an absolute rate (spec §6): the
|
|
363
|
+
* half-spread self-calibrates ε per chain and per maker. Today the spread
|
|
364
|
+
* is caller-supplied (from the maker's board / RFQ response once toon#145's
|
|
365
|
+
* RFQ lands); there is deliberately no default — an invented spread would
|
|
366
|
+
* rot and silently mis-size ε.
|
|
367
|
+
*/
|
|
368
|
+
advertisedSpread: number;
|
|
369
|
+
/**
|
|
370
|
+
* ε as a fraction of the advertised HALF-spread. Default `0.5`
|
|
371
|
+
* (spec §6 default `ε = 0.5 × halfSpread`).
|
|
372
|
+
*/
|
|
373
|
+
epsilonHalfSpreadFraction?: number;
|
|
374
|
+
/**
|
|
375
|
+
* Absolute per-packet ceiling in source micro-units (the maker's advertised
|
|
376
|
+
* `maxAmount`). Applied to δ at issuance AND to every widen step — the
|
|
377
|
+
* uncompromising absolute cap alongside the measured `v·τ` bound.
|
|
378
|
+
*/
|
|
379
|
+
maxPacketAmount?: bigint;
|
|
380
|
+
/** Floor on δ in source micro-units (`δ_min`). Default `1n`. */
|
|
381
|
+
minPacketAmount?: bigint;
|
|
382
|
+
/** Ceiling on W (`W_max`). Default `8`. */
|
|
383
|
+
maxWindow?: number;
|
|
384
|
+
/** Clean-fulfill streak length K per widen step. Default `16` (spec §6). */
|
|
385
|
+
cleanStreakLength?: number;
|
|
386
|
+
/**
|
|
387
|
+
* Cold-start divisor: `δ_0 = notional / coldStartDivisor` (further clamped
|
|
388
|
+
* by `delta_cap` and `maxPacketAmount`). Default `256` (spec §6).
|
|
389
|
+
*/
|
|
390
|
+
coldStartDivisor?: number;
|
|
391
|
+
/** EWMA smoothing factor α for both `v` and `τ`, in (0, 1]. Default `0.2`. */
|
|
392
|
+
ewmaAlpha?: number;
|
|
393
|
+
/**
|
|
394
|
+
* Pluggable persistence. Default: a fresh volatile
|
|
395
|
+
* {@link InMemorySwapControllerStateStore} (state survives the session
|
|
396
|
+
* only). Pass {@link JsonFileSwapControllerStateStore} (Node) or a custom
|
|
397
|
+
* store to persist ramp/trust across swaps.
|
|
398
|
+
*/
|
|
399
|
+
store?: SwapControllerStateStore;
|
|
400
|
+
/** Injectable clock for deterministic tests. Default `Date.now`. */
|
|
401
|
+
now?: () => number;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Per-(chain, maker, pair) adaptive δ/W controller (spec §6). Construct via
|
|
405
|
+
* {@link AdaptiveDeltaController.create} (async: loads persisted state).
|
|
406
|
+
*
|
|
407
|
+
* Concurrency note: one instance drives one swap session. Running two
|
|
408
|
+
* concurrent sessions against the same tuple is last-write-wins on the
|
|
409
|
+
* persisted state (same as concurrent channel use).
|
|
410
|
+
*/
|
|
411
|
+
declare class AdaptiveDeltaController implements StreamSwapAdaptiveController {
|
|
412
|
+
/** Persistence key for this tuple (see {@link swapControllerStateKey}). */
|
|
413
|
+
readonly key: string;
|
|
414
|
+
private readonly store;
|
|
415
|
+
private readonly pair;
|
|
416
|
+
private readonly epsilon;
|
|
417
|
+
private readonly maxPacketAmount;
|
|
418
|
+
private readonly minPacketAmount;
|
|
419
|
+
private readonly maxWindow;
|
|
420
|
+
private readonly cleanStreakLength;
|
|
421
|
+
private readonly coldStartDivisor;
|
|
422
|
+
private readonly alpha;
|
|
423
|
+
private readonly now;
|
|
424
|
+
private readonly stateInternal;
|
|
425
|
+
/** δ in-memory as bigint (mirrors `stateInternal.delta`). */
|
|
426
|
+
private delta;
|
|
427
|
+
/** Cold-start δ_0 — also the additive widen increment. Set on first nextDelta(). */
|
|
428
|
+
private delta0;
|
|
429
|
+
/** Previous tape entry for the per-second volatility measurement. */
|
|
430
|
+
private lastRate;
|
|
431
|
+
private lastRateTimestamp;
|
|
432
|
+
private constructor();
|
|
433
|
+
/**
|
|
434
|
+
* Create a controller for one swap session: loads the persisted state for
|
|
435
|
+
* the tuple from the store, or starts cold (`δ` unset until the first
|
|
436
|
+
* `nextDelta()`, `W = 1`).
|
|
437
|
+
*/
|
|
438
|
+
static create(config: AdaptiveDeltaControllerConfig): Promise<AdaptiveDeltaController>;
|
|
439
|
+
/** Defensive snapshot of the current controller state. */
|
|
440
|
+
get state(): SwapControllerState;
|
|
441
|
+
/** Current in-flight window W. */
|
|
442
|
+
get window(): number;
|
|
443
|
+
/**
|
|
444
|
+
* Current `delta_cap = ε/(v·τ)` as a fraction of remaining notional.
|
|
445
|
+
* `Infinity` while `v·τ` has no measurement yet (cold tape) — the
|
|
446
|
+
* cold-start `δ_0` and absolute caps bound δ in that regime.
|
|
447
|
+
*/
|
|
448
|
+
get deltaCapFraction(): number;
|
|
449
|
+
/** `delta_cap` in absolute source micro-units for a given remaining notional. */
|
|
450
|
+
private deltaCapAbsolute;
|
|
451
|
+
/**
|
|
452
|
+
* Decide the next packet size (source micro-units) for a session with
|
|
453
|
+
* `remaining` notional left. Enforces, in order: the measured
|
|
454
|
+
* `delta_cap = ε/(v·τ)` bound, the absolute `maxPacketAmount` cap, the
|
|
455
|
+
* remaining notional, and the `minPacketAmount` floor. Seeds the
|
|
456
|
+
* cold-start `δ_0 = min(delta_cap, notional/256, maxPacketAmount)` on
|
|
457
|
+
* first call.
|
|
458
|
+
*/
|
|
459
|
+
nextDelta(remaining: bigint): bigint;
|
|
460
|
+
/**
|
|
461
|
+
* Feed one packet observation: updates the measured EWMAs (`v`, `τ`),
|
|
462
|
+
* applies at most ONE knob adjustment (asymmetric: multiplicative shrink /
|
|
463
|
+
* additive widen), and persists the state.
|
|
464
|
+
*/
|
|
465
|
+
observe(observation: PacketObservation): Promise<void>;
|
|
466
|
+
/**
|
|
467
|
+
* Realized per-packet slip as a fraction: shortfall of the delivered
|
|
468
|
+
* target amount vs the amount implied by the packet's own tape rate `R_i`
|
|
469
|
+
* (spec §7.1 `Δcumulative` vs `⌊δ · R_i⌋` cross-check). `0` when the
|
|
470
|
+
* inputs are unavailable or delivery met the tape.
|
|
471
|
+
*/
|
|
472
|
+
private realizedSlip;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Sender-side `streamSwap()` API (Story 12.5).
|
|
477
|
+
*
|
|
478
|
+
* Drives the sender side of the Token Swap Primitive: chunks a total source
|
|
479
|
+
* amount into N sender-chosen packets, wraps each with `wrapSwapPacketToToon()`
|
|
480
|
+
* using a fresh ephemeral gift-wrap key per packet (D12-003 / risk R-006),
|
|
481
|
+
* sends each via `ToonClient.sendSwapPacket(...)`, decrypts each FULFILL's
|
|
482
|
+
* NIP-44-encrypted claim with `decryptFulfillClaim()`, accumulates the
|
|
483
|
+
* decrypted claims into an ordered array, invokes a per-packet rate monitoring
|
|
484
|
+
* callback, and supports pause / resume / stop / AbortSignal so the sender
|
|
485
|
+
* can abort when rate drifts past tolerance.
|
|
486
|
+
*
|
|
487
|
+
* Composition story: consumes Stories 12.1 (SwapPair type), 12.2 (gift wrap
|
|
488
|
+
* primitives), 12.3 (handler wire contract). Produces `AccumulatedClaim[]`
|
|
489
|
+
* consumed by Story 12.6 (`buildSettlementTx()`).
|
|
490
|
+
*
|
|
491
|
+
* Design decisions:
|
|
492
|
+
* - `streamSwap()` does NOT throw on mid-stream failure. Caller gets
|
|
493
|
+
* `StreamSwapResult` with `state` and `abortReason`. Only construction-time
|
|
494
|
+
* validation throws synchronously.
|
|
495
|
+
* - `streamSwap()` does NOT retry individual packets. BTP-level connection
|
|
496
|
+
* retries are handled inside `BtpRuntimeClient`; application-layer retry
|
|
497
|
+
* (e.g., T04 insufficient inventory) is a future-story concern.
|
|
498
|
+
* - Rate deviation math stays BigInt throughout. The `effectiveRate` on
|
|
499
|
+
* `PacketProgress` is a display-only `number` (Epic 11 retro MAX_SAFE_INTEGER
|
|
500
|
+
* guard).
|
|
501
|
+
*
|
|
502
|
+
* @module
|
|
503
|
+
*/
|
|
504
|
+
|
|
505
|
+
/** Minimal `IlpSendResult` shape — mirrors `packages/client/src/types.ts`. */
|
|
506
|
+
interface IlpSendResultLike {
|
|
507
|
+
accepted: boolean;
|
|
508
|
+
data?: string;
|
|
509
|
+
code?: string;
|
|
510
|
+
message?: string;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Minimal `SignedBalanceProof` structural type.
|
|
514
|
+
*
|
|
515
|
+
* We avoid importing from `@toon-protocol/client` so the SDK package does not
|
|
516
|
+
* gain a runtime dep on the client package. The sender layer only forwards
|
|
517
|
+
* this opaquely to `ToonClient.sendSwapPacket`.
|
|
518
|
+
*/
|
|
519
|
+
type SignedBalanceProofLike = any;
|
|
520
|
+
/**
|
|
521
|
+
* The subset of `ToonClient` that `streamSwap()` requires. This narrow shape
|
|
522
|
+
* exists so that consumers can pass a real `ToonClient` OR a mock without
|
|
523
|
+
* importing the whole class.
|
|
524
|
+
*/
|
|
525
|
+
interface StreamSwapClient {
|
|
526
|
+
sendSwapPacket(params: {
|
|
527
|
+
destination: string;
|
|
528
|
+
amount: bigint;
|
|
529
|
+
toonData: Uint8Array;
|
|
530
|
+
timeout?: number;
|
|
531
|
+
claim?: SignedBalanceProofLike;
|
|
532
|
+
/**
|
|
533
|
+
* Explicit per-packet PREPARE expiry. When set, the transport MUST use
|
|
534
|
+
* exactly this expiry on the wire instead of deriving one from the
|
|
535
|
+
* request timeout. Optional so existing `ToonClient` implementations
|
|
536
|
+
* remain structurally compatible (they ignore the extra field until
|
|
537
|
+
* the client-transport work lands).
|
|
538
|
+
*/
|
|
539
|
+
expiresAt?: Date;
|
|
540
|
+
}): Promise<IlpSendResultLike>;
|
|
541
|
+
getPublicKey(): string;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Parameters for {@link streamSwap} / {@link streamSwapControlled}.
|
|
545
|
+
*
|
|
546
|
+
* @stable — downstream Stories 12.6 and 12.8 depend on this shape.
|
|
547
|
+
*/
|
|
548
|
+
interface StreamSwapParams {
|
|
549
|
+
/** SDK client with BTP wiring (see {@link StreamSwapClient}). */
|
|
550
|
+
client: StreamSwapClient;
|
|
551
|
+
/** Swap's 64-char lowercase hex pubkey (recipient of gift wrap). */
|
|
552
|
+
swapPubkey: string;
|
|
553
|
+
/** Swap's ILP destination address (e.g., 'g.toon.swap1'). */
|
|
554
|
+
swapIlpAddress: string;
|
|
555
|
+
/** The `SwapPair` being executed (from kind:10032 discovery, Story 12.1). */
|
|
556
|
+
pair: SwapPair;
|
|
557
|
+
/** Sender's 32-byte secp256k1 secret key. Used for seal signing AND FULFILL decryption. */
|
|
558
|
+
senderSecretKey: Uint8Array;
|
|
559
|
+
/**
|
|
560
|
+
* Sender's chain-specific payout address for `pair.to.chain` (Story 12.9 AC-4).
|
|
561
|
+
*
|
|
562
|
+
* REQUIRED. The Nostr `senderPubkey` (identity layer) cannot be used as the
|
|
563
|
+
* on-chain `recipient` in a balance-proof because they are cryptographically
|
|
564
|
+
* independent keys (D12-011). For `evm:*` chains this must be a 20-byte
|
|
565
|
+
* lowercased `0x`-prefixed hex address; for `solana:*` a base58 pubkey that
|
|
566
|
+
* decodes to 32 bytes; for `mina:*` a base58 public-key string. The value
|
|
567
|
+
* is validated at `streamSwapControlled()` entry via `validateChainAddress`
|
|
568
|
+
* and echoed on every rumor as the `chain-recipient` tag (AC-6).
|
|
569
|
+
*/
|
|
570
|
+
chainRecipient: string;
|
|
571
|
+
/** Total source-asset amount to swap (source micro-units). */
|
|
572
|
+
totalAmount: bigint;
|
|
573
|
+
/** Even-split packet count. EXACTLY ONE of this, `packetAmounts`, or `controller` is required. */
|
|
574
|
+
packetCount?: number;
|
|
575
|
+
/** Explicit per-packet amounts. EXACTLY ONE of this, `packetCount`, or `controller` is required. */
|
|
576
|
+
packetAmounts?: readonly bigint[];
|
|
577
|
+
/**
|
|
578
|
+
* Adaptive δ/W controller (issue #83, rolling-swap spec §6). EXACTLY ONE
|
|
579
|
+
* of this, `packetCount`, or `packetAmounts` is required.
|
|
580
|
+
*
|
|
581
|
+
* When set, packetization is DYNAMIC: the static even split is replaced by
|
|
582
|
+
* per-packet sizing from `controller.nextDelta(remaining)` (δ, capped by
|
|
583
|
+
* the measured `ε/(v·τ)` bound), and up to `controller.window` (W) packets
|
|
584
|
+
* are kept in flight concurrently. After every packet resolves, the
|
|
585
|
+
* controller receives a {@link PacketObservation} (realized rate, tape
|
|
586
|
+
* entry, RTT, resolution class) so δ and W adapt across the stream —
|
|
587
|
+
* multiplicative shrink on stale/slip/reject, additive widen on clean
|
|
588
|
+
* streaks.
|
|
589
|
+
*
|
|
590
|
+
* Adaptive-mode contract differences (each N/A to the legacy paths):
|
|
591
|
+
* - `PacketProgress.total` is the number of packets scheduled SO FAR (the
|
|
592
|
+
* final count is unknown upfront).
|
|
593
|
+
* - With `W > 1`, `onPacket` fires in packet COMPLETION order, not strict
|
|
594
|
+
* index order.
|
|
595
|
+
* - On a mid-stream halt (floor breach, stop, abort), already-sent
|
|
596
|
+
* in-flight packets are drained and their claims harvested before the
|
|
597
|
+
* result resolves.
|
|
598
|
+
*
|
|
599
|
+
* INVARIANT: the controller only tightens/loosens δ and W. It is consulted
|
|
600
|
+
* strictly AFTER the `minExchangeRate` floor check and can never relax it.
|
|
601
|
+
*/
|
|
602
|
+
controller?: StreamSwapAdaptiveController;
|
|
603
|
+
/** Source-asset balance proof claim. Required unless ChannelManager is wired. */
|
|
604
|
+
claim?: SignedBalanceProofLike;
|
|
605
|
+
/** Rate monitoring callback (fires after each accepted FULFILL). */
|
|
606
|
+
onPacket?: RateMonitorCallback;
|
|
607
|
+
/** Rate deviation threshold (decimal, e.g., 0.02 = 2%). */
|
|
608
|
+
rateDeviationThreshold?: number;
|
|
609
|
+
/**
|
|
610
|
+
* Hard floor on the per-packet exchange rate (issue #82, rfc-0029
|
|
611
|
+
* `minExchangeRate` semantics; rolling-swap spec §5). Decimal string in
|
|
612
|
+
* `SwapPair.rate` format (target whole-units per source whole-unit),
|
|
613
|
+
* strictly positive.
|
|
614
|
+
*
|
|
615
|
+
* When set:
|
|
616
|
+
* - The quote tape (`rate` + `rateTimestamp` on each FULFILL's metadata)
|
|
617
|
+
* becomes REQUIRED: a fulfilled packet whose metadata is missing the
|
|
618
|
+
* tape is a loud per-packet `FULFILL_DECODE_FAILED` error, never a
|
|
619
|
+
* silent drop.
|
|
620
|
+
* - Each fulfilled packet is checked BEFORE its claim is decrypted or
|
|
621
|
+
* accumulated: if the maker's tape rate `R_i` is below the floor, OR
|
|
622
|
+
* the delivered `targetAmount` is below `applyRate(sourceAmount,
|
|
623
|
+
* minExchangeRate)`, the packet is recorded as a rejection with code
|
|
624
|
+
* `BELOW_FLOOR` and the stream halts with `abortReason: 'below-floor'`.
|
|
625
|
+
* A violating packet NEVER accumulates into `claims[]`.
|
|
626
|
+
*
|
|
627
|
+
* This is the safety mechanism, deliberately independent of (and never
|
|
628
|
+
* relaxed by) the soft `rateDeviationThreshold` monitor, the `onPacket`
|
|
629
|
+
* callback, or any future adaptive-controller signal: a calm tape must
|
|
630
|
+
* not be able to talk the sender into a worse worst case.
|
|
631
|
+
*
|
|
632
|
+
* When omitted, behavior is unchanged (legacy back-compat): the tape is
|
|
633
|
+
* optional and only the soft deviation monitor applies.
|
|
634
|
+
*/
|
|
635
|
+
minExchangeRate?: string;
|
|
636
|
+
/** Per-packet timeout in ms. Default 30000. */
|
|
637
|
+
packetTimeoutMs?: number;
|
|
638
|
+
/**
|
|
639
|
+
* Per-packet PREPARE expiry window in ms. When set, each packet is sent
|
|
640
|
+
* with `expiresAt = now + packetExpiryMs` (computed at send time) so a
|
|
641
|
+
* stalled packet expires deterministically and releases its in-flight
|
|
642
|
+
* slot (rolling-swap R7). When omitted, behavior is unchanged: the
|
|
643
|
+
* transport derives the expiry from its timeout (back-compat default).
|
|
644
|
+
*/
|
|
645
|
+
packetExpiryMs?: number;
|
|
646
|
+
/** Optional AbortSignal. */
|
|
647
|
+
signal?: AbortSignal;
|
|
648
|
+
/**
|
|
649
|
+
* Optional pino-compatible logger. Defaults to a no-op.
|
|
650
|
+
*
|
|
651
|
+
* Note: `streamSwap` calls each method with a single structured-event object
|
|
652
|
+
* (e.g., `logger.warn({ event: 'stream_swap.packet_rejected', code, ... })`).
|
|
653
|
+
* Pino accepts this form (the object is logged as top-level fields and the
|
|
654
|
+
* message is left empty); other loggers that expect `(msg, meta)` should
|
|
655
|
+
* wrap accordingly.
|
|
656
|
+
*/
|
|
657
|
+
logger?: {
|
|
658
|
+
debug: (...a: unknown[]) => void;
|
|
659
|
+
info: (...a: unknown[]) => void;
|
|
660
|
+
warn: (...a: unknown[]) => void;
|
|
661
|
+
error: (...a: unknown[]) => void;
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Rate monitoring callback. Fires after each successful FULFILL decryption,
|
|
666
|
+
* before the next packet is sent. If the callback throws or rejects, the
|
|
667
|
+
* stream is treated as stopped.
|
|
668
|
+
*/
|
|
669
|
+
type RateMonitorCallback = (progress: PacketProgress) => void | Promise<void>;
|
|
670
|
+
/**
|
|
671
|
+
* Progress payload delivered to {@link RateMonitorCallback}. Frozen to
|
|
672
|
+
* prevent callback mutation.
|
|
673
|
+
*/
|
|
674
|
+
interface PacketProgress {
|
|
675
|
+
/** 0-indexed packet number within this streamSwap invocation. */
|
|
676
|
+
index: number;
|
|
677
|
+
/**
|
|
678
|
+
* Total number of packets scheduled. In adaptive mode (`controller` set)
|
|
679
|
+
* packet sizing is dynamic, so this is the number scheduled SO FAR.
|
|
680
|
+
*/
|
|
681
|
+
total: number;
|
|
682
|
+
/** Source-asset amount sent for this packet (micro-units). */
|
|
683
|
+
sourceAmount: bigint;
|
|
684
|
+
/** Target-asset claim amount derived for this packet (micro-units). */
|
|
685
|
+
targetAmount: bigint;
|
|
686
|
+
/** Rate advertised on the `SwapPair` at swap start (decimal string). */
|
|
687
|
+
advertisedRate: string;
|
|
688
|
+
/** Effective rate for this packet as JS Number (target / source in whole units). Display-only. */
|
|
689
|
+
effectiveRate: number;
|
|
690
|
+
/** Absolute deviation from advertisedRate as a decimal (e.g., 0.0125 = 1.25%). */
|
|
691
|
+
rateDeviation: number;
|
|
692
|
+
/** Cumulative source sent across accepted packets so far (including this one). */
|
|
693
|
+
cumulativeSource: bigint;
|
|
694
|
+
/** Cumulative target received so far (including this one). */
|
|
695
|
+
cumulativeTarget: bigint;
|
|
696
|
+
/**
|
|
697
|
+
* Quote-tape entry (issue #82, rolling-swap spec §7.1): the maker's fresh
|
|
698
|
+
* rate `R_i` actually applied to THIS packet, as reported on the FULFILL
|
|
699
|
+
* metadata. Present iff the maker emitted the tape. The sequence of these
|
|
700
|
+
* values across packets, in `index` order, IS the price tape the adaptive
|
|
701
|
+
* controller (toon#83) consumes.
|
|
702
|
+
*/
|
|
703
|
+
rate?: string;
|
|
704
|
+
/** Unix ms timestamp when the maker's rate source produced {@link rate}. Present iff `rate` is. */
|
|
705
|
+
rateTimestamp?: number;
|
|
706
|
+
/** Controller state at callback time. */
|
|
707
|
+
state: 'running' | 'paused' | 'stopped';
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* An accumulated claim successfully harvested from a single packet.
|
|
711
|
+
*
|
|
712
|
+
* @stable — Story 12.6 (`buildSettlementTx()`) depends on this shape.
|
|
713
|
+
* Breaking changes require a coordinated migration.
|
|
714
|
+
*
|
|
715
|
+
* Story 12.6 ADDITIVE extension: the settlement-context fields
|
|
716
|
+
* `channelId`, `nonce`, `cumulativeAmount`, `recipient`, and
|
|
717
|
+
* `swapSignerAddress` are marked optional (`?:`) for one story-cycle of
|
|
718
|
+
* backward compat but are REQUIRED in practice: Story 12.6's
|
|
719
|
+
* `buildSettlementTx()` throws `MISSING_SETTLEMENT_METADATA` when any of
|
|
720
|
+
* these are absent.
|
|
721
|
+
*/
|
|
722
|
+
interface AccumulatedClaim {
|
|
723
|
+
/** 0-indexed position in the swap's packet stream. */
|
|
724
|
+
packetIndex: number;
|
|
725
|
+
/** Source-asset amount sent for this packet (micro-units). */
|
|
726
|
+
sourceAmount: bigint;
|
|
727
|
+
/**
|
|
728
|
+
* Target-asset amount claimed (micro-units).
|
|
729
|
+
*
|
|
730
|
+
* **Source of truth caveat:** This is the expected target amount computed
|
|
731
|
+
* by `applyRate(pair.rate)`. The actual signed-claim amount lives inside
|
|
732
|
+
* `claimBytes`; Story 12.6 is responsible for parsing `claimBytes` per
|
|
733
|
+
* chain and verifying the on-wire signed amount equals this expected amount.
|
|
734
|
+
*/
|
|
735
|
+
targetAmount: bigint;
|
|
736
|
+
/** Decrypted signed claim bytes. Chain-specific encoding per Story 12.4. */
|
|
737
|
+
claimBytes: Uint8Array;
|
|
738
|
+
/** Swap's ephemeral pubkey from the FULFILL (64-char lowercase hex). */
|
|
739
|
+
swapEphemeralPubkey: string;
|
|
740
|
+
/** Optional Swap-side claim ID (passed through from handler metadata). */
|
|
741
|
+
claimId?: string;
|
|
742
|
+
/** Swap pair this claim was priced against (copy of `pair` for settlement-time routing). */
|
|
743
|
+
pair: SwapPair;
|
|
744
|
+
/** Unix ms timestamp when this claim was accepted. */
|
|
745
|
+
receivedAt: number;
|
|
746
|
+
/** Channel identifier on the target chain (lowercase hex with 0x prefix for EVM; base58 for Solana). */
|
|
747
|
+
channelId?: string;
|
|
748
|
+
/** Balance-proof nonce (decimal string). Monotonically increasing within a channel. */
|
|
749
|
+
nonce?: string;
|
|
750
|
+
/** Cumulative transferred amount on the channel (target micro-units, decimal string). */
|
|
751
|
+
cumulativeAmount?: string;
|
|
752
|
+
/** Recipient address (the sender's target-asset address). */
|
|
753
|
+
recipient?: string;
|
|
754
|
+
/** Swap's on-chain signer address. */
|
|
755
|
+
swapSignerAddress?: string;
|
|
756
|
+
/** Maker's fresh rate `R_i` applied to this packet (decimal string), from the FULFILL quote tape. */
|
|
757
|
+
rate?: string;
|
|
758
|
+
/** Unix ms timestamp when the maker's rate source produced `rate`. Present iff `rate` is. */
|
|
759
|
+
rateTimestamp?: number;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Aggregate result of a `streamSwap()` invocation.
|
|
763
|
+
*
|
|
764
|
+
* @stable — Stories 12.6 and 12.8 depend on this shape.
|
|
765
|
+
*/
|
|
766
|
+
interface StreamSwapResult {
|
|
767
|
+
state: 'completed' | 'failed' | 'stopped';
|
|
768
|
+
claims: AccumulatedClaim[];
|
|
769
|
+
rejections: {
|
|
770
|
+
packetIndex: number;
|
|
771
|
+
sourceAmount: bigint;
|
|
772
|
+
code: string;
|
|
773
|
+
message: string;
|
|
774
|
+
}[];
|
|
775
|
+
errors: {
|
|
776
|
+
packetIndex: number;
|
|
777
|
+
cause: Error;
|
|
778
|
+
}[];
|
|
779
|
+
abortReason: 'complete' | 'aborted' | 'stopped' | 'callback-stop' | 'callback-throw' | 'rate-deviation' | 'below-floor' | 'all-rejected';
|
|
780
|
+
cumulativeSource: bigint;
|
|
781
|
+
cumulativeTarget: bigint;
|
|
782
|
+
packetsSent: number;
|
|
783
|
+
packetsScheduled: number;
|
|
784
|
+
}
|
|
785
|
+
/** External controller returned by {@link streamSwapControlled}. */
|
|
786
|
+
interface StreamSwapController {
|
|
787
|
+
pause(): void;
|
|
788
|
+
resume(): void;
|
|
789
|
+
stop(): void;
|
|
790
|
+
readonly state: 'running' | 'paused' | 'stopped' | 'completed' | 'failed';
|
|
791
|
+
}
|
|
792
|
+
/** Length-split `total` into `count` bigints. Remainder absorbed on last element. */
|
|
793
|
+
declare function chunkAmount(total: bigint, count: number): bigint[];
|
|
794
|
+
/** Build the kind:20032 unsigned "swap rumor" event per AC-4.
|
|
795
|
+
*
|
|
796
|
+
* Story 12.9 AC-1/AC-6: the returned rumor MUST include a `chain-recipient`
|
|
797
|
+
* tag carrying the sender-supplied chain-format payout address for
|
|
798
|
+
* `pair.to.chain`. The value is echoed verbatim per packet — no case-folding
|
|
799
|
+
* or transformation beyond what the sender-side `validateChainAddress`
|
|
800
|
+
* accepts.
|
|
801
|
+
*/
|
|
802
|
+
declare function buildSwapRumor(input: {
|
|
803
|
+
senderPubkey: string;
|
|
804
|
+
pair: SwapPair;
|
|
805
|
+
sourceAmount: bigint;
|
|
806
|
+
packetIndex: number;
|
|
807
|
+
totalPackets: number;
|
|
808
|
+
nonce: Uint8Array;
|
|
809
|
+
createdAt: number;
|
|
810
|
+
chainRecipient: string;
|
|
811
|
+
}): UnsignedEvent;
|
|
812
|
+
/**
|
|
813
|
+
* Decode the FULFILL response `data` (base64-encoded JSON metadata) into
|
|
814
|
+
* the `{ claim, ephemeralPubkey, claimId?, targetAmount?, ...settlement }`
|
|
815
|
+
* shape.
|
|
816
|
+
*
|
|
817
|
+
* Story 12.6 extension: also parses the settlement-context fields
|
|
818
|
+
* (`channelId`, `nonce`, `cumulativeAmount`, `recipient`, `swapSignerAddress`).
|
|
819
|
+
* These are OPTIONAL and best-effort (#153): each is threaded through only
|
|
820
|
+
* when it is a well-formed string for the target chain; an absent or malformed
|
|
821
|
+
* settlement field is silently dropped rather than failing the whole decode,
|
|
822
|
+
* so a fulfilled swap still surfaces its signed `claim` + `ephemeralPubkey`.
|
|
823
|
+
* Only `claim` and `ephemeralPubkey` are strictly required.
|
|
824
|
+
*
|
|
825
|
+
* Issue #82 extension (quote tape, rolling-swap spec §7.1): parses the
|
|
826
|
+
* per-packet quote-tape fields `rate` (decimal string, the maker's fresh
|
|
827
|
+
* `R_i`) and `rateTimestamp` (unix ms). Unlike the best-effort settlement
|
|
828
|
+
* fields, a MALFORMED tape is always a loud `FULFILL_DECODE_FAILED` — a
|
|
829
|
+
* present-but-garbled `rate`/`rateTimestamp`, or one field without the
|
|
830
|
+
* other, fails the decode rather than silently dropping, so a rolling
|
|
831
|
+
* sender can never run blind on a corrupt tape. A wholly ABSENT tape is
|
|
832
|
+
* permitted for legacy makers unless `opts.requireQuoteTape` is set (which
|
|
833
|
+
* `runLoop` does whenever `minExchangeRate` is armed).
|
|
834
|
+
*
|
|
835
|
+
* @param chain Optional `pair.to.chain` string for per-chain format validation
|
|
836
|
+
* of channelId / recipient / swapSignerAddress. When omitted, format checks
|
|
837
|
+
* fall back to a length-only sanity check.
|
|
838
|
+
* @param opts.requireQuoteTape When true, a missing `rate`/`rateTimestamp`
|
|
839
|
+
* pair is a `FULFILL_DECODE_FAILED` error instead of being tolerated.
|
|
840
|
+
*/
|
|
841
|
+
declare function decodeFulfillMetadata(data: string | undefined, chain?: string, opts?: {
|
|
842
|
+
requireQuoteTape?: boolean;
|
|
843
|
+
}): {
|
|
844
|
+
claim: string;
|
|
845
|
+
ephemeralPubkey: string;
|
|
846
|
+
claimId?: string;
|
|
847
|
+
/** Optional Swap-reported actual target amount (decimal string). Used for rate deviation when present. */
|
|
848
|
+
targetAmount?: string;
|
|
849
|
+
/** Quote-tape rate `R_i` (decimal string). Present iff the maker emitted the tape. */
|
|
850
|
+
rate?: string;
|
|
851
|
+
/** Quote-tape timestamp (unix ms). Present iff `rate` is. */
|
|
852
|
+
rateTimestamp?: number;
|
|
853
|
+
channelId?: string;
|
|
854
|
+
nonce?: string;
|
|
855
|
+
cumulativeAmount?: string;
|
|
856
|
+
recipient?: string;
|
|
857
|
+
swapSignerAddress?: string;
|
|
858
|
+
};
|
|
859
|
+
/**
|
|
860
|
+
* Compare two decimal-string rates (RATE_REGEX format) exactly, in BigInt —
|
|
861
|
+
* no float coercion. Returns -1 if `a < b`, 0 if equal, 1 if `a > b`.
|
|
862
|
+
*
|
|
863
|
+
* Used by the issue #82 `minExchangeRate` floor to compare the maker's tape
|
|
864
|
+
* rate `R_i` against the floor without precision loss on long fractions.
|
|
865
|
+
*/
|
|
866
|
+
declare function compareDecimalRates(a: string, b: string): -1 | 0 | 1;
|
|
867
|
+
/**
|
|
868
|
+
* Drive a multi-packet swap against a Swap and return a `StreamSwapResult`.
|
|
869
|
+
*
|
|
870
|
+
* `streamSwap()` does NOT throw on mid-stream failure — inspect the result's
|
|
871
|
+
* `state`, `abortReason`, `rejections[]`, and `errors[]` to diagnose.
|
|
872
|
+
*
|
|
873
|
+
* @example
|
|
874
|
+
* ```ts
|
|
875
|
+
* // Discover the SwapPair from the Swap's kind:10032 peer-info event (Story 12.1).
|
|
876
|
+
* const result = await streamSwap({
|
|
877
|
+
* client: toonClient,
|
|
878
|
+
* swapPubkey: swap.pubkey,
|
|
879
|
+
* swapIlpAddress: 'g.toon.swap1',
|
|
880
|
+
* pair,
|
|
881
|
+
* senderSecretKey,
|
|
882
|
+
* totalAmount: 1_000_000n,
|
|
883
|
+
* packetCount: 10,
|
|
884
|
+
* onPacket: (p) => console.log('packet', p.index, 'rate', p.effectiveRate),
|
|
885
|
+
* rateDeviationThreshold: 0.02,
|
|
886
|
+
* });
|
|
887
|
+
* // Feed result.claims into buildSettlementTx() from Story 12.6.
|
|
888
|
+
* ```
|
|
889
|
+
*
|
|
890
|
+
* @throws {StreamSwapError} Only for construction-time validation failures
|
|
891
|
+
* (INVALID_AMOUNT, INVALID_CHUNKING, INVALID_PAIR, INVALID_STATE).
|
|
892
|
+
*/
|
|
893
|
+
declare function streamSwap(params: StreamSwapParams): Promise<StreamSwapResult>;
|
|
894
|
+
/**
|
|
895
|
+
* Two-form variant of {@link streamSwap} that additionally returns a
|
|
896
|
+
* {@link StreamSwapController} with `pause()` / `resume()` / `stop()`.
|
|
897
|
+
*
|
|
898
|
+
* @throws {StreamSwapError} Synchronously for construction-time validation.
|
|
899
|
+
*/
|
|
900
|
+
declare function streamSwapControlled(params: StreamSwapParams): {
|
|
901
|
+
result: Promise<StreamSwapResult>;
|
|
902
|
+
controller: StreamSwapController;
|
|
903
|
+
};
|
|
904
|
+
declare const __testing: {
|
|
905
|
+
chunkAmount: typeof chunkAmount;
|
|
906
|
+
decodeFulfillMetadata: typeof decodeFulfillMetadata;
|
|
907
|
+
buildSwapRumor: typeof buildSwapRumor;
|
|
908
|
+
compareDecimalRates: typeof compareDecimalRates;
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
export { type AccumulatedClaim as A, type StreamSwapClient as B, type DecryptFulfillClaimParams as D, type EncryptFulfillClaimParams as E, InMemorySwapControllerStateStore as I, JsonFileSwapControllerStateStore as J, type PacketObservation as P, type RateMonitorCallback as R, type StreamSwapAdaptiveController as S, type UnwrapSwapPacketFromToonParams as U, type WrapSwapPacketParams as W, __testing as _, AdaptiveDeltaController as a, type AdaptiveDeltaControllerConfig as b, type EncryptFulfillClaimResult as c, type PacketProgress as d, type PacketResolution as e, type StreamSwapController as f, type StreamSwapParams as g, type StreamSwapResult as h, SwapControllerError as i, type SwapControllerState as j, type SwapControllerStateStore as k, type UnwrapSwapPacketParams as l, type UnwrapSwapPacketResult as m, type WrapSwapPacketResult as n, type WrapSwapPacketToToonParams as o, type WrapSwapPacketToToonResult as p, decryptFulfillClaim as q, encryptFulfillClaim as r, isSwapControllerState as s, streamSwap as t, streamSwapControlled as u, swapControllerStateKey as v, unwrapSwapPacket as w, unwrapSwapPacketFromToon as x, wrapSwapPacket as y, wrapSwapPacketToToon as z };
|