@anywalletkit/protocol-swap2-core 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -22,6 +22,7 @@ var src_exports = {};
22
22
  __export(src_exports, {
23
23
  BaseSwap2Protocol: () => BaseSwap2Protocol,
24
24
  NATIVE_DECIMALS: () => NATIVE_DECIMALS,
25
+ SLIPPAGE_ROUNDING_BPS: () => SLIPPAGE_ROUNDING_BPS,
25
26
  SWAP2_CHAIN_ALIASES: () => SWAP2_CHAIN_ALIASES,
26
27
  SWAP2_ID_PREFIX: () => SWAP2_ID_PREFIX,
27
28
  SingleProviderSwap2: () => SingleProviderSwap2,
@@ -55,6 +56,7 @@ __export(src_exports, {
55
56
  remoteProvider: () => remoteProvider,
56
57
  shortAddress: () => shortAddress,
57
58
  slippageClassOf: () => slippageClassOf,
59
+ slippageFloor: () => slippageFloor,
58
60
  sourceAssetOf: () => sourceAssetOf,
59
61
  suggestSlippage: () => suggestSlippage,
60
62
  swap2ChainId: () => swap2ChainId,
@@ -122,6 +124,11 @@ var Swap2MaybeSentError = class extends Swap2ExecutionError {
122
124
  };
123
125
 
124
126
  // src/validate.ts
127
+ var SLIPPAGE_ROUNDING_BPS = 1;
128
+ function slippageFloor(dest, slippageBps) {
129
+ const keep = 1e4 - slippageBps - SLIPPAGE_ROUNDING_BPS;
130
+ return keep <= 0 ? 0n : dest * BigInt(keep) / 10000n;
131
+ }
125
132
  function validQuote(q, params, provider) {
126
133
  try {
127
134
  if (q.provider !== provider || q.sourceAmount !== params.amountIn) return false;
@@ -136,8 +143,7 @@ function validQuote(q, params, provider) {
136
143
  }
137
144
  if (params.slippageBps !== void 0) {
138
145
  if (!Number.isInteger(params.slippageBps) || params.slippageBps < 0 || params.slippageBps >= 1e4) return false;
139
- const required = dest * BigInt(1e4 - params.slippageBps) / 10000n;
140
- if (min < required) return false;
146
+ if (min < slippageFloor(dest, params.slippageBps)) return false;
141
147
  }
142
148
  if (!Number.isFinite(q.etaSeconds) || q.etaSeconds < 0) return false;
143
149
  if (!Number.isFinite(q.reliability) || q.reliability < 0 || q.reliability > 1) return false;
@@ -571,16 +577,18 @@ function reviewedIdentity(plan) {
571
577
  return JSON.stringify(pinned, (_k, v) => typeof v === "bigint" ? v.toString() : v);
572
578
  }
573
579
  var SWAP2_ID_PREFIX = "awk1";
574
- function encodeId(provider, intentId, sourceTxHash, destTxHash) {
580
+ var CONFIRMED_SEGMENT = "ok";
581
+ function encodeId(provider, intentId, sourceTxHash, destTxHash, destTxConfirmed = false) {
575
582
  const enc = (v) => encodeURIComponent(v).replace(/\./g, "%2E");
576
- const parts = [provider, intentId, ...sourceTxHash != null ? [sourceTxHash, ...destTxHash != null ? [destTxHash] : []] : []];
583
+ const dest = destTxHash != null ? [destTxHash, ...destTxConfirmed ? [CONFIRMED_SEGMENT] : []] : [];
584
+ const parts = [provider, intentId, ...sourceTxHash != null ? [sourceTxHash, ...dest] : []];
577
585
  return `${SWAP2_ID_PREFIX}.${parts.map(enc).join(".")}`;
578
586
  }
579
587
  function decodeId(id) {
580
588
  try {
581
589
  if (!id.startsWith(`${SWAP2_ID_PREFIX}.`)) throw new Error("unsupported id version");
582
590
  const parts = id.slice(SWAP2_ID_PREFIX.length + 1).split(".");
583
- if (parts.length < 2 || parts.length > 4 || !parts[0] || !parts[1]) throw new Error("invalid id shape");
591
+ if (parts.length < 2 || parts.length > 5 || !parts[0] || !parts[1]) throw new Error("invalid id shape");
584
592
  const provider = decodeURIComponent(parts[0]);
585
593
  const intentId = decodeURIComponent(parts[1]);
586
594
  const sourceTxHash = parts[2] ? decodeURIComponent(parts[2]) : void 0;
@@ -588,7 +596,8 @@ function decodeId(id) {
588
596
  if (!/^[A-Za-z0-9_-]+$/.test(provider) || !intentId || parts[2] !== void 0 && !sourceTxHash || parts[3] !== void 0 && !destTxHash) {
589
597
  throw new Error("invalid id contents");
590
598
  }
591
- return { provider, intentId, ...sourceTxHash ? { sourceTxHash } : {}, ...destTxHash ? { destTxHash } : {} };
599
+ if (parts[4] !== void 0 && parts[4] !== CONFIRMED_SEGMENT) throw new Error("invalid id contents");
600
+ return { provider, intentId, ...sourceTxHash ? { sourceTxHash } : {}, ...destTxHash ? { destTxHash } : {}, ...parts[4] !== void 0 ? { destTxConfirmed: true } : {} };
592
601
  } catch {
593
602
  throw new Swap2ValidationError("swap2: malformed swap2 status id");
594
603
  }
@@ -721,13 +730,30 @@ function assertQuoteConstraints(q, options) {
721
730
  }
722
731
  if (options.slippage !== void 0) {
723
732
  const scale = 1000000000n;
724
- const permitted = BigInt(floorScaled(options.slippage, 1e9));
725
- const required = nominal * (scale - permitted) / scale;
733
+ const permitted = BigInt(floorScaled(options.slippage, 1e9)) + BigInt(SLIPPAGE_ROUNDING_BPS) * 100000n;
734
+ const required = permitted >= scale ? 0n : nominal * (scale - permitted) / scale;
726
735
  if (guaranteed < required) {
727
736
  throw new Error(`swap2: quote exceeds caller slippage (${options.slippage})`);
728
737
  }
729
738
  }
730
739
  }
740
+ function bridgeOutOf(params, q, toTokenDecimals) {
741
+ let decimals = (0, import_swap2_assets2.resolveAsset)(params.destChain, params.destToken)?.decimals;
742
+ if (decimals === void 0 && toTokenDecimals !== void 0) {
743
+ if (q.destDecimals !== void 0 && q.destDecimals !== toTokenDecimals) {
744
+ throw new Swap2ValidationError(`swap2: the quote's destDecimals ${q.destDecimals} do not match toTokenDecimals ${toTokenDecimals}`);
745
+ }
746
+ decimals = toTokenDecimals;
747
+ }
748
+ return {
749
+ destChain: params.destChain,
750
+ recipient: params.destAddress,
751
+ destToken: params.destToken,
752
+ minAmountOut: q.minDestAmount,
753
+ ...decimals !== void 0 ? { destDecimals: decimals } : {},
754
+ ...params.slippageBps !== void 0 ? { slippageBps: params.slippageBps } : {}
755
+ };
756
+ }
731
757
  function requireExecutionFloor(options) {
732
758
  if (options.minAmountOut === void 0) {
733
759
  throw new Error("swap2: swap2() requires caller-supplied minAmountOut");
@@ -826,6 +852,10 @@ var BaseSwap2Protocol = class extends import_core.Swap2Protocol {
826
852
  }
827
853
  const destChain = toCaip2(options.toChain, this.fromChain);
828
854
  const constraints = callerConstraints(options);
855
+ const toDecimals = options.toTokenDecimals;
856
+ if (toDecimals !== void 0 && (!Number.isInteger(toDecimals) || toDecimals < 0 || toDecimals > 36)) {
857
+ throw new Swap2ValidationError("swap2: toTokenDecimals must be whole decimals in 0..36");
858
+ }
829
859
  const amountIn = exactPositiveAmount(options.fromTokenAmount, "fromTokenAmount");
830
860
  const source = this.sourceAsset(options);
831
861
  const params = {
@@ -1355,7 +1385,8 @@ var BaseSwap2Protocol = class extends import_core.Swap2Protocol {
1355
1385
  maxValueOut: ((source.contract ? 0n : BigInt(q.sourceAmount)) + allowance).toString(),
1356
1386
  // Only a same-chain swap lands in this account; a bridge delivers elsewhere.
1357
1387
  ...params.destChain === this.fromChain ? { tokenIn: { token: params.destToken, minAmount: q.minDestAmount } } : {},
1358
- ...source.contract ? { source: { token: source.contract, maxAmount: q.sourceAmount } } : {}
1388
+ ...source.contract ? { source: { token: source.contract, maxAmount: q.sourceAmount } } : {},
1389
+ ...params.destChain !== this.fromChain ? { bridgeOut: bridgeOutOf(params, q, executionOptions.toTokenDecimals) } : {}
1359
1390
  };
1360
1391
  sent = await signer.sendSerializedTransaction(unsigned.serializedTx, expect);
1361
1392
  } else if (tokenId) {
@@ -1439,10 +1470,10 @@ var BaseSwap2Protocol = class extends import_core.Swap2Protocol {
1439
1470
  }
1440
1471
  /** Status of a swap by the id `swap2()` returned, asked from the provider that executed it. */
1441
1472
  async getSwap2Status(id, _options) {
1442
- const { provider: name, intentId, sourceTxHash, destTxHash } = decodeId(id);
1473
+ const { provider: name, intentId, sourceTxHash, destTxHash, destTxConfirmed } = decodeId(id);
1443
1474
  const provider = this.providerFor(name);
1444
1475
  if (!provider) throw new Error(`swap2: no swap2 found for id '${id}'`);
1445
- return mapStatus(await provider.trackIntent({ intentId, ...sourceTxHash != null ? { sourceTxHash } : {}, ...destTxHash != null ? { destTxHash } : {} }));
1476
+ return mapStatus(await provider.trackIntent({ intentId, ...sourceTxHash != null ? { sourceTxHash } : {}, ...destTxHash != null ? { destTxHash, ...destTxConfirmed ? { destTxConfirmed } : {} } : {} }));
1446
1477
  }
1447
1478
  /**
1448
1479
  * Two-phase routes (CCTP-style burn-and-mint): once `getSwap2Status` reports
@@ -1515,7 +1546,21 @@ var BaseSwap2Protocol = class extends import_core.Swap2Protocol {
1515
1546
  });
1516
1547
  if (!sent.hash) throw new Swap2ExecutionError("swap2: account did not return a claim transaction hash");
1517
1548
  this.emitPhase("broadcast", { hash: sent.hash });
1518
- return { hash: sent.hash, chain: unsigned.chain, id: encodeId(name, intentId, sourceTxHash, sent.hash) };
1549
+ let confirmed = false;
1550
+ if (typeof signer.waitForTransaction === "function") {
1551
+ try {
1552
+ await signer.waitForTransaction(sent.hash);
1553
+ confirmed = true;
1554
+ } catch (e) {
1555
+ const err = e;
1556
+ const timedOut = /timeout|timed out/i.test(String(err?.name ?? "")) || /timed out|timeout/i.test(String(err?.message ?? ""));
1557
+ const reverted = err?.code === "reverted" || !timedOut && /\breverted\b/i.test(String(err?.message ?? ""));
1558
+ if (reverted) {
1559
+ throw new Swap2ExecutionError(`swap2: the claim transaction ${sent.hash} on ${unsigned.chain} reverted \u2014 poll the status of '${id}' and claim again if it is still action-required`, { cause: e });
1560
+ }
1561
+ }
1562
+ }
1563
+ return { hash: sent.hash, chain: unsigned.chain, id: encodeId(name, intentId, sourceTxHash, sent.hash, confirmed) };
1519
1564
  }
1520
1565
  /** The inherited bridge() has no floor field: quote first, then bind execution
1521
1566
  * to that quote's minimum so the inherited convenience API stays safe. */
@@ -1592,7 +1637,10 @@ function structuredCloneSafe(value) {
1592
1637
  if (value === null || typeof value !== "object") return value;
1593
1638
  if (Array.isArray(value)) return value.map((v) => structuredCloneSafe(v));
1594
1639
  const out = {};
1595
- for (const [k, v] of Object.entries(value)) out[k] = structuredCloneSafe(v);
1640
+ for (const [k, v] of Object.entries(value)) {
1641
+ if (k === "__proto__") Object.defineProperty(out, k, { value: structuredCloneSafe(v), writable: true, enumerable: true, configurable: true });
1642
+ else out[k] = structuredCloneSafe(v);
1643
+ }
1596
1644
  return out;
1597
1645
  }
1598
1646
  function deepFreeze(value) {