@toon-protocol/sdk 0.5.0 → 0.6.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/README.md +19 -19
- package/dist/chunk-XTHUXP63.js +1637 -0
- package/dist/chunk-XTHUXP63.js.map +1 -0
- package/dist/index.d.ts +1960 -0
- package/dist/index.js +1187 -301
- package/dist/index.js.map +1 -1
- package/dist/swap-D4Ozr_BM.d.ts +484 -0
- package/dist/swap.d.ts +3 -0
- package/dist/swap.js +14 -0
- package/dist/swap.js.map +1 -0
- package/package.json +23 -18
|
@@ -0,0 +1,1637 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__require
|
|
3
|
+
} from "./chunk-UP2VWCW5.js";
|
|
4
|
+
|
|
5
|
+
// src/errors.ts
|
|
6
|
+
import { ToonError } from "@toon-protocol/core";
|
|
7
|
+
var IdentityError = class extends ToonError {
|
|
8
|
+
constructor(message, cause) {
|
|
9
|
+
super(message, "IDENTITY_ERROR", cause);
|
|
10
|
+
this.name = "IdentityError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var NodeError = class extends ToonError {
|
|
14
|
+
constructor(message, cause) {
|
|
15
|
+
super(message, "NODE_ERROR", cause);
|
|
16
|
+
this.name = "NodeError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var HandlerError = class extends ToonError {
|
|
20
|
+
constructor(message, cause) {
|
|
21
|
+
super(message, "HANDLER_ERROR", cause);
|
|
22
|
+
this.name = "HandlerError";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var VerificationError = class extends ToonError {
|
|
26
|
+
constructor(message, cause) {
|
|
27
|
+
super(message, "VERIFICATION_ERROR", cause);
|
|
28
|
+
this.name = "VerificationError";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var PricingError = class extends ToonError {
|
|
32
|
+
constructor(message, cause) {
|
|
33
|
+
super(message, "PRICING_ERROR", cause);
|
|
34
|
+
this.name = "PricingError";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var GiftWrapError = class extends ToonError {
|
|
38
|
+
constructor(message, cause) {
|
|
39
|
+
super(message, "GIFT_WRAP_ERROR", cause);
|
|
40
|
+
this.name = "GiftWrapError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var SwapHandlerError = class extends ToonError {
|
|
44
|
+
constructor(message, cause) {
|
|
45
|
+
super(message, "SWAP_HANDLER_ERROR", cause);
|
|
46
|
+
this.name = "SwapHandlerError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var StreamSwapError = class extends Error {
|
|
50
|
+
code;
|
|
51
|
+
constructor(code, message, options) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "StreamSwapError";
|
|
54
|
+
this.code = code;
|
|
55
|
+
if (options && "cause" in options) {
|
|
56
|
+
Object.defineProperty(this, "cause", {
|
|
57
|
+
value: options.cause,
|
|
58
|
+
enumerable: false,
|
|
59
|
+
writable: true,
|
|
60
|
+
configurable: true
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var SettlementTxError = class extends Error {
|
|
66
|
+
code;
|
|
67
|
+
constructor(code, message, options) {
|
|
68
|
+
super(message);
|
|
69
|
+
this.name = "SettlementTxError";
|
|
70
|
+
this.code = code;
|
|
71
|
+
if (options && "cause" in options) {
|
|
72
|
+
Object.defineProperty(this, "cause", {
|
|
73
|
+
value: options.cause,
|
|
74
|
+
enumerable: false,
|
|
75
|
+
writable: true,
|
|
76
|
+
configurable: true
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// src/identity.ts
|
|
83
|
+
import {
|
|
84
|
+
generateMnemonic as _generateMnemonic,
|
|
85
|
+
validateMnemonic,
|
|
86
|
+
mnemonicToSeedSync
|
|
87
|
+
} from "@scure/bip39";
|
|
88
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
89
|
+
import { HDKey } from "@scure/bip32";
|
|
90
|
+
import { getPublicKey } from "nostr-tools/pure";
|
|
91
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
92
|
+
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
93
|
+
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
94
|
+
import { hmac } from "@noble/hashes/hmac.js";
|
|
95
|
+
import { sha512 } from "@noble/hashes/sha2.js";
|
|
96
|
+
import { bytesToHex } from "@noble/hashes/utils.js";
|
|
97
|
+
import { hexToMinaBase58PrivateKey } from "@toon-protocol/core";
|
|
98
|
+
function generateMnemonic() {
|
|
99
|
+
return _generateMnemonic(wordlist, 128);
|
|
100
|
+
}
|
|
101
|
+
function fromMnemonic(mnemonic, options) {
|
|
102
|
+
if (!validateMnemonic(mnemonic, wordlist)) {
|
|
103
|
+
throw new IdentityError(
|
|
104
|
+
`Invalid BIP-39 mnemonic: the provided words do not form a valid mnemonic phrase`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const accountIndex = options?.accountIndex ?? 0;
|
|
108
|
+
if (!Number.isInteger(accountIndex) || accountIndex < 0 || accountIndex > MAX_BIP32_INDEX) {
|
|
109
|
+
throw new IdentityError(
|
|
110
|
+
`Invalid accountIndex: expected a non-negative integer (0 to ${MAX_BIP32_INDEX}), got ${String(accountIndex)}`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const path = `m/44'/1237'/0'/0/${accountIndex}`;
|
|
114
|
+
let seed;
|
|
115
|
+
try {
|
|
116
|
+
seed = mnemonicToSeedSync(mnemonic);
|
|
117
|
+
const hdKey = HDKey.fromMasterSeed(seed).derive(path);
|
|
118
|
+
if (!hdKey.privateKey) {
|
|
119
|
+
throw new IdentityError(`Failed to derive private key at path ${path}`);
|
|
120
|
+
}
|
|
121
|
+
const secretKey = hdKey.privateKey;
|
|
122
|
+
const base = deriveIdentity(secretKey);
|
|
123
|
+
const solana = deriveSolanaIdentity(seed, accountIndex);
|
|
124
|
+
return { ...base, solana };
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (error instanceof IdentityError) {
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
throw new IdentityError(
|
|
130
|
+
`Key derivation failed at path ${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
131
|
+
error instanceof Error ? error : void 0
|
|
132
|
+
);
|
|
133
|
+
} finally {
|
|
134
|
+
if (seed) {
|
|
135
|
+
seed.fill(0);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function fromSecretKey(secretKey) {
|
|
140
|
+
if (!(secretKey instanceof Uint8Array)) {
|
|
141
|
+
throw new IdentityError(
|
|
142
|
+
`Invalid secret key: expected Uint8Array, got ${secretKey === null ? "null" : typeof secretKey}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (secretKey.length !== 32) {
|
|
146
|
+
throw new IdentityError(
|
|
147
|
+
`Invalid secret key: expected 32 bytes, got ${secretKey.length} bytes`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
return deriveIdentity(secretKey);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error instanceof IdentityError) {
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
throw new IdentityError(
|
|
157
|
+
`Invalid secret key: ${error instanceof Error ? error.message : String(error)}`,
|
|
158
|
+
error instanceof Error ? error : void 0
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
var MAX_BIP32_INDEX = 2147483647;
|
|
163
|
+
function deriveIdentity(secretKey) {
|
|
164
|
+
const pubkey = getPublicKey(secretKey);
|
|
165
|
+
const evmAddress = computeEvmAddress(secretKey);
|
|
166
|
+
return { secretKey: new Uint8Array(secretKey), pubkey, evmAddress };
|
|
167
|
+
}
|
|
168
|
+
function computeEvmAddress(secretKey) {
|
|
169
|
+
const uncompressedPubkey = secp256k1.getPublicKey(secretKey, false);
|
|
170
|
+
const pubkeyWithoutPrefix = uncompressedPubkey.slice(1);
|
|
171
|
+
const hash = keccak_256(pubkeyWithoutPrefix);
|
|
172
|
+
const addressBytes = hash.slice(-20);
|
|
173
|
+
const addressHex = bytesToHex(addressBytes);
|
|
174
|
+
return toChecksumAddress(addressHex);
|
|
175
|
+
}
|
|
176
|
+
function toChecksumAddress(addressHex) {
|
|
177
|
+
const lower = addressHex.toLowerCase();
|
|
178
|
+
const hash = bytesToHex(keccak_256(new TextEncoder().encode(lower)));
|
|
179
|
+
let checksummed = "0x";
|
|
180
|
+
for (let i = 0; i < 40; i++) {
|
|
181
|
+
const char = lower[i];
|
|
182
|
+
const hashChar = hash[i];
|
|
183
|
+
if (char === void 0 || hashChar === void 0) {
|
|
184
|
+
throw new IdentityError(
|
|
185
|
+
`Unexpected undefined at index ${i} during checksum computation`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const hashNibble = parseInt(hashChar, 16);
|
|
189
|
+
checksummed += hashNibble >= 8 ? char.toUpperCase() : char;
|
|
190
|
+
}
|
|
191
|
+
return checksummed;
|
|
192
|
+
}
|
|
193
|
+
function slip0010Derive(seed, path) {
|
|
194
|
+
const encoder = new TextEncoder();
|
|
195
|
+
let I = hmac(sha512, encoder.encode("ed25519 seed"), seed);
|
|
196
|
+
let key = I.slice(0, 32);
|
|
197
|
+
let chainCode = I.slice(32);
|
|
198
|
+
for (const index of path) {
|
|
199
|
+
const data = new Uint8Array(37);
|
|
200
|
+
data[0] = 0;
|
|
201
|
+
data.set(key, 1);
|
|
202
|
+
data[33] = index >>> 24 & 255;
|
|
203
|
+
data[34] = index >>> 16 & 255;
|
|
204
|
+
data[35] = index >>> 8 & 255;
|
|
205
|
+
data[36] = index & 255;
|
|
206
|
+
I = hmac(sha512, chainCode, data);
|
|
207
|
+
key = I.slice(0, 32);
|
|
208
|
+
chainCode = I.slice(32);
|
|
209
|
+
}
|
|
210
|
+
return key;
|
|
211
|
+
}
|
|
212
|
+
function solanaPath(accountIndex) {
|
|
213
|
+
return [
|
|
214
|
+
2147483692,
|
|
215
|
+
// 44'
|
|
216
|
+
2147484149,
|
|
217
|
+
// 501'
|
|
218
|
+
2147483648 + accountIndex >>> 0,
|
|
219
|
+
// {accountIndex}'
|
|
220
|
+
2147483648
|
|
221
|
+
// 0'
|
|
222
|
+
];
|
|
223
|
+
}
|
|
224
|
+
function deriveSolanaIdentity(seed, accountIndex = 0) {
|
|
225
|
+
const privateKey = slip0010Derive(seed, solanaPath(accountIndex));
|
|
226
|
+
const publicKeyBytes = ed25519.getPublicKey(privateKey);
|
|
227
|
+
const keypair = new Uint8Array(64);
|
|
228
|
+
keypair.set(privateKey, 0);
|
|
229
|
+
keypair.set(publicKeyBytes, 32);
|
|
230
|
+
return { secretKey: keypair, publicKey: base58Encode(publicKeyBytes) };
|
|
231
|
+
}
|
|
232
|
+
async function deriveMinaIdentity(seed, accountIndex = 0) {
|
|
233
|
+
const path = `m/44'/12586'/${accountIndex}'/0/0`;
|
|
234
|
+
const hdKey = HDKey.fromMasterSeed(seed).derive(path);
|
|
235
|
+
if (!hdKey.privateKey) {
|
|
236
|
+
throw new IdentityError(
|
|
237
|
+
`Failed to derive Mina private key at path ${path}`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
const keyBytes = new Uint8Array(hdKey.privateKey);
|
|
241
|
+
keyBytes[0] = (keyBytes[0] ?? 0) & 63;
|
|
242
|
+
const hexKey = bytesToHex(keyBytes);
|
|
243
|
+
try {
|
|
244
|
+
const MinaSignerLib = await import("mina-signer");
|
|
245
|
+
const Client = "default" in MinaSignerLib ? MinaSignerLib.default : MinaSignerLib;
|
|
246
|
+
const client = new Client({ network: "mainnet" });
|
|
247
|
+
const minaBase58PrivateKey = hexToMinaBase58PrivateKey(hexKey);
|
|
248
|
+
const publicKey = client.derivePublicKey(minaBase58PrivateKey);
|
|
249
|
+
return { privateKey: hexKey, publicKey };
|
|
250
|
+
} catch (err) {
|
|
251
|
+
const code = err?.code;
|
|
252
|
+
if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
|
|
253
|
+
return void 0;
|
|
254
|
+
}
|
|
255
|
+
throw new IdentityError(
|
|
256
|
+
`Mina identity derivation failed at path ${path}: ${err instanceof Error ? err.message : String(err)}`,
|
|
257
|
+
err instanceof Error ? err : void 0
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function fromMnemonicFull(mnemonic, options) {
|
|
262
|
+
const identity = fromMnemonic(mnemonic, options);
|
|
263
|
+
const accountIndex = options?.accountIndex ?? 0;
|
|
264
|
+
let seed;
|
|
265
|
+
try {
|
|
266
|
+
seed = mnemonicToSeedSync(mnemonic);
|
|
267
|
+
const mina = await deriveMinaIdentity(seed, accountIndex);
|
|
268
|
+
if (mina) {
|
|
269
|
+
return { ...identity, mina };
|
|
270
|
+
}
|
|
271
|
+
} finally {
|
|
272
|
+
if (seed) {
|
|
273
|
+
seed.fill(0);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return identity;
|
|
277
|
+
}
|
|
278
|
+
function generateSolanaKeypair() {
|
|
279
|
+
const privateKey = ed25519.utils.randomSecretKey();
|
|
280
|
+
const publicKeyBytes = ed25519.getPublicKey(privateKey);
|
|
281
|
+
const keypair = new Uint8Array(64);
|
|
282
|
+
keypair.set(privateKey, 0);
|
|
283
|
+
keypair.set(publicKeyBytes, 32);
|
|
284
|
+
return { secretKey: keypair, publicKey: base58Encode(publicKeyBytes) };
|
|
285
|
+
}
|
|
286
|
+
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
287
|
+
function base58Encode(bytes) {
|
|
288
|
+
let zeros = 0;
|
|
289
|
+
for (let i = 0; i < bytes.length && bytes[i] === 0; i++) zeros++;
|
|
290
|
+
let value = 0n;
|
|
291
|
+
for (const byte of bytes) {
|
|
292
|
+
value = value * 256n + BigInt(byte);
|
|
293
|
+
}
|
|
294
|
+
let result = "";
|
|
295
|
+
while (value > 0n) {
|
|
296
|
+
result = BASE58_ALPHABET[Number(value % 58n)] + result;
|
|
297
|
+
value = value / 58n;
|
|
298
|
+
}
|
|
299
|
+
for (let i = 0; i < zeros; i++) {
|
|
300
|
+
result = "1" + result;
|
|
301
|
+
}
|
|
302
|
+
return result || "1";
|
|
303
|
+
}
|
|
304
|
+
function base58Decode(str) {
|
|
305
|
+
let zeros = 0;
|
|
306
|
+
for (let i = 0; i < str.length && str[i] === "1"; i++) zeros++;
|
|
307
|
+
let value = 0n;
|
|
308
|
+
for (const ch of str) {
|
|
309
|
+
const idx = BASE58_ALPHABET.indexOf(ch);
|
|
310
|
+
if (idx === -1) throw new IdentityError(`Invalid base58 character: ${ch}`);
|
|
311
|
+
value = value * 58n + BigInt(idx);
|
|
312
|
+
}
|
|
313
|
+
const hex = value === 0n ? "" : value.toString(16);
|
|
314
|
+
const hexPadded = hex.length % 2 ? "0" + hex : hex;
|
|
315
|
+
const rawBytes = [];
|
|
316
|
+
for (let i = 0; i < hexPadded.length; i += 2) {
|
|
317
|
+
rawBytes.push(parseInt(hexPadded.slice(i, i + 2), 16));
|
|
318
|
+
}
|
|
319
|
+
const result = new Uint8Array(zeros + rawBytes.length);
|
|
320
|
+
result.set(rawBytes, zeros);
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/gift-wrap.ts
|
|
325
|
+
import { generateSecretKey, getPublicKey as getPublicKey2 } from "nostr-tools/pure";
|
|
326
|
+
import { createRumor, createSeal, createWrap } from "nostr-tools/nip59";
|
|
327
|
+
import {
|
|
328
|
+
encrypt as nip44Encrypt,
|
|
329
|
+
decrypt as nip44Decrypt,
|
|
330
|
+
getConversationKey
|
|
331
|
+
} from "nostr-tools/nip44";
|
|
332
|
+
import {
|
|
333
|
+
encodeEventToToon,
|
|
334
|
+
decodeEventFromToon
|
|
335
|
+
} from "@toon-protocol/core/toon";
|
|
336
|
+
import { buildIlpPrepare } from "@toon-protocol/core";
|
|
337
|
+
function validateSecretKey(key, paramName) {
|
|
338
|
+
if (!(key instanceof Uint8Array) || key.length !== 32) {
|
|
339
|
+
throw new GiftWrapError(
|
|
340
|
+
`${paramName} must be a 32-byte Uint8Array, got ${key instanceof Uint8Array ? `${key.length} bytes` : typeof key}`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function validatePubkey(pubkey, paramName) {
|
|
345
|
+
if (typeof pubkey !== "string" || !/^[0-9a-f]{64}$/.test(pubkey)) {
|
|
346
|
+
throw new GiftWrapError(
|
|
347
|
+
`${paramName} must be a 64-character lowercase hex string`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function wrapSwapPacket(params) {
|
|
352
|
+
const { rumor, senderSecretKey, recipientPubkey } = params;
|
|
353
|
+
validateSecretKey(senderSecretKey, "senderSecretKey");
|
|
354
|
+
validatePubkey(recipientPubkey, "recipientPubkey");
|
|
355
|
+
try {
|
|
356
|
+
const rumorEvent = createRumor(rumor, senderSecretKey);
|
|
357
|
+
const seal = createSeal(rumorEvent, senderSecretKey, recipientPubkey);
|
|
358
|
+
const giftWrap = createWrap(seal, recipientPubkey);
|
|
359
|
+
const ephemeralPubkey = giftWrap.pubkey;
|
|
360
|
+
return { giftWrap, ephemeralPubkey };
|
|
361
|
+
} catch (error) {
|
|
362
|
+
throw new GiftWrapError(
|
|
363
|
+
`Failed to wrap swap packet: ${error instanceof Error ? error.message : String(error)}`,
|
|
364
|
+
error instanceof Error ? error : void 0
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function unwrapSwapPacket(params) {
|
|
369
|
+
const { giftWrap, recipientSecretKey } = params;
|
|
370
|
+
validateSecretKey(recipientSecretKey, "recipientSecretKey");
|
|
371
|
+
if (!giftWrap || typeof giftWrap !== "object") {
|
|
372
|
+
throw new GiftWrapError("giftWrap must be a non-null object");
|
|
373
|
+
}
|
|
374
|
+
if (giftWrap.kind !== 1059) {
|
|
375
|
+
throw new GiftWrapError("Expected kind:1059 gift wrap");
|
|
376
|
+
}
|
|
377
|
+
let conversationKey1 = null;
|
|
378
|
+
let conversationKey2 = null;
|
|
379
|
+
try {
|
|
380
|
+
conversationKey1 = getConversationKey(recipientSecretKey, giftWrap.pubkey);
|
|
381
|
+
const sealJson = nip44Decrypt(giftWrap.content, conversationKey1);
|
|
382
|
+
const seal = JSON.parse(sealJson);
|
|
383
|
+
if (seal.kind !== 13) {
|
|
384
|
+
throw new GiftWrapError(
|
|
385
|
+
`Expected kind:13 seal inside gift wrap, got kind:${seal.kind}`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
const senderPubkey = seal.pubkey;
|
|
389
|
+
validatePubkey(senderPubkey, "seal.pubkey (sender identity)");
|
|
390
|
+
conversationKey2 = getConversationKey(recipientSecretKey, senderPubkey);
|
|
391
|
+
const rumorJson = nip44Decrypt(seal.content, conversationKey2);
|
|
392
|
+
const rumor = JSON.parse(rumorJson);
|
|
393
|
+
delete rumor.sig;
|
|
394
|
+
return { rumor, senderPubkey };
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (error instanceof GiftWrapError) {
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
throw new GiftWrapError(
|
|
400
|
+
`Failed to unwrap swap packet: ${error instanceof Error ? error.message : String(error)}`,
|
|
401
|
+
error instanceof Error ? error : void 0
|
|
402
|
+
);
|
|
403
|
+
} finally {
|
|
404
|
+
if (conversationKey1) {
|
|
405
|
+
conversationKey1.fill(0);
|
|
406
|
+
conversationKey1 = null;
|
|
407
|
+
}
|
|
408
|
+
if (conversationKey2) {
|
|
409
|
+
conversationKey2.fill(0);
|
|
410
|
+
conversationKey2 = null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function wrapSwapPacketToToon(params) {
|
|
415
|
+
const {
|
|
416
|
+
rumor,
|
|
417
|
+
senderSecretKey,
|
|
418
|
+
recipientPubkey,
|
|
419
|
+
destination,
|
|
420
|
+
amount,
|
|
421
|
+
expiresAt
|
|
422
|
+
} = params;
|
|
423
|
+
const { giftWrap, ephemeralPubkey } = wrapSwapPacket({
|
|
424
|
+
rumor,
|
|
425
|
+
senderSecretKey,
|
|
426
|
+
recipientPubkey
|
|
427
|
+
});
|
|
428
|
+
const toonBinary = encodeEventToToon(giftWrap);
|
|
429
|
+
const ilpPrepare = buildIlpPrepare({
|
|
430
|
+
destination,
|
|
431
|
+
amount,
|
|
432
|
+
data: toonBinary,
|
|
433
|
+
expiresAt
|
|
434
|
+
});
|
|
435
|
+
return { ilpPrepare, ephemeralPubkey };
|
|
436
|
+
}
|
|
437
|
+
function unwrapSwapPacketFromToon(params) {
|
|
438
|
+
const { toonData, recipientSecretKey } = params;
|
|
439
|
+
if (!(toonData instanceof Uint8Array) || toonData.length === 0) {
|
|
440
|
+
throw new GiftWrapError("toonData must be a non-empty Uint8Array");
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
const giftWrap = decodeEventFromToon(toonData);
|
|
444
|
+
return unwrapSwapPacket({ giftWrap, recipientSecretKey });
|
|
445
|
+
} catch (error) {
|
|
446
|
+
if (error instanceof GiftWrapError) {
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
throw new GiftWrapError(
|
|
450
|
+
`Failed to unwrap swap packet from TOON: ${error instanceof Error ? error.message : String(error)}`,
|
|
451
|
+
error instanceof Error ? error : void 0
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function encryptFulfillClaim(params) {
|
|
456
|
+
const { claimData, senderPubkey } = params;
|
|
457
|
+
validatePubkey(senderPubkey, "senderPubkey");
|
|
458
|
+
if (!(claimData instanceof Uint8Array)) {
|
|
459
|
+
throw new GiftWrapError(
|
|
460
|
+
`claimData must be a Uint8Array, got ${typeof claimData}`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
if (claimData.length === 0) {
|
|
464
|
+
throw new GiftWrapError("claimData must not be empty");
|
|
465
|
+
}
|
|
466
|
+
let ephemeralSecretKey = generateSecretKey();
|
|
467
|
+
const ephemeralPubkey = getPublicKey2(ephemeralSecretKey);
|
|
468
|
+
let conversationKey = null;
|
|
469
|
+
try {
|
|
470
|
+
conversationKey = getConversationKey(ephemeralSecretKey, senderPubkey);
|
|
471
|
+
const claimString = Buffer.from(claimData).toString("base64");
|
|
472
|
+
const ciphertextString = nip44Encrypt(claimString, conversationKey);
|
|
473
|
+
const ciphertext = new TextEncoder().encode(ciphertextString);
|
|
474
|
+
return { ciphertext, ephemeralPubkey };
|
|
475
|
+
} catch (error) {
|
|
476
|
+
if (error instanceof GiftWrapError) {
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
479
|
+
throw new GiftWrapError(
|
|
480
|
+
`Failed to encrypt FULFILL claim: ${error instanceof Error ? error.message : String(error)}`,
|
|
481
|
+
error instanceof Error ? error : void 0
|
|
482
|
+
);
|
|
483
|
+
} finally {
|
|
484
|
+
if (ephemeralSecretKey) {
|
|
485
|
+
ephemeralSecretKey.fill(0);
|
|
486
|
+
ephemeralSecretKey = null;
|
|
487
|
+
}
|
|
488
|
+
if (conversationKey) {
|
|
489
|
+
conversationKey.fill(0);
|
|
490
|
+
conversationKey = null;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function decryptFulfillClaim(params) {
|
|
495
|
+
const { ciphertext, ephemeralPubkey, recipientSecretKey } = params;
|
|
496
|
+
validateSecretKey(recipientSecretKey, "recipientSecretKey");
|
|
497
|
+
validatePubkey(ephemeralPubkey, "ephemeralPubkey");
|
|
498
|
+
if (!(ciphertext instanceof Uint8Array) || ciphertext.length === 0) {
|
|
499
|
+
throw new GiftWrapError("ciphertext must be a non-empty Uint8Array");
|
|
500
|
+
}
|
|
501
|
+
let conversationKey = null;
|
|
502
|
+
try {
|
|
503
|
+
conversationKey = getConversationKey(recipientSecretKey, ephemeralPubkey);
|
|
504
|
+
const ciphertextString = new TextDecoder().decode(ciphertext);
|
|
505
|
+
const claimString = nip44Decrypt(ciphertextString, conversationKey);
|
|
506
|
+
return new Uint8Array(Buffer.from(claimString, "base64"));
|
|
507
|
+
} catch (error) {
|
|
508
|
+
if (error instanceof GiftWrapError) {
|
|
509
|
+
throw error;
|
|
510
|
+
}
|
|
511
|
+
throw new GiftWrapError(
|
|
512
|
+
`Failed to decrypt FULFILL claim: ${error instanceof Error ? error.message : String(error)}`,
|
|
513
|
+
error instanceof Error ? error : void 0
|
|
514
|
+
);
|
|
515
|
+
} finally {
|
|
516
|
+
if (conversationKey) {
|
|
517
|
+
conversationKey.fill(0);
|
|
518
|
+
conversationKey = null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/swap-handler.ts
|
|
524
|
+
import { createHash } from "crypto";
|
|
525
|
+
var DEFAULT_SEEN_PACKET_IDS_CAP = 1e4;
|
|
526
|
+
var SWAP_HANDLER_REJECT_CODES = {
|
|
527
|
+
/** Malformed / invalid PREPARE content — gift-wrap shape or amount invalid. */
|
|
528
|
+
INVALID_GIFT_WRAP: "F01",
|
|
529
|
+
/** No route — the handler did not match a registered destination. */
|
|
530
|
+
UNREACHABLE: "F02",
|
|
531
|
+
/** Duplicate packet — `seenPacketIds` replay hit. */
|
|
532
|
+
DUPLICATE_PACKET: "F04",
|
|
533
|
+
/** Requested swap pair is not advertised by this Mill. */
|
|
534
|
+
UNSUPPORTED_PAIR: "F06",
|
|
535
|
+
/** Transient internal failure — signing, rate provider, or unexpected. */
|
|
536
|
+
INTERNAL: "T00",
|
|
537
|
+
/** Insufficient Mill inventory for the requested amount. */
|
|
538
|
+
INSUFFICIENT_LIQUIDITY: "T04"
|
|
539
|
+
};
|
|
540
|
+
var SWAP_HANDLER_REJECT_MESSAGES = {
|
|
541
|
+
INVALID_GIFT_WRAP: "Invalid gift wrap",
|
|
542
|
+
INVALID_AMOUNT: "Invalid amount",
|
|
543
|
+
UNREACHABLE: "Unreachable",
|
|
544
|
+
DUPLICATE_PACKET: "Duplicate packet",
|
|
545
|
+
UNSUPPORTED_PAIR: "Unsupported swap pair",
|
|
546
|
+
INTERNAL: "Internal error",
|
|
547
|
+
INSUFFICIENT_LIQUIDITY: "Insufficient liquidity",
|
|
548
|
+
RATE_PROVIDER: "Rate provider error",
|
|
549
|
+
RATE_CONVERSION: "Rate conversion error"
|
|
550
|
+
};
|
|
551
|
+
var BoundedSeenPacketIds = class {
|
|
552
|
+
#map = /* @__PURE__ */ new Map();
|
|
553
|
+
#cap;
|
|
554
|
+
[Symbol.toStringTag] = "Set";
|
|
555
|
+
constructor(cap = DEFAULT_SEEN_PACKET_IDS_CAP) {
|
|
556
|
+
if (!Number.isInteger(cap) || cap <= 0) {
|
|
557
|
+
throw new Error(
|
|
558
|
+
`BoundedSeenPacketIds cap must be a positive integer, got ${cap}`
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
this.#cap = cap;
|
|
562
|
+
}
|
|
563
|
+
get size() {
|
|
564
|
+
return this.#map.size;
|
|
565
|
+
}
|
|
566
|
+
/** Exposed for AC-10 test introspection. */
|
|
567
|
+
get cap() {
|
|
568
|
+
return this.#cap;
|
|
569
|
+
}
|
|
570
|
+
has(value) {
|
|
571
|
+
if (!this.#map.has(value)) return false;
|
|
572
|
+
this.#map.delete(value);
|
|
573
|
+
this.#map.set(value, true);
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
add(value) {
|
|
577
|
+
if (this.#map.has(value)) {
|
|
578
|
+
this.#map.delete(value);
|
|
579
|
+
this.#map.set(value, true);
|
|
580
|
+
return this;
|
|
581
|
+
}
|
|
582
|
+
this.#map.set(value, true);
|
|
583
|
+
while (this.#map.size > this.#cap) {
|
|
584
|
+
const first = this.#map.keys().next();
|
|
585
|
+
if (first.done) break;
|
|
586
|
+
this.#map.delete(first.value);
|
|
587
|
+
}
|
|
588
|
+
return this;
|
|
589
|
+
}
|
|
590
|
+
delete(value) {
|
|
591
|
+
return this.#map.delete(value);
|
|
592
|
+
}
|
|
593
|
+
clear() {
|
|
594
|
+
this.#map.clear();
|
|
595
|
+
}
|
|
596
|
+
forEach(callback, thisArg) {
|
|
597
|
+
for (const k of this.#map.keys()) {
|
|
598
|
+
callback.call(thisArg, k, k, this);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
*[Symbol.iterator]() {
|
|
602
|
+
yield* this.#map.keys();
|
|
603
|
+
}
|
|
604
|
+
*keys() {
|
|
605
|
+
yield* this.#map.keys();
|
|
606
|
+
}
|
|
607
|
+
*values() {
|
|
608
|
+
yield* this.#map.keys();
|
|
609
|
+
}
|
|
610
|
+
*entries() {
|
|
611
|
+
for (const k of this.#map.keys()) yield [k, k];
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
var RATE_REGEX = /^(0|[1-9]\d*)(\.\d+)?$/;
|
|
615
|
+
function applyRate(params) {
|
|
616
|
+
const { sourceAmount, fromScale, toScale, rate } = params;
|
|
617
|
+
if (!RATE_REGEX.test(rate)) {
|
|
618
|
+
throw new SwapHandlerError(`Invalid rate format: ${rate}`);
|
|
619
|
+
}
|
|
620
|
+
if (/^0(\.0+)?$/.test(rate)) {
|
|
621
|
+
throw new SwapHandlerError("Rate is zero (pair not quoting)");
|
|
622
|
+
}
|
|
623
|
+
if (sourceAmount <= 0n) {
|
|
624
|
+
throw new SwapHandlerError(
|
|
625
|
+
`sourceAmount must be positive, got ${sourceAmount}`
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
const dotIdx = rate.indexOf(".");
|
|
629
|
+
const integerPart = dotIdx === -1 ? rate : rate.slice(0, dotIdx);
|
|
630
|
+
const fractionalPart = dotIdx === -1 ? "" : rate.slice(dotIdx + 1);
|
|
631
|
+
const rateNumerator = BigInt(integerPart + fractionalPart);
|
|
632
|
+
const rateDenominator = 10n ** BigInt(fractionalPart.length);
|
|
633
|
+
const scaleUp = 10n ** BigInt(toScale);
|
|
634
|
+
const scaleDown = 10n ** BigInt(fromScale);
|
|
635
|
+
return sourceAmount * rateNumerator * scaleUp / (rateDenominator * scaleDown);
|
|
636
|
+
}
|
|
637
|
+
function findSwapPair(rumor, pairs) {
|
|
638
|
+
const fromTag = findTagValue(rumor, "swap-from");
|
|
639
|
+
const toTag = findTagValue(rumor, "swap-to");
|
|
640
|
+
if (!fromTag || !toTag) return null;
|
|
641
|
+
const fromParts = splitAssetChain(fromTag);
|
|
642
|
+
const toParts = splitAssetChain(toTag);
|
|
643
|
+
if (!fromParts || !toParts) return null;
|
|
644
|
+
for (const pair of pairs) {
|
|
645
|
+
if (pair.from.assetCode === fromParts.assetCode && pair.from.chain === fromParts.chain && pair.to.assetCode === toParts.assetCode && pair.to.chain === toParts.chain) {
|
|
646
|
+
return pair;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
function findTagValue(rumor, tagName) {
|
|
652
|
+
if (!Array.isArray(rumor.tags)) return void 0;
|
|
653
|
+
for (const t of rumor.tags) {
|
|
654
|
+
if (Array.isArray(t) && t[0] === tagName && typeof t[1] === "string") {
|
|
655
|
+
return t[1];
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return void 0;
|
|
659
|
+
}
|
|
660
|
+
function splitAssetChain(raw) {
|
|
661
|
+
const idx = raw.indexOf(":");
|
|
662
|
+
if (idx <= 0 || idx === raw.length - 1) return null;
|
|
663
|
+
const assetCode = raw.slice(0, idx);
|
|
664
|
+
const chain = raw.slice(idx + 1);
|
|
665
|
+
if (!assetCode || !chain) return null;
|
|
666
|
+
return { assetCode, chain };
|
|
667
|
+
}
|
|
668
|
+
var SWAP_HANDLER_EVM_ADDRESS_REGEX = /^0x[0-9a-f]{40}$/;
|
|
669
|
+
var SWAP_HANDLER_BASE58_REGEX = /^[1-9A-HJ-NP-Za-km-z]+$/;
|
|
670
|
+
function validateChainRecipient(value, chain) {
|
|
671
|
+
if (typeof value !== "string" || value.length === 0) return false;
|
|
672
|
+
if (chain.startsWith("evm:")) {
|
|
673
|
+
return SWAP_HANDLER_EVM_ADDRESS_REGEX.test(value);
|
|
674
|
+
}
|
|
675
|
+
if (chain.startsWith("solana:")) {
|
|
676
|
+
if (!SWAP_HANDLER_BASE58_REGEX.test(value)) return false;
|
|
677
|
+
if (value.length < 32 || value.length > 44) return false;
|
|
678
|
+
try {
|
|
679
|
+
return base58Decode(value).length === 32;
|
|
680
|
+
} catch {
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (chain.startsWith("mina:")) {
|
|
685
|
+
return SWAP_HANDLER_BASE58_REGEX.test(value) && value.length >= 32;
|
|
686
|
+
}
|
|
687
|
+
return value.length > 0;
|
|
688
|
+
}
|
|
689
|
+
function findChainRecipient(rumor, chain) {
|
|
690
|
+
const raw = findTagValue(rumor, "chain-recipient");
|
|
691
|
+
if (!raw) return null;
|
|
692
|
+
if (!validateChainRecipient(raw, chain)) return null;
|
|
693
|
+
return raw;
|
|
694
|
+
}
|
|
695
|
+
var noop = () => void 0;
|
|
696
|
+
var NOOP_LOGGER = {
|
|
697
|
+
debug: noop,
|
|
698
|
+
info: noop,
|
|
699
|
+
warn: noop,
|
|
700
|
+
error: noop
|
|
701
|
+
};
|
|
702
|
+
function createSwapHandler(config) {
|
|
703
|
+
if (!(config.recipientSecretKey instanceof Uint8Array) || config.recipientSecretKey.length !== 32) {
|
|
704
|
+
throw new SwapHandlerError(
|
|
705
|
+
"recipientSecretKey must be a 32-byte Uint8Array"
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
if (!Array.isArray(config.swapPairs)) {
|
|
709
|
+
throw new SwapHandlerError("swapPairs must be an array");
|
|
710
|
+
}
|
|
711
|
+
if (!config.claimIssuer || typeof config.claimIssuer.issueClaim !== "function") {
|
|
712
|
+
throw new SwapHandlerError(
|
|
713
|
+
"claimIssuer must implement issueClaim(params): Promise<IssueClaimResult>"
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
const logger = config.logger ?? NOOP_LOGGER;
|
|
717
|
+
const seenPacketIds = config.seenPacketIds ?? new BoundedSeenPacketIds();
|
|
718
|
+
return async (ctx) => {
|
|
719
|
+
if (ctx.kind !== 1059) {
|
|
720
|
+
return ctx.reject("F02", "Unreachable");
|
|
721
|
+
}
|
|
722
|
+
if (typeof ctx.amount !== "bigint" || ctx.amount <= 0n) {
|
|
723
|
+
logger.warn({
|
|
724
|
+
event: "swap_handler.invalid_amount",
|
|
725
|
+
destination: ctx.destination
|
|
726
|
+
});
|
|
727
|
+
return ctx.reject("F01", "Invalid amount");
|
|
728
|
+
}
|
|
729
|
+
if (typeof ctx.toon !== "string" || ctx.toon.length === 0) {
|
|
730
|
+
logger.warn({
|
|
731
|
+
event: "swap_handler.invalid_toon",
|
|
732
|
+
destination: ctx.destination
|
|
733
|
+
});
|
|
734
|
+
return ctx.reject("F01", "Invalid gift wrap");
|
|
735
|
+
}
|
|
736
|
+
let rumor;
|
|
737
|
+
let senderPubkey;
|
|
738
|
+
try {
|
|
739
|
+
const toonData = new Uint8Array(Buffer.from(ctx.toon, "base64"));
|
|
740
|
+
({ rumor, senderPubkey } = unwrapSwapPacketFromToon({
|
|
741
|
+
toonData,
|
|
742
|
+
recipientSecretKey: config.recipientSecretKey
|
|
743
|
+
}));
|
|
744
|
+
} catch (err) {
|
|
745
|
+
if (err instanceof GiftWrapError) {
|
|
746
|
+
logger.warn({
|
|
747
|
+
event: "swap_handler.unwrap_failed",
|
|
748
|
+
destination: ctx.destination,
|
|
749
|
+
error: err.message
|
|
750
|
+
});
|
|
751
|
+
return ctx.reject("F01", "Invalid gift wrap");
|
|
752
|
+
}
|
|
753
|
+
logger.error({
|
|
754
|
+
event: "swap_handler.unwrap_unexpected_error",
|
|
755
|
+
destination: ctx.destination,
|
|
756
|
+
error: err instanceof Error ? err.message : String(err)
|
|
757
|
+
});
|
|
758
|
+
return ctx.reject("F01", "Invalid gift wrap");
|
|
759
|
+
}
|
|
760
|
+
const pair = findSwapPair(rumor, config.swapPairs);
|
|
761
|
+
if (!pair) {
|
|
762
|
+
logger.debug({
|
|
763
|
+
event: "swap_handler.unsupported_pair",
|
|
764
|
+
destination: ctx.destination
|
|
765
|
+
});
|
|
766
|
+
return ctx.reject("F06", "Unsupported swap pair");
|
|
767
|
+
}
|
|
768
|
+
const chainRecipient = findChainRecipient(rumor, pair.to.chain);
|
|
769
|
+
if (!chainRecipient) {
|
|
770
|
+
logger.debug({
|
|
771
|
+
event: "swap_handler.malformed_rumor",
|
|
772
|
+
destination: ctx.destination,
|
|
773
|
+
reason: "missing_or_malformed_chain_recipient",
|
|
774
|
+
chain: pair.to.chain
|
|
775
|
+
});
|
|
776
|
+
return ctx.reject("T00", "Internal error");
|
|
777
|
+
}
|
|
778
|
+
const packetId = computePacketId(senderPubkey, ctx.amount, rumor);
|
|
779
|
+
if (seenPacketIds.has(packetId)) {
|
|
780
|
+
logger.debug({
|
|
781
|
+
event: "swap_handler.duplicate_packet",
|
|
782
|
+
packetId
|
|
783
|
+
});
|
|
784
|
+
return ctx.reject("F04", "Duplicate packet");
|
|
785
|
+
}
|
|
786
|
+
seenPacketIds.add(packetId);
|
|
787
|
+
const releaseReservation = () => {
|
|
788
|
+
seenPacketIds.delete(packetId);
|
|
789
|
+
};
|
|
790
|
+
let rate;
|
|
791
|
+
try {
|
|
792
|
+
rate = config.rateProvider ? await config.rateProvider(pair) : pair.rate;
|
|
793
|
+
} catch (err) {
|
|
794
|
+
logger.error({
|
|
795
|
+
event: "swap_handler.rate_provider_failed",
|
|
796
|
+
error: err instanceof Error ? err.message : String(err)
|
|
797
|
+
});
|
|
798
|
+
releaseReservation();
|
|
799
|
+
return ctx.reject("T00", "Rate provider error");
|
|
800
|
+
}
|
|
801
|
+
let targetAmount;
|
|
802
|
+
try {
|
|
803
|
+
targetAmount = applyRate({
|
|
804
|
+
sourceAmount: ctx.amount,
|
|
805
|
+
fromScale: pair.from.assetScale,
|
|
806
|
+
toScale: pair.to.assetScale,
|
|
807
|
+
rate
|
|
808
|
+
});
|
|
809
|
+
logger.debug({
|
|
810
|
+
event: "swap_handler.rate_applied",
|
|
811
|
+
sourceAmount: ctx.amount.toString(),
|
|
812
|
+
targetAmount: targetAmount.toString(),
|
|
813
|
+
rate
|
|
814
|
+
});
|
|
815
|
+
} catch (err) {
|
|
816
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
817
|
+
logger.warn({
|
|
818
|
+
event: "swap_handler.rate_conversion_failed",
|
|
819
|
+
error: msg
|
|
820
|
+
});
|
|
821
|
+
releaseReservation();
|
|
822
|
+
return ctx.reject("T00", "Rate conversion error");
|
|
823
|
+
}
|
|
824
|
+
let claim;
|
|
825
|
+
let claimId;
|
|
826
|
+
let settlementChannelId;
|
|
827
|
+
let settlementNonce;
|
|
828
|
+
let settlementCumulative;
|
|
829
|
+
let settlementRecipient;
|
|
830
|
+
let settlementMillSigner;
|
|
831
|
+
try {
|
|
832
|
+
const result = await config.claimIssuer.issueClaim({
|
|
833
|
+
sourceAmount: ctx.amount,
|
|
834
|
+
targetAmount,
|
|
835
|
+
pair,
|
|
836
|
+
senderPubkey,
|
|
837
|
+
chainRecipient,
|
|
838
|
+
rumor
|
|
839
|
+
});
|
|
840
|
+
claim = result.claim;
|
|
841
|
+
claimId = result.claimId;
|
|
842
|
+
settlementChannelId = result.channelId;
|
|
843
|
+
settlementNonce = result.nonce;
|
|
844
|
+
settlementCumulative = result.cumulativeAmount;
|
|
845
|
+
settlementRecipient = result.recipient;
|
|
846
|
+
settlementMillSigner = result.millSignerAddress;
|
|
847
|
+
} catch (err) {
|
|
848
|
+
const code = err?.code;
|
|
849
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
850
|
+
if (code === "INSUFFICIENT_INVENTORY" || /insufficient/i.test(message)) {
|
|
851
|
+
logger.warn({
|
|
852
|
+
event: "swap_handler.insufficient_inventory",
|
|
853
|
+
error: message
|
|
854
|
+
});
|
|
855
|
+
releaseReservation();
|
|
856
|
+
return ctx.reject("T04", "Insufficient liquidity");
|
|
857
|
+
}
|
|
858
|
+
logger.error({
|
|
859
|
+
event: "swap_handler.issuer_failed",
|
|
860
|
+
error: message
|
|
861
|
+
});
|
|
862
|
+
releaseReservation();
|
|
863
|
+
return ctx.reject("T00", "Internal error");
|
|
864
|
+
}
|
|
865
|
+
let ciphertext;
|
|
866
|
+
let ephemeralPubkey;
|
|
867
|
+
try {
|
|
868
|
+
const enc = encryptFulfillClaim({ claimData: claim, senderPubkey });
|
|
869
|
+
ciphertext = enc.ciphertext;
|
|
870
|
+
ephemeralPubkey = enc.ephemeralPubkey;
|
|
871
|
+
} catch (err) {
|
|
872
|
+
logger.error({
|
|
873
|
+
event: "swap_handler.encrypt_failed",
|
|
874
|
+
error: err instanceof Error ? err.message : String(err)
|
|
875
|
+
});
|
|
876
|
+
releaseReservation();
|
|
877
|
+
return ctx.reject("T00", "Internal error");
|
|
878
|
+
}
|
|
879
|
+
const claimBase64 = Buffer.from(ciphertext).toString("base64");
|
|
880
|
+
logger.info({
|
|
881
|
+
event: "swap_handler.claim_issued",
|
|
882
|
+
claimId,
|
|
883
|
+
ephemeralPubkey
|
|
884
|
+
});
|
|
885
|
+
const metadata = {
|
|
886
|
+
claim: claimBase64,
|
|
887
|
+
ephemeralPubkey,
|
|
888
|
+
targetAmount: targetAmount.toString()
|
|
889
|
+
};
|
|
890
|
+
if (claimId !== void 0) metadata["claimId"] = claimId;
|
|
891
|
+
if (settlementChannelId !== void 0 && settlementNonce !== void 0 && settlementCumulative !== void 0 && settlementRecipient !== void 0 && settlementMillSigner !== void 0) {
|
|
892
|
+
metadata["channelId"] = settlementChannelId;
|
|
893
|
+
metadata["nonce"] = settlementNonce.toString();
|
|
894
|
+
metadata["cumulativeAmount"] = settlementCumulative.toString();
|
|
895
|
+
metadata["recipient"] = settlementRecipient;
|
|
896
|
+
metadata["millSignerAddress"] = settlementMillSigner;
|
|
897
|
+
}
|
|
898
|
+
return ctx.accept(metadata);
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function computePacketId(senderPubkey, sourceAmount, rumor) {
|
|
902
|
+
const rumorId = rumor.id ?? "";
|
|
903
|
+
const hash = createHash("sha256");
|
|
904
|
+
const parts = [senderPubkey, sourceAmount.toString(), rumorId];
|
|
905
|
+
for (const p of parts) {
|
|
906
|
+
const buf = Buffer.from(p, "utf8");
|
|
907
|
+
const lenBuf = Buffer.alloc(4);
|
|
908
|
+
lenBuf.writeUInt32BE(buf.length, 0);
|
|
909
|
+
hash.update(lenBuf);
|
|
910
|
+
hash.update(buf);
|
|
911
|
+
}
|
|
912
|
+
return hash.digest("hex");
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// src/stream-swap.ts
|
|
916
|
+
import { getPublicKey as getPublicKey3 } from "nostr-tools/pure";
|
|
917
|
+
var RATE_REGEX2 = /^(0|[1-9]\d*)(\.\d+)?$/;
|
|
918
|
+
var HEX64_REGEX = /^[0-9a-f]{64}$/;
|
|
919
|
+
var BASE64_REGEX = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
920
|
+
function isBase64(s) {
|
|
921
|
+
if (s.length === 0 || s.length % 4 !== 0) return false;
|
|
922
|
+
return BASE64_REGEX.test(s);
|
|
923
|
+
}
|
|
924
|
+
function chunkAmount(total, count) {
|
|
925
|
+
if (!Number.isInteger(count) || count <= 0 || count > Number.MAX_SAFE_INTEGER) {
|
|
926
|
+
throw new StreamSwapError(
|
|
927
|
+
"INVALID_CHUNKING",
|
|
928
|
+
`packetCount must be a positive integer, got ${count}`
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
if (total < BigInt(count)) {
|
|
932
|
+
throw new StreamSwapError(
|
|
933
|
+
"INVALID_CHUNKING",
|
|
934
|
+
`totalAmount (${total}) must be >= packetCount (${count}) so per-packet amount >= 1`
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
const base = total / BigInt(count);
|
|
938
|
+
const remainder = total - base * BigInt(count);
|
|
939
|
+
const out = new Array(count);
|
|
940
|
+
for (let i = 0; i < count; i++) out[i] = base;
|
|
941
|
+
const last = out[count - 1] ?? 0n;
|
|
942
|
+
out[count - 1] = last + remainder;
|
|
943
|
+
return out;
|
|
944
|
+
}
|
|
945
|
+
function buildSwapRumor(input) {
|
|
946
|
+
const {
|
|
947
|
+
senderPubkey,
|
|
948
|
+
pair,
|
|
949
|
+
sourceAmount,
|
|
950
|
+
packetIndex,
|
|
951
|
+
totalPackets,
|
|
952
|
+
nonce,
|
|
953
|
+
createdAt,
|
|
954
|
+
chainRecipient
|
|
955
|
+
} = input;
|
|
956
|
+
return {
|
|
957
|
+
kind: 20032,
|
|
958
|
+
pubkey: senderPubkey,
|
|
959
|
+
content: "",
|
|
960
|
+
created_at: createdAt,
|
|
961
|
+
tags: [
|
|
962
|
+
["swap-from", `${pair.from.assetCode}:${pair.from.chain}`],
|
|
963
|
+
["swap-to", `${pair.to.assetCode}:${pair.to.chain}`],
|
|
964
|
+
["amount", sourceAmount.toString()],
|
|
965
|
+
["seq", String(packetIndex), String(totalPackets)],
|
|
966
|
+
["nonce", Buffer.from(nonce).toString("hex")],
|
|
967
|
+
["chain-recipient", chainRecipient]
|
|
968
|
+
]
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
var EVM_CHANNEL_ID_REGEX = /^0x[0-9a-f]{64}$/;
|
|
972
|
+
var EVM_ADDRESS_REGEX = /^0x[0-9a-f]{40}$/;
|
|
973
|
+
var DECIMAL_UINT_REGEX = /^(0|[1-9]\d*)$/;
|
|
974
|
+
var BASE58_REGEX = /^[1-9A-HJ-NP-Za-km-z]+$/;
|
|
975
|
+
function validateChainAddress(value, chain, kind) {
|
|
976
|
+
if (chain.startsWith("evm:")) {
|
|
977
|
+
const normalized = value.toLowerCase();
|
|
978
|
+
if (kind === "channelId") return EVM_CHANNEL_ID_REGEX.test(normalized);
|
|
979
|
+
return EVM_ADDRESS_REGEX.test(normalized);
|
|
980
|
+
}
|
|
981
|
+
if (chain.startsWith("solana:")) {
|
|
982
|
+
if (!BASE58_REGEX.test(value)) return false;
|
|
983
|
+
if (value.length < 32 || value.length > 44) return false;
|
|
984
|
+
try {
|
|
985
|
+
return base58Decode(value).length === 32;
|
|
986
|
+
} catch {
|
|
987
|
+
return false;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (chain.startsWith("mina:")) {
|
|
991
|
+
return BASE58_REGEX.test(value) && value.length >= 32;
|
|
992
|
+
}
|
|
993
|
+
return value.length > 0;
|
|
994
|
+
}
|
|
995
|
+
function decodeFulfillMetadata(data, chain) {
|
|
996
|
+
if (data === void 0 || data === null || data === "") {
|
|
997
|
+
throw new StreamSwapError("FULFILL_DECODE_FAILED", "FULFILL data missing");
|
|
998
|
+
}
|
|
999
|
+
if (!isBase64(data)) {
|
|
1000
|
+
throw new StreamSwapError(
|
|
1001
|
+
"FULFILL_DECODE_FAILED",
|
|
1002
|
+
"FULFILL data is not valid base64"
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
let jsonBytes;
|
|
1006
|
+
try {
|
|
1007
|
+
jsonBytes = Buffer.from(data, "base64");
|
|
1008
|
+
} catch (err) {
|
|
1009
|
+
throw new StreamSwapError(
|
|
1010
|
+
"FULFILL_DECODE_FAILED",
|
|
1011
|
+
`FULFILL data base64 decode failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1012
|
+
{ cause: err }
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
let parsed;
|
|
1016
|
+
try {
|
|
1017
|
+
parsed = JSON.parse(jsonBytes.toString("utf8"));
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
throw new StreamSwapError(
|
|
1020
|
+
"FULFILL_DECODE_FAILED",
|
|
1021
|
+
`FULFILL data JSON parse failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1022
|
+
{ cause: err }
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1026
|
+
throw new StreamSwapError(
|
|
1027
|
+
"FULFILL_DECODE_FAILED",
|
|
1028
|
+
"FULFILL metadata is not an object"
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
const obj = parsed;
|
|
1032
|
+
const claim = obj["claim"];
|
|
1033
|
+
const ephemeralPubkey = obj["ephemeralPubkey"];
|
|
1034
|
+
if (typeof claim !== "string" || !isBase64(claim)) {
|
|
1035
|
+
throw new StreamSwapError(
|
|
1036
|
+
"FULFILL_DECODE_FAILED",
|
|
1037
|
+
"FULFILL metadata.claim is missing or not base64 string"
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
if (typeof ephemeralPubkey !== "string" || !HEX64_REGEX.test(ephemeralPubkey)) {
|
|
1041
|
+
throw new StreamSwapError(
|
|
1042
|
+
"FULFILL_DECODE_FAILED",
|
|
1043
|
+
"FULFILL metadata.ephemeralPubkey is missing or not 64-char hex"
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
const result = {
|
|
1047
|
+
claim,
|
|
1048
|
+
ephemeralPubkey
|
|
1049
|
+
};
|
|
1050
|
+
if (typeof obj["claimId"] === "string") {
|
|
1051
|
+
result.claimId = obj["claimId"];
|
|
1052
|
+
}
|
|
1053
|
+
if (obj["targetAmount"] !== void 0) {
|
|
1054
|
+
const ta = obj["targetAmount"];
|
|
1055
|
+
if (typeof ta !== "string" || !/^(0|[1-9]\d*)$/.test(ta)) {
|
|
1056
|
+
throw new StreamSwapError(
|
|
1057
|
+
"FULFILL_DECODE_FAILED",
|
|
1058
|
+
"FULFILL metadata.targetAmount must be a non-negative integer decimal string"
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
result.targetAmount = ta;
|
|
1062
|
+
}
|
|
1063
|
+
const channelId = obj["channelId"];
|
|
1064
|
+
if (typeof channelId === "string" && (!chain || validateChainAddress(channelId, chain, "channelId"))) {
|
|
1065
|
+
result.channelId = channelId;
|
|
1066
|
+
}
|
|
1067
|
+
const nonce = obj["nonce"];
|
|
1068
|
+
if (typeof nonce === "string" && DECIMAL_UINT_REGEX.test(nonce)) {
|
|
1069
|
+
result.nonce = nonce;
|
|
1070
|
+
}
|
|
1071
|
+
const cumulativeAmount = obj["cumulativeAmount"];
|
|
1072
|
+
if (typeof cumulativeAmount === "string" && DECIMAL_UINT_REGEX.test(cumulativeAmount)) {
|
|
1073
|
+
result.cumulativeAmount = cumulativeAmount;
|
|
1074
|
+
}
|
|
1075
|
+
const recipient = obj["recipient"];
|
|
1076
|
+
if (typeof recipient === "string" && recipient.length > 0) {
|
|
1077
|
+
result.recipient = recipient;
|
|
1078
|
+
}
|
|
1079
|
+
const millSignerAddress = obj["millSignerAddress"];
|
|
1080
|
+
if (typeof millSignerAddress === "string" && (!chain || validateChainAddress(millSignerAddress, chain, "address"))) {
|
|
1081
|
+
result.millSignerAddress = millSignerAddress;
|
|
1082
|
+
}
|
|
1083
|
+
return result;
|
|
1084
|
+
}
|
|
1085
|
+
var Deferred = class {
|
|
1086
|
+
promise;
|
|
1087
|
+
resolve;
|
|
1088
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- mirror Promise constructor
|
|
1089
|
+
reject;
|
|
1090
|
+
constructor() {
|
|
1091
|
+
this.promise = new Promise((res, rej) => {
|
|
1092
|
+
this.resolve = res;
|
|
1093
|
+
this.reject = rej;
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
var noop2 = () => void 0;
|
|
1098
|
+
var NOOP_LOGGER2 = {
|
|
1099
|
+
debug: noop2,
|
|
1100
|
+
info: noop2,
|
|
1101
|
+
warn: noop2,
|
|
1102
|
+
error: noop2
|
|
1103
|
+
};
|
|
1104
|
+
function validateParams(params) {
|
|
1105
|
+
if (typeof params.totalAmount !== "bigint" || params.totalAmount <= 0n) {
|
|
1106
|
+
throw new StreamSwapError(
|
|
1107
|
+
"INVALID_AMOUNT",
|
|
1108
|
+
`totalAmount must be a positive bigint, got ${String(params.totalAmount)}`
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
const hasCount = params.packetCount !== void 0;
|
|
1112
|
+
const hasAmounts = params.packetAmounts !== void 0;
|
|
1113
|
+
if (hasCount === hasAmounts) {
|
|
1114
|
+
throw new StreamSwapError(
|
|
1115
|
+
"INVALID_CHUNKING",
|
|
1116
|
+
"Exactly one of packetCount or packetAmounts must be provided"
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
if (hasCount) {
|
|
1120
|
+
const c = params.packetCount;
|
|
1121
|
+
if (!Number.isInteger(c) || c <= 0) {
|
|
1122
|
+
throw new StreamSwapError(
|
|
1123
|
+
"INVALID_CHUNKING",
|
|
1124
|
+
`packetCount must be a positive integer, got ${c}`
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
if (BigInt(c) > params.totalAmount) {
|
|
1128
|
+
throw new StreamSwapError(
|
|
1129
|
+
"INVALID_CHUNKING",
|
|
1130
|
+
`packetCount (${c}) exceeds totalAmount (${params.totalAmount}); per-packet amount would be < 1 micro-unit`
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
} else {
|
|
1134
|
+
const arr = params.packetAmounts;
|
|
1135
|
+
if (!Array.isArray(arr) || arr.length === 0) {
|
|
1136
|
+
throw new StreamSwapError(
|
|
1137
|
+
"INVALID_CHUNKING",
|
|
1138
|
+
"packetAmounts must be a non-empty array"
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
let sum = 0n;
|
|
1142
|
+
for (const a of arr) {
|
|
1143
|
+
if (typeof a !== "bigint" || a <= 0n) {
|
|
1144
|
+
throw new StreamSwapError(
|
|
1145
|
+
"INVALID_CHUNKING",
|
|
1146
|
+
`packetAmounts entries must be positive bigint, got ${String(a)}`
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
sum += a;
|
|
1150
|
+
}
|
|
1151
|
+
if (sum !== params.totalAmount) {
|
|
1152
|
+
throw new StreamSwapError(
|
|
1153
|
+
"INVALID_CHUNKING",
|
|
1154
|
+
`sum(packetAmounts) (${sum}) !== totalAmount (${params.totalAmount})`
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
if (!(params.senderSecretKey instanceof Uint8Array) || params.senderSecretKey.length !== 32) {
|
|
1159
|
+
throw new StreamSwapError(
|
|
1160
|
+
"INVALID_STATE",
|
|
1161
|
+
"senderSecretKey must be a 32-byte Uint8Array"
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
if (typeof params.millPubkey !== "string" || !HEX64_REGEX.test(params.millPubkey)) {
|
|
1165
|
+
throw new StreamSwapError(
|
|
1166
|
+
"INVALID_STATE",
|
|
1167
|
+
"millPubkey must be a 64-char lowercase hex string"
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
if (!params.pair || typeof params.pair !== "object") {
|
|
1171
|
+
throw new StreamSwapError("INVALID_PAIR", "pair is required");
|
|
1172
|
+
}
|
|
1173
|
+
if (!params.pair.from || typeof params.pair.from !== "object" || typeof params.pair.from.assetCode !== "string" || typeof params.pair.from.assetScale !== "number" || typeof params.pair.from.chain !== "string") {
|
|
1174
|
+
throw new StreamSwapError(
|
|
1175
|
+
"INVALID_PAIR",
|
|
1176
|
+
"pair.from must have { assetCode: string, assetScale: number, chain: string }"
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
if (!params.pair.to || typeof params.pair.to !== "object" || typeof params.pair.to.assetCode !== "string" || typeof params.pair.to.assetScale !== "number" || typeof params.pair.to.chain !== "string") {
|
|
1180
|
+
throw new StreamSwapError(
|
|
1181
|
+
"INVALID_PAIR",
|
|
1182
|
+
"pair.to must have { assetCode: string, assetScale: number, chain: string }"
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
if (typeof params.pair.rate !== "string" || !RATE_REGEX2.test(params.pair.rate)) {
|
|
1186
|
+
throw new StreamSwapError(
|
|
1187
|
+
"INVALID_PAIR",
|
|
1188
|
+
`pair.rate must match ${RATE_REGEX2}, got ${params.pair.rate}`
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
try {
|
|
1192
|
+
applyRate({
|
|
1193
|
+
sourceAmount: 1n,
|
|
1194
|
+
fromScale: params.pair.from.assetScale,
|
|
1195
|
+
toScale: params.pair.to.assetScale,
|
|
1196
|
+
rate: params.pair.rate
|
|
1197
|
+
});
|
|
1198
|
+
} catch (err) {
|
|
1199
|
+
throw new StreamSwapError(
|
|
1200
|
+
"INVALID_PAIR",
|
|
1201
|
+
`pair failed applyRate sanity check: ${err instanceof Error ? err.message : String(err)}`,
|
|
1202
|
+
{ cause: err }
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
if (params.rateDeviationThreshold !== void 0 && (typeof params.rateDeviationThreshold !== "number" || !Number.isFinite(params.rateDeviationThreshold) || params.rateDeviationThreshold < 0)) {
|
|
1206
|
+
throw new StreamSwapError(
|
|
1207
|
+
"INVALID_STATE",
|
|
1208
|
+
`rateDeviationThreshold must be a non-negative finite number, got ${params.rateDeviationThreshold}`
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
if (typeof params.chainRecipient !== "string" || params.chainRecipient.length === 0) {
|
|
1212
|
+
throw new StreamSwapError(
|
|
1213
|
+
"INVALID_STATE",
|
|
1214
|
+
"chainRecipient must be a non-empty string (sender payout address for pair.to.chain)"
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
if (!validateChainAddress(
|
|
1218
|
+
params.chainRecipient,
|
|
1219
|
+
params.pair.to.chain,
|
|
1220
|
+
"address"
|
|
1221
|
+
)) {
|
|
1222
|
+
throw new StreamSwapError(
|
|
1223
|
+
"INVALID_CHAIN_RECIPIENT",
|
|
1224
|
+
`chainRecipient ${params.chainRecipient} is malformed for chain ${params.pair.to.chain}`
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
async function streamSwap(params) {
|
|
1229
|
+
return streamSwapControlled(params).result;
|
|
1230
|
+
}
|
|
1231
|
+
function streamSwapControlled(params) {
|
|
1232
|
+
validateParams(params);
|
|
1233
|
+
const logger = params.logger ?? NOOP_LOGGER2;
|
|
1234
|
+
const schedule = params.packetAmounts ? [...params.packetAmounts] : chunkAmount(params.totalAmount, params.packetCount);
|
|
1235
|
+
const frozenPair = Object.freeze({
|
|
1236
|
+
from: Object.freeze({ ...params.pair.from }),
|
|
1237
|
+
to: Object.freeze({ ...params.pair.to }),
|
|
1238
|
+
rate: params.pair.rate
|
|
1239
|
+
});
|
|
1240
|
+
const senderPubkey = getPublicKey3(params.senderSecretKey);
|
|
1241
|
+
let streamState = "running";
|
|
1242
|
+
let resumeDeferred = null;
|
|
1243
|
+
const controller = {
|
|
1244
|
+
pause() {
|
|
1245
|
+
if (streamState === "running") {
|
|
1246
|
+
streamState = "paused";
|
|
1247
|
+
}
|
|
1248
|
+
},
|
|
1249
|
+
resume() {
|
|
1250
|
+
if (streamState === "paused") {
|
|
1251
|
+
streamState = "running";
|
|
1252
|
+
if (resumeDeferred) {
|
|
1253
|
+
resumeDeferred.resolve("resume");
|
|
1254
|
+
resumeDeferred = null;
|
|
1255
|
+
}
|
|
1256
|
+
} else if (streamState === "running") {
|
|
1257
|
+
} else {
|
|
1258
|
+
throw new StreamSwapError(
|
|
1259
|
+
"INVALID_STATE",
|
|
1260
|
+
`Cannot resume from state "${streamState}"`
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
},
|
|
1264
|
+
stop() {
|
|
1265
|
+
if (streamState === "completed" || streamState === "failed") return;
|
|
1266
|
+
const prev = streamState;
|
|
1267
|
+
streamState = "stopped";
|
|
1268
|
+
if (prev === "paused" && resumeDeferred) {
|
|
1269
|
+
resumeDeferred.resolve("stop");
|
|
1270
|
+
resumeDeferred = null;
|
|
1271
|
+
}
|
|
1272
|
+
},
|
|
1273
|
+
get state() {
|
|
1274
|
+
return streamState;
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
const result = runLoop(
|
|
1278
|
+
params,
|
|
1279
|
+
frozenPair,
|
|
1280
|
+
schedule,
|
|
1281
|
+
senderPubkey,
|
|
1282
|
+
logger,
|
|
1283
|
+
() => streamState,
|
|
1284
|
+
(v) => {
|
|
1285
|
+
streamState = v;
|
|
1286
|
+
},
|
|
1287
|
+
() => {
|
|
1288
|
+
if (streamState !== "paused") return Promise.resolve("resume");
|
|
1289
|
+
if (!resumeDeferred) resumeDeferred = new Deferred();
|
|
1290
|
+
return resumeDeferred.promise;
|
|
1291
|
+
}
|
|
1292
|
+
);
|
|
1293
|
+
return { result, controller };
|
|
1294
|
+
}
|
|
1295
|
+
async function runLoop(params, pair, schedule, senderPubkey, logger, getState, setState, waitForResumeOrStop) {
|
|
1296
|
+
const claims = [];
|
|
1297
|
+
const rejections = [];
|
|
1298
|
+
const errors = [];
|
|
1299
|
+
let cumulativeSource = 0n;
|
|
1300
|
+
let cumulativeTarget = 0n;
|
|
1301
|
+
let packetsSent = 0;
|
|
1302
|
+
let abortReason = "complete";
|
|
1303
|
+
const totalPackets = schedule.length;
|
|
1304
|
+
const isAborted = () => params.signal?.aborted === true;
|
|
1305
|
+
packetLoop: for (let packetIndex = 0; packetIndex < totalPackets; packetIndex++) {
|
|
1306
|
+
if (isAborted()) {
|
|
1307
|
+
abortReason = "aborted";
|
|
1308
|
+
break;
|
|
1309
|
+
}
|
|
1310
|
+
if (getState() === "stopped") {
|
|
1311
|
+
abortReason = "stopped";
|
|
1312
|
+
break;
|
|
1313
|
+
}
|
|
1314
|
+
if (getState() === "paused") {
|
|
1315
|
+
const resumedBy = await waitForResumeOrStop();
|
|
1316
|
+
if (resumedBy === "stop" || getState() === "stopped") {
|
|
1317
|
+
abortReason = "stopped";
|
|
1318
|
+
break;
|
|
1319
|
+
}
|
|
1320
|
+
if (isAborted()) {
|
|
1321
|
+
abortReason = "aborted";
|
|
1322
|
+
break;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
const sourceAmount = schedule[packetIndex];
|
|
1326
|
+
if (sourceAmount === void 0) {
|
|
1327
|
+
throw new StreamSwapError(
|
|
1328
|
+
"INVALID_STATE",
|
|
1329
|
+
`schedule[${packetIndex}] is undefined; schedule was mutated mid-stream`
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
const nonce = new Uint8Array(16);
|
|
1333
|
+
getRandomValues(nonce);
|
|
1334
|
+
const rumor = buildSwapRumor({
|
|
1335
|
+
senderPubkey,
|
|
1336
|
+
pair,
|
|
1337
|
+
sourceAmount,
|
|
1338
|
+
packetIndex: packetIndex + 1,
|
|
1339
|
+
totalPackets,
|
|
1340
|
+
nonce,
|
|
1341
|
+
createdAt: Math.floor(Date.now() / 1e3),
|
|
1342
|
+
chainRecipient: params.chainRecipient
|
|
1343
|
+
});
|
|
1344
|
+
let toonData;
|
|
1345
|
+
try {
|
|
1346
|
+
const wrapped = wrapSwapPacketToToon({
|
|
1347
|
+
rumor,
|
|
1348
|
+
senderSecretKey: params.senderSecretKey,
|
|
1349
|
+
recipientPubkey: params.millPubkey,
|
|
1350
|
+
destination: params.millIlpAddress,
|
|
1351
|
+
amount: sourceAmount
|
|
1352
|
+
});
|
|
1353
|
+
toonData = new Uint8Array(Buffer.from(wrapped.ilpPrepare.data, "base64"));
|
|
1354
|
+
} catch (err) {
|
|
1355
|
+
logger.error({
|
|
1356
|
+
event: "stream_swap.wrap_failed",
|
|
1357
|
+
packetIndex,
|
|
1358
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1359
|
+
});
|
|
1360
|
+
errors.push({
|
|
1361
|
+
packetIndex,
|
|
1362
|
+
cause: err instanceof Error ? err : new Error(String(err))
|
|
1363
|
+
});
|
|
1364
|
+
continue;
|
|
1365
|
+
}
|
|
1366
|
+
let sendResult;
|
|
1367
|
+
try {
|
|
1368
|
+
sendResult = await params.client.sendSwapPacket({
|
|
1369
|
+
destination: params.millIlpAddress,
|
|
1370
|
+
amount: sourceAmount,
|
|
1371
|
+
toonData,
|
|
1372
|
+
timeout: params.packetTimeoutMs ?? 3e4,
|
|
1373
|
+
claim: params.claim
|
|
1374
|
+
});
|
|
1375
|
+
packetsSent += 1;
|
|
1376
|
+
} catch (err) {
|
|
1377
|
+
logger.error({
|
|
1378
|
+
event: "stream_swap.send_failed",
|
|
1379
|
+
packetIndex,
|
|
1380
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1381
|
+
});
|
|
1382
|
+
errors.push({
|
|
1383
|
+
packetIndex,
|
|
1384
|
+
cause: err instanceof Error ? err : new Error(String(err))
|
|
1385
|
+
});
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
if (!sendResult.accepted) {
|
|
1389
|
+
const code = sendResult.code ?? "F00";
|
|
1390
|
+
const message = sendResult.message ?? "rejected";
|
|
1391
|
+
logger.warn({
|
|
1392
|
+
event: "stream_swap.packet_rejected",
|
|
1393
|
+
packetIndex,
|
|
1394
|
+
code,
|
|
1395
|
+
message
|
|
1396
|
+
});
|
|
1397
|
+
rejections.push({ packetIndex, sourceAmount, code, message });
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
let metadata;
|
|
1401
|
+
try {
|
|
1402
|
+
metadata = decodeFulfillMetadata(sendResult.data, pair.to.chain);
|
|
1403
|
+
} catch (err) {
|
|
1404
|
+
logger.error({
|
|
1405
|
+
event: "stream_swap.fulfill_decode_failed",
|
|
1406
|
+
packetIndex,
|
|
1407
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1408
|
+
});
|
|
1409
|
+
errors.push({
|
|
1410
|
+
packetIndex,
|
|
1411
|
+
cause: err instanceof Error ? err : new Error(String(err))
|
|
1412
|
+
});
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
const isEvmTarget = pair.to.chain.startsWith("evm:");
|
|
1416
|
+
const recipientMatches = metadata.recipient === void 0 || (isEvmTarget ? metadata.recipient.toLowerCase() === params.chainRecipient.toLowerCase() : metadata.recipient === params.chainRecipient);
|
|
1417
|
+
if (!recipientMatches) {
|
|
1418
|
+
logger.warn({
|
|
1419
|
+
event: "stream_swap.recipient_mismatch",
|
|
1420
|
+
packetIndex,
|
|
1421
|
+
expected: params.chainRecipient,
|
|
1422
|
+
actual: metadata.recipient
|
|
1423
|
+
});
|
|
1424
|
+
rejections.push({
|
|
1425
|
+
packetIndex,
|
|
1426
|
+
sourceAmount,
|
|
1427
|
+
code: "MILL_RECIPIENT_MISMATCH",
|
|
1428
|
+
message: `Mill echoed recipient ${metadata.recipient} but sender expected ${params.chainRecipient}`
|
|
1429
|
+
});
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
let claimBytes;
|
|
1433
|
+
try {
|
|
1434
|
+
const ciphertext = new Uint8Array(Buffer.from(metadata.claim, "base64"));
|
|
1435
|
+
claimBytes = decryptFulfillClaim({
|
|
1436
|
+
ciphertext,
|
|
1437
|
+
ephemeralPubkey: metadata.ephemeralPubkey,
|
|
1438
|
+
recipientSecretKey: params.senderSecretKey
|
|
1439
|
+
});
|
|
1440
|
+
} catch (err) {
|
|
1441
|
+
logger.error({
|
|
1442
|
+
event: "stream_swap.decrypt_failed",
|
|
1443
|
+
packetIndex,
|
|
1444
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1445
|
+
});
|
|
1446
|
+
errors.push({
|
|
1447
|
+
packetIndex,
|
|
1448
|
+
cause: err instanceof Error ? err : new Error(String(err))
|
|
1449
|
+
});
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
if (claimBytes.length === 0) {
|
|
1453
|
+
logger.warn({
|
|
1454
|
+
event: "stream_swap.empty_claim_bytes",
|
|
1455
|
+
packetIndex
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
const expectedTargetAmount = applyRate({
|
|
1459
|
+
sourceAmount,
|
|
1460
|
+
fromScale: pair.from.assetScale,
|
|
1461
|
+
toScale: pair.to.assetScale,
|
|
1462
|
+
rate: pair.rate
|
|
1463
|
+
});
|
|
1464
|
+
const targetAmount = metadata.targetAmount !== void 0 ? BigInt(metadata.targetAmount) : expectedTargetAmount;
|
|
1465
|
+
let rateDeviation = 0;
|
|
1466
|
+
if (expectedTargetAmount > 0n) {
|
|
1467
|
+
const diff = targetAmount >= expectedTargetAmount ? targetAmount - expectedTargetAmount : expectedTargetAmount - targetAmount;
|
|
1468
|
+
const scaled = diff * 1000000n / expectedTargetAmount;
|
|
1469
|
+
rateDeviation = Number(scaled) / 1e6;
|
|
1470
|
+
}
|
|
1471
|
+
const advertisedRate = parseFloat(pair.rate);
|
|
1472
|
+
let effectiveRate;
|
|
1473
|
+
if (targetAmount === expectedTargetAmount) {
|
|
1474
|
+
effectiveRate = advertisedRate;
|
|
1475
|
+
} else {
|
|
1476
|
+
const signedDeviation = targetAmount >= expectedTargetAmount ? rateDeviation : -rateDeviation;
|
|
1477
|
+
effectiveRate = advertisedRate * (1 + signedDeviation);
|
|
1478
|
+
}
|
|
1479
|
+
if (!Number.isFinite(effectiveRate)) {
|
|
1480
|
+
effectiveRate = advertisedRate;
|
|
1481
|
+
}
|
|
1482
|
+
cumulativeSource += sourceAmount;
|
|
1483
|
+
cumulativeTarget += targetAmount;
|
|
1484
|
+
const accumulated = {
|
|
1485
|
+
packetIndex,
|
|
1486
|
+
sourceAmount,
|
|
1487
|
+
targetAmount,
|
|
1488
|
+
claimBytes,
|
|
1489
|
+
millEphemeralPubkey: metadata.ephemeralPubkey,
|
|
1490
|
+
pair,
|
|
1491
|
+
receivedAt: Date.now()
|
|
1492
|
+
};
|
|
1493
|
+
if (metadata.claimId !== void 0) accumulated.claimId = metadata.claimId;
|
|
1494
|
+
if (metadata.channelId !== void 0)
|
|
1495
|
+
accumulated.channelId = metadata.channelId;
|
|
1496
|
+
if (metadata.nonce !== void 0) accumulated.nonce = metadata.nonce;
|
|
1497
|
+
if (metadata.cumulativeAmount !== void 0)
|
|
1498
|
+
accumulated.cumulativeAmount = metadata.cumulativeAmount;
|
|
1499
|
+
if (metadata.recipient !== void 0)
|
|
1500
|
+
accumulated.recipient = metadata.recipient;
|
|
1501
|
+
if (metadata.millSignerAddress !== void 0)
|
|
1502
|
+
accumulated.millSignerAddress = metadata.millSignerAddress;
|
|
1503
|
+
claims.push(accumulated);
|
|
1504
|
+
logger.debug({
|
|
1505
|
+
event: "stream_swap.packet_accepted",
|
|
1506
|
+
packetIndex,
|
|
1507
|
+
sourceAmount: sourceAmount.toString(),
|
|
1508
|
+
targetAmount: targetAmount.toString()
|
|
1509
|
+
});
|
|
1510
|
+
if (params.onPacket) {
|
|
1511
|
+
const progress = Object.freeze({
|
|
1512
|
+
index: packetIndex,
|
|
1513
|
+
total: totalPackets,
|
|
1514
|
+
sourceAmount,
|
|
1515
|
+
targetAmount,
|
|
1516
|
+
advertisedRate: pair.rate,
|
|
1517
|
+
effectiveRate,
|
|
1518
|
+
rateDeviation,
|
|
1519
|
+
cumulativeSource,
|
|
1520
|
+
cumulativeTarget,
|
|
1521
|
+
state: getState() === "paused" ? "paused" : getState() === "stopped" ? "stopped" : "running"
|
|
1522
|
+
});
|
|
1523
|
+
try {
|
|
1524
|
+
const maybePromise = params.onPacket(progress);
|
|
1525
|
+
if (maybePromise && typeof maybePromise.then === "function") {
|
|
1526
|
+
await maybePromise;
|
|
1527
|
+
}
|
|
1528
|
+
} catch (err) {
|
|
1529
|
+
logger.warn({
|
|
1530
|
+
event: "stream_swap.callback_threw",
|
|
1531
|
+
packetIndex,
|
|
1532
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1533
|
+
});
|
|
1534
|
+
errors.push({
|
|
1535
|
+
packetIndex,
|
|
1536
|
+
cause: err instanceof Error ? err : new Error(String(err))
|
|
1537
|
+
});
|
|
1538
|
+
abortReason = "callback-throw";
|
|
1539
|
+
break packetLoop;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
if (isAborted()) {
|
|
1543
|
+
abortReason = "aborted";
|
|
1544
|
+
break;
|
|
1545
|
+
}
|
|
1546
|
+
if (getState() === "stopped") {
|
|
1547
|
+
abortReason = "stopped";
|
|
1548
|
+
break;
|
|
1549
|
+
}
|
|
1550
|
+
if (params.rateDeviationThreshold !== void 0 && rateDeviation > params.rateDeviationThreshold) {
|
|
1551
|
+
abortReason = "rate-deviation";
|
|
1552
|
+
break;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
let finalState;
|
|
1556
|
+
if (abortReason === "aborted" || abortReason === "stopped") {
|
|
1557
|
+
finalState = "stopped";
|
|
1558
|
+
} else if (claims.length === 0 && (rejections.length > 0 || errors.length > 0)) {
|
|
1559
|
+
finalState = "failed";
|
|
1560
|
+
if (abortReason === "complete" && rejections.length > 0 && errors.length === 0) {
|
|
1561
|
+
abortReason = "all-rejected";
|
|
1562
|
+
}
|
|
1563
|
+
} else {
|
|
1564
|
+
finalState = "completed";
|
|
1565
|
+
}
|
|
1566
|
+
setState(finalState);
|
|
1567
|
+
return {
|
|
1568
|
+
state: finalState,
|
|
1569
|
+
claims,
|
|
1570
|
+
rejections,
|
|
1571
|
+
errors,
|
|
1572
|
+
abortReason,
|
|
1573
|
+
cumulativeSource,
|
|
1574
|
+
cumulativeTarget,
|
|
1575
|
+
packetsSent,
|
|
1576
|
+
packetsScheduled: totalPackets
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
function getRandomValues(buf) {
|
|
1580
|
+
const g = globalThis;
|
|
1581
|
+
if (g.crypto && typeof g.crypto.getRandomValues === "function") {
|
|
1582
|
+
g.crypto.getRandomValues(buf);
|
|
1583
|
+
return buf;
|
|
1584
|
+
}
|
|
1585
|
+
const nodeCrypto = __require("crypto");
|
|
1586
|
+
if (nodeCrypto.webcrypto?.getRandomValues) {
|
|
1587
|
+
nodeCrypto.webcrypto.getRandomValues(buf);
|
|
1588
|
+
return buf;
|
|
1589
|
+
}
|
|
1590
|
+
if (nodeCrypto.randomFillSync) {
|
|
1591
|
+
nodeCrypto.randomFillSync(buf);
|
|
1592
|
+
return buf;
|
|
1593
|
+
}
|
|
1594
|
+
throw new StreamSwapError(
|
|
1595
|
+
"INVALID_STATE",
|
|
1596
|
+
"No crypto.getRandomValues available in this environment"
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
var __testing = {
|
|
1600
|
+
chunkAmount,
|
|
1601
|
+
decodeFulfillMetadata,
|
|
1602
|
+
buildSwapRumor
|
|
1603
|
+
};
|
|
1604
|
+
|
|
1605
|
+
export {
|
|
1606
|
+
IdentityError,
|
|
1607
|
+
NodeError,
|
|
1608
|
+
HandlerError,
|
|
1609
|
+
VerificationError,
|
|
1610
|
+
PricingError,
|
|
1611
|
+
GiftWrapError,
|
|
1612
|
+
SwapHandlerError,
|
|
1613
|
+
StreamSwapError,
|
|
1614
|
+
SettlementTxError,
|
|
1615
|
+
generateMnemonic,
|
|
1616
|
+
fromMnemonic,
|
|
1617
|
+
fromSecretKey,
|
|
1618
|
+
fromMnemonicFull,
|
|
1619
|
+
generateSolanaKeypair,
|
|
1620
|
+
base58Encode,
|
|
1621
|
+
base58Decode,
|
|
1622
|
+
wrapSwapPacket,
|
|
1623
|
+
unwrapSwapPacket,
|
|
1624
|
+
wrapSwapPacketToToon,
|
|
1625
|
+
unwrapSwapPacketFromToon,
|
|
1626
|
+
encryptFulfillClaim,
|
|
1627
|
+
decryptFulfillClaim,
|
|
1628
|
+
SWAP_HANDLER_REJECT_CODES,
|
|
1629
|
+
SWAP_HANDLER_REJECT_MESSAGES,
|
|
1630
|
+
applyRate,
|
|
1631
|
+
findSwapPair,
|
|
1632
|
+
createSwapHandler,
|
|
1633
|
+
streamSwap,
|
|
1634
|
+
streamSwapControlled,
|
|
1635
|
+
__testing
|
|
1636
|
+
};
|
|
1637
|
+
//# sourceMappingURL=chunk-XTHUXP63.js.map
|