@usewhisk/cctp 0.0.0 → 0.1.1

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/iris.cjs ADDED
@@ -0,0 +1,557 @@
1
+ 'use strict';
2
+
3
+ // src/iris/urls.ts
4
+ var IRIS_MAINNET_URL = "https://iris-api.circle.com";
5
+ var IRIS_SANDBOX_URL = "https://iris-api-sandbox.circle.com";
6
+ function irisBaseUrl(network) {
7
+ return network === "mainnet" ? IRIS_MAINNET_URL : IRIS_SANDBOX_URL;
8
+ }
9
+
10
+ // src/errors.ts
11
+ var CctpError = class extends Error {
12
+ code;
13
+ cause;
14
+ constructor(code, message, cause) {
15
+ super(message);
16
+ this.name = "CctpError";
17
+ this.code = code;
18
+ if (cause !== void 0) this.cause = cause;
19
+ }
20
+ };
21
+ var IrisError = class extends CctpError {
22
+ constructor(code, message, cause) {
23
+ super(code, message, cause);
24
+ this.name = "IrisError";
25
+ }
26
+ };
27
+ var IrisHttpError = class extends IrisError {
28
+ status;
29
+ bodySnippet;
30
+ url;
31
+ constructor(status, url, bodySnippet) {
32
+ super(
33
+ "iris_http",
34
+ `Iris ${status} on ${url}: ${bodySnippet.slice(0, 200)}`
35
+ );
36
+ this.name = "IrisHttpError";
37
+ this.status = status;
38
+ this.url = url;
39
+ this.bodySnippet = bodySnippet;
40
+ }
41
+ };
42
+ var IrisRateLimitedError = class extends IrisError {
43
+ retryAt;
44
+ host;
45
+ constructor(host, retryAt) {
46
+ super(
47
+ "iris_rate_limited",
48
+ `Iris rate limit reached on ${host}; requests resume at ${new Date(retryAt).toISOString()}`
49
+ );
50
+ this.name = "IrisRateLimitedError";
51
+ this.host = host;
52
+ this.retryAt = retryAt;
53
+ }
54
+ };
55
+ var IrisNetworkError = class extends IrisError {
56
+ constructor(message, cause) {
57
+ super("iris_network", message, cause);
58
+ this.name = "IrisNetworkError";
59
+ }
60
+ };
61
+ var IrisParseError = class extends IrisError {
62
+ constructor(message, cause) {
63
+ super("iris_parse", message, cause);
64
+ this.name = "IrisParseError";
65
+ }
66
+ };
67
+ var IrisAbortedError = class extends IrisError {
68
+ constructor(message) {
69
+ super("iris_aborted", message);
70
+ this.name = "IrisAbortedError";
71
+ }
72
+ };
73
+ var ProtocolError = class extends CctpError {
74
+ constructor(code, message, cause) {
75
+ super(code, message, cause);
76
+ this.name = "ProtocolError";
77
+ }
78
+ };
79
+
80
+ // src/iris/limiter.ts
81
+ function createIrisLimiter(options = {}) {
82
+ const rate = options.ratePerSecond ?? 30;
83
+ const standDownMs = options.standDownMs ?? 5 * 6e4;
84
+ const now = options.now ?? (() => Date.now());
85
+ const sleep3 = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
86
+ const buckets = /* @__PURE__ */ new Map();
87
+ const bucket = (host) => {
88
+ let b = buckets.get(host);
89
+ if (!b) {
90
+ b = { tokens: rate, updatedAt: now(), blockedUntil: 0 };
91
+ buckets.set(host, b);
92
+ }
93
+ return b;
94
+ };
95
+ return {
96
+ async acquire(host) {
97
+ const b = bucket(host);
98
+ if (now() < b.blockedUntil) {
99
+ throw new IrisRateLimitedError(host, b.blockedUntil);
100
+ }
101
+ const elapsed = (now() - b.updatedAt) / 1e3;
102
+ b.tokens = Math.min(rate, b.tokens + elapsed * rate);
103
+ b.updatedAt = now();
104
+ if (b.tokens < 1) {
105
+ const waitMs = Math.ceil((1 - b.tokens) / rate * 1e3);
106
+ await sleep3(waitMs);
107
+ b.tokens += waitMs / 1e3 * rate;
108
+ b.updatedAt = now();
109
+ }
110
+ b.tokens -= 1;
111
+ },
112
+ standDown(host) {
113
+ const b = bucket(host);
114
+ b.blockedUntil = now() + standDownMs;
115
+ return b.blockedUntil;
116
+ },
117
+ now,
118
+ sleep: sleep3
119
+ };
120
+ }
121
+ var defaultIrisLimiter = createIrisLimiter();
122
+
123
+ // src/iris/fetch.ts
124
+ var DEFAULT_TIMEOUT_MS = 5e3;
125
+ var DEFAULT_MAX_RETRIES = 3;
126
+ var DEFAULT_BASE_DELAY_MS = 300;
127
+ async function fetchIrisJson(url, options = {}, request = {
128
+ method: "GET"
129
+ }) {
130
+ const {
131
+ timeoutMs = DEFAULT_TIMEOUT_MS,
132
+ maxRetries = DEFAULT_MAX_RETRIES,
133
+ baseDelayMs = DEFAULT_BASE_DELAY_MS,
134
+ signal,
135
+ fetch: fetchImpl = fetch,
136
+ limiter = defaultIrisLimiter
137
+ } = options;
138
+ const host = new URL(url).host;
139
+ let lastError;
140
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
141
+ if (signal?.aborted) throw new IrisAbortedError("Aborted by caller");
142
+ await limiter.acquire(host);
143
+ const ctrl = new AbortController();
144
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
145
+ const onCallerAbort = () => ctrl.abort();
146
+ signal?.addEventListener("abort", onCallerAbort);
147
+ try {
148
+ const res = await fetchImpl(url, {
149
+ method: request.method,
150
+ headers: {
151
+ accept: "application/json",
152
+ ...request.method === "POST" && {
153
+ "content-type": "application/json"
154
+ }
155
+ },
156
+ ...request.method === "POST" && {
157
+ body: JSON.stringify(request.body)
158
+ },
159
+ signal: ctrl.signal
160
+ });
161
+ if (res.ok) {
162
+ let json;
163
+ try {
164
+ json = await res.json();
165
+ } catch (err) {
166
+ throw new IrisParseError(`Invalid JSON from ${url}`, err);
167
+ }
168
+ return json;
169
+ }
170
+ if (res.status === 429) {
171
+ throw new IrisRateLimitedError(host, limiter.standDown(host));
172
+ }
173
+ if (res.status >= 400 && res.status < 500 && res.status !== 429) {
174
+ throw new IrisHttpError(res.status, url, await safeText(res));
175
+ }
176
+ lastError = new IrisHttpError(res.status, url, await safeText(res));
177
+ } catch (err) {
178
+ if (err instanceof IrisParseError) throw err;
179
+ if (err instanceof IrisRateLimitedError) throw err;
180
+ if (err instanceof IrisHttpError) {
181
+ if (err.status < 500 && err.status !== 429) throw err;
182
+ }
183
+ if (signal?.aborted) throw new IrisAbortedError("Aborted by caller");
184
+ lastError = err;
185
+ } finally {
186
+ clearTimeout(timer);
187
+ signal?.removeEventListener("abort", onCallerAbort);
188
+ }
189
+ if (attempt < maxRetries) {
190
+ await sleep(baseDelayMs * (attempt + 1), signal);
191
+ }
192
+ }
193
+ if (lastError instanceof IrisHttpError) throw lastError;
194
+ throw new IrisNetworkError(
195
+ `Iris request failed after ${maxRetries + 1} attempts`,
196
+ lastError
197
+ );
198
+ }
199
+ async function safeText(res) {
200
+ try {
201
+ return await res.text();
202
+ } catch {
203
+ return "";
204
+ }
205
+ }
206
+ function sleep(ms, signal) {
207
+ return new Promise((resolve, reject) => {
208
+ if (signal?.aborted) {
209
+ reject(new IrisAbortedError("Aborted by caller"));
210
+ return;
211
+ }
212
+ const timer = setTimeout(() => {
213
+ signal?.removeEventListener("abort", onAbort);
214
+ resolve();
215
+ }, ms);
216
+ function onAbort() {
217
+ clearTimeout(timer);
218
+ reject(new IrisAbortedError("Aborted by caller"));
219
+ }
220
+ signal?.addEventListener("abort", onAbort);
221
+ });
222
+ }
223
+
224
+ // src/iris/types.ts
225
+ var FINALITY_FAST = 1e3;
226
+ var FINALITY_STANDARD = 2e3;
227
+
228
+ // src/fees/math.ts
229
+ var DECIMAL = /^(\d+)(?:\.(\d+))?$/;
230
+ function ceilDecimal(raw, decimals, what) {
231
+ const text = typeof raw === "number" ? String(raw) : typeof raw === "string" ? raw : "";
232
+ const match = DECIMAL.exec(text);
233
+ if (!match) {
234
+ throw new IrisParseError(`Iris ${what} is invalid: ${String(raw)}`);
235
+ }
236
+ const fraction = match[2] ?? "";
237
+ const kept = fraction.slice(0, decimals).padEnd(decimals, "0");
238
+ const dropped = /[1-9]/.test(fraction.slice(decimals)) ? 1n : 0n;
239
+ return BigInt((match[1] ?? "0") + kept) + dropped;
240
+ }
241
+ function parseFeeBps(raw) {
242
+ return ceilDecimal(raw, 2, "minimumFee");
243
+ }
244
+ function parseForwarderFee(raw) {
245
+ return ceilDecimal(raw, 0, "forwardFee");
246
+ }
247
+ function selectForwarderFee(tier, level = "med") {
248
+ if (!tier.forwardFee) {
249
+ throw new ProtocolError(
250
+ "fee_unavailable",
251
+ "Tier has no forwardFee. Fetch burn fees with includeForwarderFee=true"
252
+ );
253
+ }
254
+ const value = tier.forwardFee[level];
255
+ if (value < 0n) {
256
+ throw new ProtocolError(
257
+ "fee_unavailable",
258
+ `Invalid forwardFee.${level}: ${value}`
259
+ );
260
+ }
261
+ return value;
262
+ }
263
+
264
+ // src/iris/fees.ts
265
+ async function getBurnFees(network, sourceDomain, destinationDomain, options = {}) {
266
+ const { includeForwarderFee = true, ...fetchOpts } = options;
267
+ const base = irisBaseUrl(network);
268
+ const url = new URL(
269
+ `${base}/v2/burn/USDC/fees/${sourceDomain}/${destinationDomain}`
270
+ );
271
+ if (includeForwarderFee) url.searchParams.set("forward", "true");
272
+ const raw = await fetchIrisJson(url.toString(), fetchOpts);
273
+ if (!Array.isArray(raw)) {
274
+ throw new IrisParseError("Iris burn-fees response is not an array");
275
+ }
276
+ return raw.map((row) => normalizeTier(row)).filter((t) => t !== null);
277
+ }
278
+ function normalizeTier(row) {
279
+ if (!row || typeof row !== "object") {
280
+ throw new IrisParseError("Iris fee tier is not an object");
281
+ }
282
+ const r = row;
283
+ const threshold = r.finalityThreshold;
284
+ if (threshold !== FINALITY_FAST && threshold !== FINALITY_STANDARD) {
285
+ return null;
286
+ }
287
+ const out = {
288
+ finalityThreshold: threshold,
289
+ minimumFee: parseFeeBps(r.minimumFee)
290
+ };
291
+ if (r.forwardFee && typeof r.forwardFee === "object") {
292
+ const ff = r.forwardFee;
293
+ out.forwardFee = {
294
+ low: parseForwarderFee(ff.low),
295
+ med: parseForwarderFee(ff.med),
296
+ high: parseForwarderFee(ff.high)
297
+ };
298
+ }
299
+ return out;
300
+ }
301
+ function pickFeeTier(tiers, speed) {
302
+ const threshold = speed === "fast" ? FINALITY_FAST : FINALITY_STANDARD;
303
+ const tier = tiers.find((t) => t.finalityThreshold === threshold);
304
+ if (!tier) {
305
+ throw new ProtocolError(
306
+ "tier_not_found",
307
+ `Iris returned no ${speed === "fast" ? "Fast" : "Standard"} Transfer fee for this route.`
308
+ );
309
+ }
310
+ return tier;
311
+ }
312
+ function pickForwarderFee(tier, level = "med") {
313
+ return selectForwarderFee(tier, level);
314
+ }
315
+
316
+ // src/iris/messages.ts
317
+ async function getMessages(query, options) {
318
+ const { network, baseUrl, ...fetchOpts } = options;
319
+ const base = (baseUrl ?? irisBaseUrl(network)).replace(/\/+$/, "");
320
+ const url = new URL(`${base}/v2/messages/${query.sourceDomain}`);
321
+ if (query.transactionHash) {
322
+ url.searchParams.set("transactionHash", query.transactionHash);
323
+ }
324
+ if (query.nonce) {
325
+ url.searchParams.set("nonce", query.nonce);
326
+ }
327
+ try {
328
+ const json = await fetchIrisJson(
329
+ url.toString(),
330
+ fetchOpts
331
+ );
332
+ return json.messages ?? [];
333
+ } catch (err) {
334
+ if (err instanceof IrisHttpError && err.status === 404) {
335
+ return [];
336
+ }
337
+ throw err;
338
+ }
339
+ }
340
+ var DEFAULT_INTERVAL_MS = 3e3;
341
+ var DEFAULT_TIMEOUT_MS2 = 15 * 60 * 1e3;
342
+ var TIMEOUT_MARGIN_MS = 5 * 60 * 1e3;
343
+ function pollBudget(eta) {
344
+ const intervalMs = eta.max <= 60 ? 2e3 : eta.max <= 30 * 60 ? 1e4 : 3e4;
345
+ return { intervalMs, timeoutMs: eta.max * 2 * 1e3 + TIMEOUT_MARGIN_MS };
346
+ }
347
+ function isForwardDone(message) {
348
+ return (message.forwardState === "CONFIRMED" || message.forwardState === "COMPLETE") && !!message.forwardTxHash;
349
+ }
350
+ function defaultIsDone(message) {
351
+ if (message.forwardState != null) {
352
+ return isForwardDone(message) || message.forwardState === "FAILED";
353
+ }
354
+ return message.status === "complete";
355
+ }
356
+ async function pollMessages(query, options) {
357
+ const {
358
+ intervalMs = DEFAULT_INTERVAL_MS,
359
+ timeoutMs = DEFAULT_TIMEOUT_MS2,
360
+ isDone = defaultIsDone,
361
+ onRateLimited,
362
+ signal,
363
+ ...rest
364
+ } = options;
365
+ const limiter = rest.limiter ?? defaultIrisLimiter;
366
+ let deadline = limiter.now() + timeoutMs;
367
+ let latest = null;
368
+ while (limiter.now() < deadline) {
369
+ if (signal?.aborted) throw new IrisAbortedError("Aborted by caller");
370
+ let messages;
371
+ try {
372
+ messages = await getMessages(query, { ...rest, signal });
373
+ } catch (err) {
374
+ if (!(err instanceof IrisRateLimitedError)) throw err;
375
+ onRateLimited?.(err.retryAt);
376
+ const waitMs = Math.max(0, err.retryAt - limiter.now());
377
+ deadline += waitMs;
378
+ await limiter.sleep(waitMs);
379
+ continue;
380
+ }
381
+ if (messages.length > 0) {
382
+ const first = messages[0];
383
+ if (first) {
384
+ latest = first;
385
+ if (isDone(first)) return first;
386
+ }
387
+ }
388
+ await sleep2(intervalMs, signal);
389
+ }
390
+ return latest;
391
+ }
392
+ function sleep2(ms, signal) {
393
+ return new Promise((resolve, reject) => {
394
+ if (signal?.aborted) {
395
+ reject(new IrisAbortedError("Aborted by caller"));
396
+ return;
397
+ }
398
+ const timer = setTimeout(() => {
399
+ signal?.removeEventListener("abort", onAbort);
400
+ resolve();
401
+ }, ms);
402
+ function onAbort() {
403
+ clearTimeout(timer);
404
+ reject(new IrisAbortedError("Aborted by caller"));
405
+ }
406
+ signal?.addEventListener("abort", onAbort);
407
+ });
408
+ }
409
+
410
+ // src/iris/reattest.ts
411
+ async function reattest(nonce, options) {
412
+ const { network, ...fetchOpts } = options;
413
+ const url = `${irisBaseUrl(network)}/v2/reattest/${nonce}`;
414
+ let json;
415
+ try {
416
+ json = await fetchIrisJson(url, fetchOpts, {
417
+ method: "POST",
418
+ body: {}
419
+ });
420
+ } catch (err) {
421
+ if (err instanceof IrisHttpError && err.status === 404) return null;
422
+ throw err;
423
+ }
424
+ const acknowledged = typeof json === "object" && json !== null && "nonce" in json ? json.nonce : void 0;
425
+ if (typeof acknowledged !== "string") {
426
+ throw new IrisParseError("Iris reattest reply has no nonce");
427
+ }
428
+ return { nonce: acknowledged };
429
+ }
430
+
431
+ // src/iris/quote.ts
432
+ async function getSignedBurnQuote(network, sourceDomain, destinationDomain, request, options = {}) {
433
+ const url = `${irisBaseUrl(network)}/v2/quote/burn/usdc/${sourceDomain}/${destinationDomain}`;
434
+ const raw = await fetchIrisJson(url, options, {
435
+ method: "POST",
436
+ body: {
437
+ amount: request.amount.toString(),
438
+ feeToken: request.feeToken,
439
+ requests: request.requests.map((type) => ({ type }))
440
+ }
441
+ });
442
+ const quote = parseQuote(raw);
443
+ if (quote.burnAmount !== void 0 && quote.burnAmount !== request.amount) {
444
+ throw new IrisParseError(
445
+ `Iris quoted ${quote.burnAmount} subunits, not the ${request.amount} requested`
446
+ );
447
+ }
448
+ if (quote.feeToken.toLowerCase() !== request.feeToken.toLowerCase()) {
449
+ throw new IrisParseError(
450
+ `Iris priced the fee in ${quote.feeToken}, not ${request.feeToken}`
451
+ );
452
+ }
453
+ const types = quote.items.map((i) => i.type).sort();
454
+ if (types.join() !== [...request.requests].sort().join()) {
455
+ throw new IrisParseError(
456
+ `Iris priced ${types.join(", ")}, not ${request.requests.join(", ")}`
457
+ );
458
+ }
459
+ return quote;
460
+ }
461
+ function parseQuote(raw) {
462
+ const fail = (what) => {
463
+ throw new IrisParseError(`Iris quote: ${what}`);
464
+ };
465
+ if (typeof raw !== "object" || raw === null) return fail("not an object");
466
+ const r = raw;
467
+ const signedQuote = r.signedQuote;
468
+ if (typeof signedQuote !== "string" || !/^0x([0-9a-fA-F]{2})+$/.test(signedQuote)) {
469
+ return fail("signedQuote is not hex bytes");
470
+ }
471
+ if (!Number.isSafeInteger(r.issuedAt))
472
+ return fail("issuedAt is not a timestamp");
473
+ const feeToken = r.feeToken;
474
+ if (typeof feeToken !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(feeToken)) {
475
+ return fail("feeToken is not an address");
476
+ }
477
+ const feeTotalAmount = subunits(r.feeTotalAmount) ?? fail("feeTotalAmount");
478
+ const burnAmount = r.burnAmount === void 0 ? void 0 : subunits(r.burnAmount) ?? fail("burnAmount");
479
+ if (!Array.isArray(r.items) || r.items.length === 0) return fail("no items");
480
+ const items = r.items.map(
481
+ (item) => {
482
+ const i = item ?? {};
483
+ if (i.type !== "FORWARD" && i.type !== "PRE_FINALITY") {
484
+ return fail(`unknown item type ${String(i.type)}`);
485
+ }
486
+ return {
487
+ type: i.type,
488
+ amount: subunits(i.amount) ?? fail("item amount")
489
+ };
490
+ }
491
+ );
492
+ if (items.reduce((sum, i) => sum + i.amount, 0n) !== feeTotalAmount) {
493
+ return fail("items do not add up to feeTotalAmount");
494
+ }
495
+ return {
496
+ signedQuote,
497
+ issuedAt: r.issuedAt,
498
+ expiry: parseExpiry(r.expiry) ?? fail("expiry"),
499
+ feeTotalAmount,
500
+ feeToken,
501
+ ...burnAmount !== void 0 && { burnAmount },
502
+ items
503
+ };
504
+ }
505
+ function parseExpiry(raw) {
506
+ if (typeof raw !== "object" || raw === null) return void 0;
507
+ const e = raw;
508
+ if (e.mode === "TIMESTAMP" && Number.isSafeInteger(e.expiresAt)) {
509
+ return { mode: "TIMESTAMP", expiresAt: e.expiresAt };
510
+ }
511
+ if (e.mode === "BLOCK_NUMBER" && Number.isSafeInteger(e.expiresAtBlock)) {
512
+ return {
513
+ mode: "BLOCK_NUMBER",
514
+ expiresAtBlock: BigInt(e.expiresAtBlock)
515
+ };
516
+ }
517
+ return void 0;
518
+ }
519
+ function subunits(raw) {
520
+ return typeof raw === "string" && /^(0|[1-9]\d*)$/.test(raw) ? BigInt(raw) : void 0;
521
+ }
522
+ async function getFastBurnAllowance(network, options = {}) {
523
+ const raw = await fetchIrisJson(
524
+ `${irisBaseUrl(network)}/v2/fastBurn/USDC/allowance`,
525
+ options
526
+ );
527
+ const value = typeof raw === "object" && raw !== null && "allowance" in raw ? raw.allowance : void 0;
528
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
529
+ throw new IrisParseError("Iris allowance is not a non-negative number");
530
+ }
531
+ const match = /^(\d+)(?:\.(\d+))?$/.exec(String(value));
532
+ if (!match)
533
+ throw new IrisParseError(`Iris allowance ${value} is not decimal`);
534
+ const frac = (match[2] ?? "").padEnd(6, "0").slice(0, 6);
535
+ return BigInt(match[1]) * 1000000n + BigInt(frac);
536
+ }
537
+
538
+ exports.FINALITY_FAST = FINALITY_FAST;
539
+ exports.FINALITY_STANDARD = FINALITY_STANDARD;
540
+ exports.IRIS_MAINNET_URL = IRIS_MAINNET_URL;
541
+ exports.IRIS_SANDBOX_URL = IRIS_SANDBOX_URL;
542
+ exports.createIrisLimiter = createIrisLimiter;
543
+ exports.defaultIrisLimiter = defaultIrisLimiter;
544
+ exports.fetchIrisJson = fetchIrisJson;
545
+ exports.getBurnFees = getBurnFees;
546
+ exports.getFastBurnAllowance = getFastBurnAllowance;
547
+ exports.getMessages = getMessages;
548
+ exports.getSignedBurnQuote = getSignedBurnQuote;
549
+ exports.irisBaseUrl = irisBaseUrl;
550
+ exports.isForwardDone = isForwardDone;
551
+ exports.pickFeeTier = pickFeeTier;
552
+ exports.pickForwarderFee = pickForwarderFee;
553
+ exports.pollBudget = pollBudget;
554
+ exports.pollMessages = pollMessages;
555
+ exports.reattest = reattest;
556
+ //# sourceMappingURL=iris.cjs.map
557
+ //# sourceMappingURL=iris.cjs.map