@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.
@@ -1,484 +0,0 @@
1
- import { NostrEvent, UnsignedEvent } from 'nostr-tools/pure';
2
- import { IlpPreparePacket, SwapPair } 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
- /** Packet expiry. Default: 30s from now (inside buildIlpPrepare). */
63
- expiresAt?: Date;
64
- }
65
- /** Result of {@link wrapSwapPacketToToon}. */
66
- interface WrapSwapPacketToToonResult {
67
- /** Ready-to-send ILP PREPARE packet with TOON-encoded gift wrap as data. */
68
- ilpPrepare: IlpPreparePacket;
69
- /** The ephemeral pubkey used for the outer gift wrap layer. */
70
- ephemeralPubkey: string;
71
- }
72
- /** Parameters for {@link unwrapSwapPacketFromToon}. */
73
- interface UnwrapSwapPacketFromToonParams {
74
- /** The data field from an incoming ILP PREPARE (TOON-encoded gift wrap). */
75
- toonData: Uint8Array;
76
- /** Recipient's (Swap's) secret key. */
77
- recipientSecretKey: Uint8Array;
78
- }
79
- /** Parameters for {@link encryptFulfillClaim}. */
80
- interface EncryptFulfillClaimParams {
81
- /** The signed claim bytes to encrypt. */
82
- claimData: Uint8Array;
83
- /** The original sender's pubkey (recovered from unwrap). */
84
- senderPubkey: string;
85
- }
86
- /** Result of {@link encryptFulfillClaim}. */
87
- interface EncryptFulfillClaimResult {
88
- /** NIP-44 encrypted claim bytes. */
89
- ciphertext: Uint8Array;
90
- /** The Swap's ephemeral pubkey (included in FULFILL so sender can decrypt). */
91
- ephemeralPubkey: string;
92
- }
93
- /** Parameters for {@link decryptFulfillClaim}. */
94
- interface DecryptFulfillClaimParams {
95
- /** The encrypted claim bytes from the FULFILL data field. */
96
- ciphertext: Uint8Array;
97
- /** The Swap's ephemeral pubkey from the FULFILL data field. */
98
- ephemeralPubkey: string;
99
- /** The sender's (recipient of FULFILL) secret key. */
100
- recipientSecretKey: Uint8Array;
101
- }
102
- /**
103
- * NIP-59 three-layer gift wrap a swap packet.
104
- *
105
- * Constructs a kind:1059 gift wrap event using a fresh ephemeral keypair.
106
- * The rumor is sealed with the sender's real key, then wrapped with the
107
- * ephemeral key. Intermediaries see only the ephemeral pubkey.
108
- *
109
- * Each invocation generates a fresh ephemeral keypair for forward secrecy
110
- * and message unlinkability (risk R-006).
111
- *
112
- * @throws {GiftWrapError} If the wrap operation fails (invalid keys, crypto failure).
113
- */
114
- declare function wrapSwapPacket(params: WrapSwapPacketParams): WrapSwapPacketResult;
115
- /**
116
- * Unwrap a NIP-59 gift-wrapped swap packet, recovering the inner rumor
117
- * and the sender's real pubkey.
118
- *
119
- * Performs two-layer decryption: gift wrap -> seal -> rumor.
120
- * The sender pubkey is extracted from the seal's pubkey field.
121
- *
122
- * @throws {GiftWrapError} If kind is not 1059, decryption fails, or structure is malformed.
123
- */
124
- declare function unwrapSwapPacket(params: UnwrapSwapPacketParams): UnwrapSwapPacketResult;
125
- /**
126
- * Convenience: gift wrap a swap packet, encode to TOON binary, and build
127
- * an ILP PREPARE packet.
128
- *
129
- * This is the path that `streamSwap()` (Story 12.5) will use in its
130
- * per-packet loop.
131
- */
132
- declare function wrapSwapPacketToToon(params: WrapSwapPacketToToonParams): WrapSwapPacketToToonResult;
133
- /**
134
- * Convenience: decode TOON binary from an incoming ILP PREPARE data field
135
- * and unwrap the gift-wrapped swap packet.
136
- *
137
- * This is the path that the Swap handler (Story 12.3) will use to process
138
- * incoming swap packets.
139
- */
140
- declare function unwrapSwapPacketFromToon(params: UnwrapSwapPacketFromToonParams): UnwrapSwapPacketResult;
141
- /**
142
- * Encrypt a FULFILL claim for the return path (D12-008).
143
- *
144
- * Generates a fresh ephemeral keypair and NIP-44 encrypts the claim data.
145
- * The ephemeral pubkey is included alongside the ciphertext so the sender
146
- * can decrypt. The ephemeral privkey is discarded after encryption.
147
- *
148
- * @throws {GiftWrapError} If claimData is empty or encryption fails.
149
- */
150
- declare function encryptFulfillClaim(params: EncryptFulfillClaimParams): EncryptFulfillClaimResult;
151
- /**
152
- * Decrypt a FULFILL claim from the return path.
153
- *
154
- * Uses the sender's secret key and the Swap's ephemeral pubkey (from the
155
- * FULFILL data field) to NIP-44 decrypt the claim bytes.
156
- *
157
- * @throws {GiftWrapError} If decryption fails (wrong key, malformed ciphertext).
158
- */
159
- declare function decryptFulfillClaim(params: DecryptFulfillClaimParams): Uint8Array;
160
-
161
- /**
162
- * Sender-side `streamSwap()` API (Story 12.5).
163
- *
164
- * Drives the sender side of the Token Swap Primitive: chunks a total source
165
- * amount into N sender-chosen packets, wraps each with `wrapSwapPacketToToon()`
166
- * using a fresh ephemeral gift-wrap key per packet (D12-003 / risk R-006),
167
- * sends each via `ToonClient.sendSwapPacket(...)`, decrypts each FULFILL's
168
- * NIP-44-encrypted claim with `decryptFulfillClaim()`, accumulates the
169
- * decrypted claims into an ordered array, invokes a per-packet rate monitoring
170
- * callback, and supports pause / resume / stop / AbortSignal so the sender
171
- * can abort when rate drifts past tolerance.
172
- *
173
- * Composition story: consumes Stories 12.1 (SwapPair type), 12.2 (gift wrap
174
- * primitives), 12.3 (handler wire contract). Produces `AccumulatedClaim[]`
175
- * consumed by Story 12.6 (`buildSettlementTx()`).
176
- *
177
- * Design decisions:
178
- * - `streamSwap()` does NOT throw on mid-stream failure. Caller gets
179
- * `StreamSwapResult` with `state` and `abortReason`. Only construction-time
180
- * validation throws synchronously.
181
- * - `streamSwap()` does NOT retry individual packets. BTP-level connection
182
- * retries are handled inside `BtpRuntimeClient`; application-layer retry
183
- * (e.g., T04 insufficient inventory) is a future-story concern.
184
- * - Rate deviation math stays BigInt throughout. The `effectiveRate` on
185
- * `PacketProgress` is a display-only `number` (Epic 11 retro MAX_SAFE_INTEGER
186
- * guard).
187
- *
188
- * @module
189
- */
190
-
191
- /** Minimal `IlpSendResult` shape — mirrors `packages/client/src/types.ts`. */
192
- interface IlpSendResultLike {
193
- accepted: boolean;
194
- data?: string;
195
- code?: string;
196
- message?: string;
197
- }
198
- /**
199
- * Minimal `SignedBalanceProof` structural type.
200
- *
201
- * We avoid importing from `@toon-protocol/client` so the SDK package does not
202
- * gain a runtime dep on the client package. The sender layer only forwards
203
- * this opaquely to `ToonClient.sendSwapPacket`.
204
- */
205
- type SignedBalanceProofLike = any;
206
- /**
207
- * The subset of `ToonClient` that `streamSwap()` requires. This narrow shape
208
- * exists so that consumers can pass a real `ToonClient` OR a mock without
209
- * importing the whole class.
210
- */
211
- interface StreamSwapClient {
212
- sendSwapPacket(params: {
213
- destination: string;
214
- amount: bigint;
215
- toonData: Uint8Array;
216
- timeout?: number;
217
- claim?: SignedBalanceProofLike;
218
- }): Promise<IlpSendResultLike>;
219
- getPublicKey(): string;
220
- }
221
- /**
222
- * Parameters for {@link streamSwap} / {@link streamSwapControlled}.
223
- *
224
- * @stable — downstream Stories 12.6 and 12.8 depend on this shape.
225
- */
226
- interface StreamSwapParams {
227
- /** SDK client with BTP wiring (see {@link StreamSwapClient}). */
228
- client: StreamSwapClient;
229
- /** Swap's 64-char lowercase hex pubkey (recipient of gift wrap). */
230
- swapPubkey: string;
231
- /** Swap's ILP destination address (e.g., 'g.toon.swap1'). */
232
- swapIlpAddress: string;
233
- /** The `SwapPair` being executed (from kind:10032 discovery, Story 12.1). */
234
- pair: SwapPair;
235
- /** Sender's 32-byte secp256k1 secret key. Used for seal signing AND FULFILL decryption. */
236
- senderSecretKey: Uint8Array;
237
- /**
238
- * Sender's chain-specific payout address for `pair.to.chain` (Story 12.9 AC-4).
239
- *
240
- * REQUIRED. The Nostr `senderPubkey` (identity layer) cannot be used as the
241
- * on-chain `recipient` in a balance-proof because they are cryptographically
242
- * independent keys (D12-011). For `evm:*` chains this must be a 20-byte
243
- * lowercased `0x`-prefixed hex address; for `solana:*` a base58 pubkey that
244
- * decodes to 32 bytes; for `mina:*` a base58 public-key string. The value
245
- * is validated at `streamSwapControlled()` entry via `validateChainAddress`
246
- * and echoed on every rumor as the `chain-recipient` tag (AC-6).
247
- */
248
- chainRecipient: string;
249
- /** Total source-asset amount to swap (source micro-units). */
250
- totalAmount: bigint;
251
- /** Even-split packet count. EXACTLY ONE of this or `packetAmounts` is required. */
252
- packetCount?: number;
253
- /** Explicit per-packet amounts. EXACTLY ONE of this or `packetCount` is required. */
254
- packetAmounts?: readonly bigint[];
255
- /** Source-asset balance proof claim. Required unless ChannelManager is wired. */
256
- claim?: SignedBalanceProofLike;
257
- /** Rate monitoring callback (fires after each accepted FULFILL). */
258
- onPacket?: RateMonitorCallback;
259
- /** Rate deviation threshold (decimal, e.g., 0.02 = 2%). */
260
- rateDeviationThreshold?: number;
261
- /** Per-packet timeout in ms. Default 30000. */
262
- packetTimeoutMs?: number;
263
- /** Optional AbortSignal. */
264
- signal?: AbortSignal;
265
- /**
266
- * Optional pino-compatible logger. Defaults to a no-op.
267
- *
268
- * Note: `streamSwap` calls each method with a single structured-event object
269
- * (e.g., `logger.warn({ event: 'stream_swap.packet_rejected', code, ... })`).
270
- * Pino accepts this form (the object is logged as top-level fields and the
271
- * message is left empty); other loggers that expect `(msg, meta)` should
272
- * wrap accordingly.
273
- */
274
- logger?: {
275
- debug: (...a: unknown[]) => void;
276
- info: (...a: unknown[]) => void;
277
- warn: (...a: unknown[]) => void;
278
- error: (...a: unknown[]) => void;
279
- };
280
- }
281
- /**
282
- * Rate monitoring callback. Fires after each successful FULFILL decryption,
283
- * before the next packet is sent. If the callback throws or rejects, the
284
- * stream is treated as stopped.
285
- */
286
- type RateMonitorCallback = (progress: PacketProgress) => void | Promise<void>;
287
- /**
288
- * Progress payload delivered to {@link RateMonitorCallback}. Frozen to
289
- * prevent callback mutation.
290
- */
291
- interface PacketProgress {
292
- /** 0-indexed packet number within this streamSwap invocation. */
293
- index: number;
294
- /** Total number of packets scheduled. */
295
- total: number;
296
- /** Source-asset amount sent for this packet (micro-units). */
297
- sourceAmount: bigint;
298
- /** Target-asset claim amount derived for this packet (micro-units). */
299
- targetAmount: bigint;
300
- /** Rate advertised on the `SwapPair` at swap start (decimal string). */
301
- advertisedRate: string;
302
- /** Effective rate for this packet as JS Number (target / source in whole units). Display-only. */
303
- effectiveRate: number;
304
- /** Absolute deviation from advertisedRate as a decimal (e.g., 0.0125 = 1.25%). */
305
- rateDeviation: number;
306
- /** Cumulative source sent across accepted packets so far (including this one). */
307
- cumulativeSource: bigint;
308
- /** Cumulative target received so far (including this one). */
309
- cumulativeTarget: bigint;
310
- /** Controller state at callback time. */
311
- state: 'running' | 'paused' | 'stopped';
312
- }
313
- /**
314
- * An accumulated claim successfully harvested from a single packet.
315
- *
316
- * @stable — Story 12.6 (`buildSettlementTx()`) depends on this shape.
317
- * Breaking changes require a coordinated migration.
318
- *
319
- * Story 12.6 ADDITIVE extension: the settlement-context fields
320
- * `channelId`, `nonce`, `cumulativeAmount`, `recipient`, and
321
- * `swapSignerAddress` are marked optional (`?:`) for one story-cycle of
322
- * backward compat but are REQUIRED in practice: Story 12.6's
323
- * `buildSettlementTx()` throws `MISSING_SETTLEMENT_METADATA` when any of
324
- * these are absent.
325
- */
326
- interface AccumulatedClaim {
327
- /** 0-indexed position in the swap's packet stream. */
328
- packetIndex: number;
329
- /** Source-asset amount sent for this packet (micro-units). */
330
- sourceAmount: bigint;
331
- /**
332
- * Target-asset amount claimed (micro-units).
333
- *
334
- * **Source of truth caveat:** This is the expected target amount computed
335
- * by `applyRate(pair.rate)`. The actual signed-claim amount lives inside
336
- * `claimBytes`; Story 12.6 is responsible for parsing `claimBytes` per
337
- * chain and verifying the on-wire signed amount equals this expected amount.
338
- */
339
- targetAmount: bigint;
340
- /** Decrypted signed claim bytes. Chain-specific encoding per Story 12.4. */
341
- claimBytes: Uint8Array;
342
- /** Swap's ephemeral pubkey from the FULFILL (64-char lowercase hex). */
343
- swapEphemeralPubkey: string;
344
- /** Optional Swap-side claim ID (passed through from handler metadata). */
345
- claimId?: string;
346
- /** Swap pair this claim was priced against (copy of `pair` for settlement-time routing). */
347
- pair: SwapPair;
348
- /** Unix ms timestamp when this claim was accepted. */
349
- receivedAt: number;
350
- /** Channel identifier on the target chain (lowercase hex with 0x prefix for EVM; base58 for Solana). */
351
- channelId?: string;
352
- /** Balance-proof nonce (decimal string). Monotonically increasing within a channel. */
353
- nonce?: string;
354
- /** Cumulative transferred amount on the channel (target micro-units, decimal string). */
355
- cumulativeAmount?: string;
356
- /** Recipient address (the sender's target-asset address). */
357
- recipient?: string;
358
- /** Swap's on-chain signer address. */
359
- swapSignerAddress?: string;
360
- }
361
- /**
362
- * Aggregate result of a `streamSwap()` invocation.
363
- *
364
- * @stable — Stories 12.6 and 12.8 depend on this shape.
365
- */
366
- interface StreamSwapResult {
367
- state: 'completed' | 'failed' | 'stopped';
368
- claims: AccumulatedClaim[];
369
- rejections: {
370
- packetIndex: number;
371
- sourceAmount: bigint;
372
- code: string;
373
- message: string;
374
- }[];
375
- errors: {
376
- packetIndex: number;
377
- cause: Error;
378
- }[];
379
- abortReason: 'complete' | 'aborted' | 'stopped' | 'callback-stop' | 'callback-throw' | 'rate-deviation' | 'all-rejected';
380
- cumulativeSource: bigint;
381
- cumulativeTarget: bigint;
382
- packetsSent: number;
383
- packetsScheduled: number;
384
- }
385
- /** External controller returned by {@link streamSwapControlled}. */
386
- interface StreamSwapController {
387
- pause(): void;
388
- resume(): void;
389
- stop(): void;
390
- readonly state: 'running' | 'paused' | 'stopped' | 'completed' | 'failed';
391
- }
392
- /** Length-split `total` into `count` bigints. Remainder absorbed on last element. */
393
- declare function chunkAmount(total: bigint, count: number): bigint[];
394
- /** Build the kind:20032 unsigned "swap rumor" event per AC-4.
395
- *
396
- * Story 12.9 AC-1/AC-6: the returned rumor MUST include a `chain-recipient`
397
- * tag carrying the sender-supplied chain-format payout address for
398
- * `pair.to.chain`. The value is echoed verbatim per packet — no case-folding
399
- * or transformation beyond what the sender-side `validateChainAddress`
400
- * accepts.
401
- */
402
- declare function buildSwapRumor(input: {
403
- senderPubkey: string;
404
- pair: SwapPair;
405
- sourceAmount: bigint;
406
- packetIndex: number;
407
- totalPackets: number;
408
- nonce: Uint8Array;
409
- createdAt: number;
410
- chainRecipient: string;
411
- }): UnsignedEvent;
412
- /**
413
- * Decode the FULFILL response `data` (base64-encoded JSON metadata) into
414
- * the `{ claim, ephemeralPubkey, claimId?, targetAmount?, ...settlement }`
415
- * shape.
416
- *
417
- * Story 12.6 extension: also parses the settlement-context fields
418
- * (`channelId`, `nonce`, `cumulativeAmount`, `recipient`, `swapSignerAddress`).
419
- * These are OPTIONAL and best-effort (#153): each is threaded through only
420
- * when it is a well-formed string for the target chain; an absent or malformed
421
- * settlement field is silently dropped rather than failing the whole decode,
422
- * so a fulfilled swap still surfaces its signed `claim` + `ephemeralPubkey`.
423
- * Only `claim` and `ephemeralPubkey` are strictly required.
424
- *
425
- * @param chain Optional `pair.to.chain` string for per-chain format validation
426
- * of channelId / recipient / swapSignerAddress. When omitted, format checks
427
- * fall back to a length-only sanity check.
428
- */
429
- declare function decodeFulfillMetadata(data: string | undefined, chain?: string): {
430
- claim: string;
431
- ephemeralPubkey: string;
432
- claimId?: string;
433
- /** Optional Swap-reported actual target amount (decimal string). Used for rate deviation when present. */
434
- targetAmount?: string;
435
- channelId?: string;
436
- nonce?: string;
437
- cumulativeAmount?: string;
438
- recipient?: string;
439
- swapSignerAddress?: string;
440
- };
441
- /**
442
- * Drive a multi-packet swap against a Swap and return a `StreamSwapResult`.
443
- *
444
- * `streamSwap()` does NOT throw on mid-stream failure — inspect the result's
445
- * `state`, `abortReason`, `rejections[]`, and `errors[]` to diagnose.
446
- *
447
- * @example
448
- * ```ts
449
- * // Discover the SwapPair from the Swap's kind:10032 peer-info event (Story 12.1).
450
- * const result = await streamSwap({
451
- * client: toonClient,
452
- * swapPubkey: swap.pubkey,
453
- * swapIlpAddress: 'g.toon.swap1',
454
- * pair,
455
- * senderSecretKey,
456
- * totalAmount: 1_000_000n,
457
- * packetCount: 10,
458
- * onPacket: (p) => console.log('packet', p.index, 'rate', p.effectiveRate),
459
- * rateDeviationThreshold: 0.02,
460
- * });
461
- * // Feed result.claims into buildSettlementTx() from Story 12.6.
462
- * ```
463
- *
464
- * @throws {StreamSwapError} Only for construction-time validation failures
465
- * (INVALID_AMOUNT, INVALID_CHUNKING, INVALID_PAIR, INVALID_STATE).
466
- */
467
- declare function streamSwap(params: StreamSwapParams): Promise<StreamSwapResult>;
468
- /**
469
- * Two-form variant of {@link streamSwap} that additionally returns a
470
- * {@link StreamSwapController} with `pause()` / `resume()` / `stop()`.
471
- *
472
- * @throws {StreamSwapError} Synchronously for construction-time validation.
473
- */
474
- declare function streamSwapControlled(params: StreamSwapParams): {
475
- result: Promise<StreamSwapResult>;
476
- controller: StreamSwapController;
477
- };
478
- declare const __testing: {
479
- chunkAmount: typeof chunkAmount;
480
- decodeFulfillMetadata: typeof decodeFulfillMetadata;
481
- buildSwapRumor: typeof buildSwapRumor;
482
- };
483
-
484
- export { type AccumulatedClaim as A, type DecryptFulfillClaimParams as D, type EncryptFulfillClaimParams as E, type PacketProgress as P, type RateMonitorCallback as R, type StreamSwapController as S, type UnwrapSwapPacketFromToonParams as U, type WrapSwapPacketParams as W, __testing as _, type EncryptFulfillClaimResult as a, type StreamSwapParams as b, type StreamSwapResult as c, type UnwrapSwapPacketParams as d, type UnwrapSwapPacketResult as e, type WrapSwapPacketResult as f, type WrapSwapPacketToToonParams as g, type WrapSwapPacketToToonResult as h, decryptFulfillClaim as i, encryptFulfillClaim as j, streamSwapControlled as k, unwrapSwapPacketFromToon as l, wrapSwapPacketToToon as m, type StreamSwapClient as n, streamSwap as s, unwrapSwapPacket as u, wrapSwapPacket as w };