@anywalletkit/protocol-swap2-core 0.4.1 → 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.
Files changed (47) hide show
  1. package/README.md +2 -1
  2. package/dist/{chunk-Y3VUEWYI.js → chunk-IGTMUTNJ.js} +106 -28
  3. package/dist/chunk-IGTMUTNJ.js.map +1 -0
  4. package/dist/{chunk-GDRKGFLH.js → chunk-JFGMF6FO.js} +2 -2
  5. package/dist/{chunk-SKV5BVXK.js → chunk-MNJTMSFN.js} +8 -2
  6. package/dist/chunk-MNJTMSFN.js.map +1 -0
  7. package/dist/{chunk-4H2JVYXT.js → chunk-ZMWK3RQ4.js} +4 -3
  8. package/dist/chunk-ZMWK3RQ4.js.map +1 -0
  9. package/dist/index.cjs +119 -37
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +2 -1
  12. package/dist/index.d.ts +2 -1
  13. package/dist/index.js +7 -5
  14. package/dist/remote.cjs +3 -1
  15. package/dist/remote.cjs.map +1 -1
  16. package/dist/remote.d.cts +6 -0
  17. package/dist/remote.d.ts +6 -0
  18. package/dist/remote.js +2 -2
  19. package/dist/server.cjs +8 -4
  20. package/dist/server.cjs.map +1 -1
  21. package/dist/server.d.cts +7 -0
  22. package/dist/server.d.ts +7 -0
  23. package/dist/server.js +8 -5
  24. package/dist/server.js.map +1 -1
  25. package/dist/swap2/base.cjs +101 -26
  26. package/dist/swap2/base.cjs.map +1 -1
  27. package/dist/swap2/base.d.cts +21 -1
  28. package/dist/swap2/base.d.ts +21 -1
  29. package/dist/swap2/base.js +1 -1
  30. package/dist/swap2/single.cjs +101 -26
  31. package/dist/swap2/single.cjs.map +1 -1
  32. package/dist/swap2/single.d.cts +1 -0
  33. package/dist/swap2/single.d.ts +1 -0
  34. package/dist/swap2/single.js +2 -2
  35. package/dist/types.cjs.map +1 -1
  36. package/dist/types.d.cts +13 -0
  37. package/dist/types.d.ts +13 -0
  38. package/dist/util.cjs +6 -0
  39. package/dist/util.cjs.map +1 -1
  40. package/dist/util.d.cts +12 -1
  41. package/dist/util.d.ts +12 -1
  42. package/dist/util.js +3 -1
  43. package/package.json +7 -7
  44. package/dist/chunk-4H2JVYXT.js.map +0 -1
  45. package/dist/chunk-SKV5BVXK.js.map +0 -1
  46. package/dist/chunk-Y3VUEWYI.js.map +0 -1
  47. /package/dist/{chunk-GDRKGFLH.js.map → chunk-JFGMF6FO.js.map} +0 -0
@@ -1,4 +1,5 @@
1
1
  import { Swap2Protocol, Swap2ProtocolConfig, Swap2Options, Swap2Fee, Swap2Quote, Swap2Result, Swap2StatusOptions, Swap2StatusResult, BridgeOptions, BridgeResult, SwapOptions, SwapResult, Swap2SupportedChain, Swap2SupportedTokensOptions, Swap2SupportedToken } from '@anywalletkit/core';
2
+ import { CcAsset } from '@anywalletkit/swap2-assets';
2
3
  import { QuoteParams, Quote, CrossChainProvider } from '../types.cjs';
3
4
 
4
5
  /**
@@ -89,6 +90,15 @@ interface Swap2EngineConfig extends Swap2ProtocolConfig {
89
90
  * called again with `requoted: true`, and throwing then revokes the allowance.
90
91
  */
91
92
  reviewExecution?: (plan: Swap2ExecutionPlan) => Promise<void> | void;
93
+ /**
94
+ * Accept tokens the swap catalog does not list, named by their contract (EVM) or mint (Solana):
95
+ * tokens the user added at their own risk. Off by default. The source token's decimals are read
96
+ * with the account's `getTokenInfo` (a `fromTokenDecimals` that disagrees is refused); an account
97
+ * without it must be given `fromTokenDecimals`. Only providers that trade any token (LI.FI,
98
+ * Uniswap, Balancer, Fynd, Jupiter) quote them, and they have no USD value: the router cannot
99
+ * judge their price, so show the user the amounts.
100
+ */
101
+ allowUnlistedTokens?: boolean;
92
102
  }
93
103
  /** What `swap2()` is about to sign, handed to {@link Swap2EngineConfig.reviewExecution}. */
94
104
  interface Swap2ExecutionPlan {
@@ -202,8 +212,18 @@ declare abstract class BaseSwap2Protocol extends Swap2Protocol {
202
212
  protected protocolEnabled(): boolean;
203
213
  /** Pair-level capability used when discovery specifies both endpoints. */
204
214
  protected pairSupported(fromChain: string, toChain: string): boolean;
215
+ private unlistedAllowed;
216
+ /** The source asset the options name: the catalog's, or (allowUnlistedTokens) an unlisted one with its decimals. */
217
+ protected sourceAsset(options: Swap2Options): CcAsset | undefined;
218
+ private readonly unlistedDecimals;
219
+ /**
220
+ * Options ready to quote: with `allowUnlistedTokens`, an unlisted source token gets its decimals,
221
+ * read from the chain through the account (cached per token) — a caller's `fromTokenDecimals` that
222
+ * disagrees is refused. Listed tokens and every option otherwise pass through unchanged.
223
+ */
224
+ protected prepareOptions(options: Swap2Options): Promise<Swap2Options>;
205
225
  /** Swap2Options → the engine's QuoteParams (exact-in only). */
206
- protected toParams(options: Swap2Options, dry: boolean): QuoteParams;
226
+ protected toParams(given: Swap2Options, dry: boolean): QuoteParams;
207
227
  private assertAccountChain;
208
228
  protected assertFeeConfig(config?: Swap2ProtocolConfig): void;
209
229
  protected assertFeeCaps(fees: readonly Swap2Fee[] | undefined, sourceAmount: bigint, sourceToken: string, config?: Swap2ProtocolConfig): void;
@@ -1,4 +1,5 @@
1
1
  import { Swap2Protocol, Swap2ProtocolConfig, Swap2Options, Swap2Fee, Swap2Quote, Swap2Result, Swap2StatusOptions, Swap2StatusResult, BridgeOptions, BridgeResult, SwapOptions, SwapResult, Swap2SupportedChain, Swap2SupportedTokensOptions, Swap2SupportedToken } from '@anywalletkit/core';
2
+ import { CcAsset } from '@anywalletkit/swap2-assets';
2
3
  import { QuoteParams, Quote, CrossChainProvider } from '../types.js';
3
4
 
4
5
  /**
@@ -89,6 +90,15 @@ interface Swap2EngineConfig extends Swap2ProtocolConfig {
89
90
  * called again with `requoted: true`, and throwing then revokes the allowance.
90
91
  */
91
92
  reviewExecution?: (plan: Swap2ExecutionPlan) => Promise<void> | void;
93
+ /**
94
+ * Accept tokens the swap catalog does not list, named by their contract (EVM) or mint (Solana):
95
+ * tokens the user added at their own risk. Off by default. The source token's decimals are read
96
+ * with the account's `getTokenInfo` (a `fromTokenDecimals` that disagrees is refused); an account
97
+ * without it must be given `fromTokenDecimals`. Only providers that trade any token (LI.FI,
98
+ * Uniswap, Balancer, Fynd, Jupiter) quote them, and they have no USD value: the router cannot
99
+ * judge their price, so show the user the amounts.
100
+ */
101
+ allowUnlistedTokens?: boolean;
92
102
  }
93
103
  /** What `swap2()` is about to sign, handed to {@link Swap2EngineConfig.reviewExecution}. */
94
104
  interface Swap2ExecutionPlan {
@@ -202,8 +212,18 @@ declare abstract class BaseSwap2Protocol extends Swap2Protocol {
202
212
  protected protocolEnabled(): boolean;
203
213
  /** Pair-level capability used when discovery specifies both endpoints. */
204
214
  protected pairSupported(fromChain: string, toChain: string): boolean;
215
+ private unlistedAllowed;
216
+ /** The source asset the options name: the catalog's, or (allowUnlistedTokens) an unlisted one with its decimals. */
217
+ protected sourceAsset(options: Swap2Options): CcAsset | undefined;
218
+ private readonly unlistedDecimals;
219
+ /**
220
+ * Options ready to quote: with `allowUnlistedTokens`, an unlisted source token gets its decimals,
221
+ * read from the chain through the account (cached per token) — a caller's `fromTokenDecimals` that
222
+ * disagrees is refused. Listed tokens and every option otherwise pass through unchanged.
223
+ */
224
+ protected prepareOptions(options: Swap2Options): Promise<Swap2Options>;
205
225
  /** Swap2Options → the engine's QuoteParams (exact-in only). */
206
- protected toParams(options: Swap2Options, dry: boolean): QuoteParams;
226
+ protected toParams(given: Swap2Options, dry: boolean): QuoteParams;
207
227
  private assertAccountChain;
208
228
  protected assertFeeConfig(config?: Swap2ProtocolConfig): void;
209
229
  protected assertFeeCaps(fees: readonly Swap2Fee[] | undefined, sourceAmount: bigint, sourceToken: string, config?: Swap2ProtocolConfig): void;
@@ -7,7 +7,7 @@ import {
7
7
  nativeUnits,
8
8
  normalizeTypedDomain,
9
9
  swap2ChainId
10
- } from "../chunk-Y3VUEWYI.js";
10
+ } from "../chunk-IGTMUTNJ.js";
11
11
  import "../chunk-IVR3SREQ.js";
12
12
  export {
13
13
  BaseSwap2Protocol,
@@ -111,12 +111,14 @@ var inFlightKey = (fromChain, o) => JSON.stringify(
111
111
  [fromChain, o.fromToken, o.toToken, o.toChain, o.recipient, o.refundAddress, o.fromTokenAmount, o.toTokenAmount, o.minAmountOut, o.slippage],
112
112
  (_k, v) => typeof v === "bigint" ? v.toString() : v
113
113
  );
114
+ var LOST_ANSWER = /socket hang up|ECONNRESET|ECONNABORTED|ETIMEDOUT|EPIPE|ENETUNREACH|EHOSTUNREACH|EAI_AGAIN|UND_ERR_[A-Z_]+|timed? ?out|TimeoutError|request aborted|AbortError|connection (?:closed|reset)|fetch failed|failed to fetch|network request failed|other side closed/i;
114
115
  function maybeSent(error) {
115
- for (let e = error, i = 0; e && typeof e === "object" && i < 5; e = e.cause, i++) {
116
- if (e.maybeSent === true) return typeof e.hash === "string" && e.hash ? { hash: e.hash } : {};
117
- }
118
- const text = error instanceof Error ? error.message : String(error);
119
- return /socket hang up|ECONNRESET|ETIMEDOUT|EPIPE|timed? ?out|request aborted|connection (?:closed|reset)/i.test(text) ? {} : null;
116
+ const chain = [];
117
+ for (let e = error, i = 0; e && typeof e === "object" && i < 6; e = e.cause, i++) chain.push(e);
118
+ for (const e of chain) if (e.maybeSent === true) return typeof e.hash === "string" && e.hash ? { hash: e.hash } : {};
119
+ if (chain.some((e) => e.answered === true)) return null;
120
+ const texts = chain.length ? chain.map((e) => [e.message, e.code, e.name].filter((x) => typeof x === "string").join(" ")) : [String(error)];
121
+ return texts.some((t) => LOST_ANSWER.test(t)) ? {} : null;
120
122
  }
121
123
  var NATIVE_DECIMALS = Object.freeze({
122
124
  "eip155:5042": 18
@@ -200,22 +202,24 @@ function reviewedIdentity(plan) {
200
202
  return JSON.stringify(pinned, (_k, v) => typeof v === "bigint" ? v.toString() : v);
201
203
  }
202
204
  var SWAP2_ID_PREFIX = "awk1";
203
- function encodeId(provider, intentId, sourceTxHash) {
205
+ function encodeId(provider, intentId, sourceTxHash, destTxHash) {
204
206
  const enc = (v) => encodeURIComponent(v).replace(/\./g, "%2E");
205
- return `${SWAP2_ID_PREFIX}.${[provider, intentId, sourceTxHash].filter((v) => v != null).map(enc).join(".")}`;
207
+ const parts = [provider, intentId, ...sourceTxHash != null ? [sourceTxHash, ...destTxHash != null ? [destTxHash] : []] : []];
208
+ return `${SWAP2_ID_PREFIX}.${parts.map(enc).join(".")}`;
206
209
  }
207
210
  function decodeId(id) {
208
211
  try {
209
212
  if (!id.startsWith(`${SWAP2_ID_PREFIX}.`)) throw new Error("unsupported id version");
210
213
  const parts = id.slice(SWAP2_ID_PREFIX.length + 1).split(".");
211
- if (parts.length < 2 || parts.length > 3 || !parts[0] || !parts[1]) throw new Error("invalid id shape");
214
+ if (parts.length < 2 || parts.length > 4 || !parts[0] || !parts[1]) throw new Error("invalid id shape");
212
215
  const provider = decodeURIComponent(parts[0]);
213
216
  const intentId = decodeURIComponent(parts[1]);
214
217
  const sourceTxHash = parts[2] ? decodeURIComponent(parts[2]) : void 0;
215
- if (!/^[A-Za-z0-9_-]+$/.test(provider) || !intentId || parts[2] !== void 0 && !sourceTxHash) {
218
+ const destTxHash = parts[3] ? decodeURIComponent(parts[3]) : void 0;
219
+ if (!/^[A-Za-z0-9_-]+$/.test(provider) || !intentId || parts[2] !== void 0 && !sourceTxHash || parts[3] !== void 0 && !destTxHash) {
216
220
  throw new Error("invalid id contents");
217
221
  }
218
- return { provider, intentId, ...sourceTxHash ? { sourceTxHash } : {} };
222
+ return { provider, intentId, ...sourceTxHash ? { sourceTxHash } : {}, ...destTxHash ? { destTxHash } : {} };
219
223
  } catch {
220
224
  throw new Swap2ValidationError("swap2: malformed swap2 status id");
221
225
  }
@@ -267,16 +271,23 @@ function toCaip2(chain, fallback) {
267
271
  }
268
272
  return swap2ChainId(chain);
269
273
  }
274
+ function nonEmptyRef(ref, side) {
275
+ if (typeof ref !== "string" || ref.trim() === "") throw new Swap2ValidationError(`swap2: ${side} token must not be empty (pass the symbol or the contract address)`);
276
+ return ref;
277
+ }
270
278
  function toSymbol(caip2, ref) {
279
+ nonEmptyRef(ref, "source");
271
280
  const byToken = ref.startsWith("0x") || ref.length > 20 ? (0, import_swap2_assets.ccAssetByToken)(caip2, ref) : void 0;
272
281
  if (byToken) return byToken.symbol;
273
282
  if ((0, import_swap2_assets.ccAsset)(caip2, ref)) return ref;
274
283
  return ref;
275
284
  }
276
- function toDestAddress(caip2, ref) {
285
+ function toDestAddress(caip2, ref, allowUnlisted = false) {
286
+ nonEmptyRef(ref, "destination");
277
287
  if (ref.startsWith("0x")) return ref;
278
288
  const asset = (0, import_swap2_assets.ccAssetByToken)(caip2, ref) ?? (0, import_swap2_assets.ccAsset)(caip2, ref);
279
289
  if (!asset) {
290
+ if (allowUnlisted && (0, import_swap2_assets.isTokenAddress)(caip2, ref)) return ref;
280
291
  throw new Error(`swap2: unknown destination token '${ref}' on ${caip2}`);
281
292
  }
282
293
  return asset.contract ?? ZERO;
@@ -358,6 +369,15 @@ function mapStatus(s) {
358
369
  const status = s.state === "completed" ? "completed" : s.state === "action-required" ? "action-required" : s.state === "refund-pending" ? "refund-pending" : s.state === "partial" ? "partial" : s.state === "failed" ? "failed" : s.state === "refunded" ? "refunded" : s.state === "cancelled" ? "cancelled" : s.state === "expired" ? "expired" : "pending";
359
370
  return s.destTxHash ? { status, transactions: [{ hash: s.destTxHash, type: "destination" }] } : { status };
360
371
  }
372
+ function blankToUndefined(v) {
373
+ return typeof v === "string" && v.trim() === "" ? void 0 : v;
374
+ }
375
+ function withoutBlankAddresses(o) {
376
+ const { recipient, refundAddress, ...rest } = o;
377
+ const r = blankToUndefined(recipient);
378
+ const f = blankToUndefined(refundAddress);
379
+ return { ...rest, ...r !== void 0 ? { recipient: r } : {}, ...f !== void 0 ? { refundAddress: f } : {} };
380
+ }
361
381
  var BaseSwap2Protocol = class extends import_core.Swap2Protocol {
362
382
  fromChain;
363
383
  constructor(account, config) {
@@ -390,19 +410,61 @@ var BaseSwap2Protocol = class extends import_core.Swap2Protocol {
390
410
  pairSupported(fromChain, toChain) {
391
411
  return this.chainSupported(fromChain) && this.chainSupported(toChain);
392
412
  }
413
+ unlistedAllowed() {
414
+ return this._config.allowUnlistedTokens === true;
415
+ }
416
+ /** The source asset the options name: the catalog's, or (allowUnlistedTokens) an unlisted one with its decimals. */
417
+ sourceAsset(options) {
418
+ const decimals = options.fromTokenDecimals;
419
+ return (0, import_swap2_assets.resolveAsset)(this.fromChain, toSymbol(this.fromChain, options.fromToken), {
420
+ allowUnlisted: this.unlistedAllowed(),
421
+ ...decimals !== void 0 ? { decimals } : {}
422
+ });
423
+ }
424
+ unlistedDecimals = /* @__PURE__ */ new Map();
425
+ /**
426
+ * Options ready to quote: with `allowUnlistedTokens`, an unlisted source token gets its decimals,
427
+ * read from the chain through the account (cached per token) — a caller's `fromTokenDecimals` that
428
+ * disagrees is refused. Listed tokens and every option otherwise pass through unchanged.
429
+ */
430
+ async prepareOptions(options) {
431
+ if (!this.unlistedAllowed()) return options;
432
+ const ref = toSymbol(this.fromChain, options.fromToken);
433
+ if ((0, import_swap2_assets.resolveAsset)(this.fromChain, ref) || !(0, import_swap2_assets.isTokenAddress)(this.fromChain, ref)) return options;
434
+ const key = ref.startsWith("0x") ? ref.toLowerCase() : ref;
435
+ let decimals = this.unlistedDecimals.get(key);
436
+ if (decimals === void 0) {
437
+ const reader = this._account;
438
+ if (typeof reader?.getTokenInfo !== "function") {
439
+ if (options.fromTokenDecimals === void 0) {
440
+ throw new Swap2ValidationError(`swap2: '${ref}' is not in the catalog \u2014 pass fromTokenDecimals, or use an account that can read token info`);
441
+ }
442
+ return options;
443
+ }
444
+ decimals = (await reader.getTokenInfo(ref)).decimals;
445
+ this.unlistedDecimals.set(key, decimals);
446
+ }
447
+ if (options.fromTokenDecimals !== void 0 && options.fromTokenDecimals !== decimals) {
448
+ throw new Swap2ValidationError(`swap2: fromTokenDecimals ${options.fromTokenDecimals} does not match the token's ${decimals}`);
449
+ }
450
+ return snapshotOptions({ ...options, fromTokenDecimals: decimals });
451
+ }
393
452
  /** Swap2Options → the engine's QuoteParams (exact-in only). */
394
- toParams(options, dry) {
453
+ toParams(given, dry) {
454
+ const options = withoutBlankAddresses(given);
395
455
  if (options.fromTokenAmount == null) {
396
456
  throw new Swap2UnsupportedOperationError("swap2: exact-out swap2 is not supported by the engine");
397
457
  }
398
458
  const destChain = toCaip2(options.toChain, this.fromChain);
399
459
  const constraints = callerConstraints(options);
400
460
  const amountIn = exactPositiveAmount(options.fromTokenAmount, "fromTokenAmount");
461
+ const source = this.sourceAsset(options);
401
462
  const params = {
402
463
  sourceChain: this.fromChain,
403
- sourceToken: toSymbol(this.fromChain, options.fromToken),
464
+ sourceToken: (0, import_swap2_assets.isUnlisted)(source) ? source.contract : toSymbol(this.fromChain, options.fromToken),
465
+ ...(0, import_swap2_assets.isUnlisted)(source) ? { sourceDecimals: source.decimals } : {},
404
466
  destChain,
405
- destToken: toDestAddress(destChain, options.toToken),
467
+ destToken: toDestAddress(destChain, options.toToken, this.unlistedAllowed()),
406
468
  amountIn: amountIn.toString(),
407
469
  destAddress: options.recipient ?? "",
408
470
  dry,
@@ -458,7 +520,7 @@ var BaseSwap2Protocol = class extends import_core.Swap2Protocol {
458
520
  }
459
521
  }
460
522
  assertQuoteFeeCaps(q, options, config) {
461
- const source = (0, import_swap2_assets.ccAsset)(this.fromChain, toSymbol(this.fromChain, options.fromToken));
523
+ const source = this.sourceAsset(options);
462
524
  if (!source) throw new Error(`swap2: unknown source token '${options.fromToken}' on ${this.fromChain}`);
463
525
  const token = source.contract ?? source.assetId;
464
526
  const fees = q.fees?.map((fee) => ({ ...fee, amount: BigInt(fee.amount) }));
@@ -503,7 +565,7 @@ var BaseSwap2Protocol = class extends import_core.Swap2Protocol {
503
565
  * gave one.
504
566
  */
505
567
  async quoteSwap2(options) {
506
- const snapshot = snapshotOptions(options);
568
+ const snapshot = await this.prepareOptions(snapshotOptions(options));
507
569
  const q = await this.resolveQuote(this.toParams(snapshot, true));
508
570
  if (!q) {
509
571
  const detail = this.noRouteDetail();
@@ -545,9 +607,9 @@ var BaseSwap2Protocol = class extends import_core.Swap2Protocol {
545
607
  this.assertFeeConfig(configSnapshot);
546
608
  await this.assertAccountChain(signer);
547
609
  const selfAddress = await signer.getAddress();
548
- const recipient = snapshot.recipient ?? selfAddress;
549
- const refundAddress = snapshot.refundAddress ?? selfAddress;
550
- const executionOptions = snapshotOptions({ ...snapshot, recipient, refundAddress });
610
+ const recipient = blankToUndefined(snapshot.recipient) ?? selfAddress;
611
+ const refundAddress = blankToUndefined(snapshot.refundAddress) ?? selfAddress;
612
+ const executionOptions = await this.prepareOptions(snapshotOptions({ ...snapshot, recipient, refundAddress }));
551
613
  const params = this.toParams(executionOptions, false);
552
614
  const resolved = await this.resolveQuote(params);
553
615
  if (!resolved) {
@@ -589,7 +651,7 @@ var BaseSwap2Protocol = class extends import_core.Swap2Protocol {
589
651
  throw new Error("swap2: provider build does not match the validated quote transaction");
590
652
  }
591
653
  } else if (!unsigned.bodyBoc && !unsigned.typedData) {
592
- if (!q.depositAddress || unsigned.to !== q.depositAddress || (unsigned.memo ?? null) !== q.depositMemo) {
654
+ if (!q.depositAddress || unsigned.to !== q.depositAddress || (unsigned.memo ?? null) !== q.depositMemo || unsigned.data !== void 0 || unsigned.approvalAddress !== void 0 || unsigned.serializedTx !== void 0) {
593
655
  throw new Error("swap2: provider build does not match the validated quote deposit");
594
656
  }
595
657
  }
@@ -628,7 +690,7 @@ var BaseSwap2Protocol = class extends import_core.Swap2Protocol {
628
690
  }
629
691
  let tokenId;
630
692
  if (!unsigned.data && !unsigned.bodyBoc && !unsigned.typedData && !serialized) {
631
- const src = (0, import_swap2_assets.ccAsset)(this.fromChain, toSymbol(this.fromChain, executionOptions.fromToken));
693
+ const src = this.sourceAsset(executionOptions);
632
694
  if (src?.contract) {
633
695
  if (this.fromChain.startsWith("eip155:")) {
634
696
  unsigned = {
@@ -654,7 +716,7 @@ var BaseSwap2Protocol = class extends import_core.Swap2Protocol {
654
716
  if (tokenId && (cellBody !== void 0 || unsigned.memo !== void 0 && !tronSource && !nearSource && !tonSource)) {
655
717
  throw new Error("swap2: token deposit with memo/cell payload is not implemented");
656
718
  }
657
- const source = (0, import_swap2_assets.ccAsset)(this.fromChain, toSymbol(this.fromChain, executionOptions.fromToken));
719
+ const source = this.sourceAsset(executionOptions);
658
720
  if (!source) throw new Error(`swap2: unknown source token '${executionOptions.fromToken}' on ${this.fromChain}`);
659
721
  const executionValue = BigInt(unsigned.value);
660
722
  if (serialized) {
@@ -944,10 +1006,10 @@ var BaseSwap2Protocol = class extends import_core.Swap2Protocol {
944
1006
  }
945
1007
  /** Status of a swap by the id `swap2()` returned, asked from the provider that executed it. */
946
1008
  async getSwap2Status(id, _options) {
947
- const { provider: name, intentId, sourceTxHash } = decodeId(id);
1009
+ const { provider: name, intentId, sourceTxHash, destTxHash } = decodeId(id);
948
1010
  const provider = this.providerFor(name);
949
1011
  if (!provider) throw new Error(`swap2: no swap2 found for id '${id}'`);
950
- return mapStatus(await provider.trackIntent(sourceTxHash == null ? { intentId } : { intentId, sourceTxHash }));
1012
+ return mapStatus(await provider.trackIntent({ intentId, ...sourceTxHash != null ? { sourceTxHash } : {}, ...destTxHash != null ? { destTxHash } : {} }));
951
1013
  }
952
1014
  /**
953
1015
  * Two-phase routes (CCTP-style burn-and-mint): once `getSwap2Status` reports
@@ -993,6 +1055,17 @@ var BaseSwap2Protocol = class extends import_core.Swap2Protocol {
993
1055
  if (unsigned.chainId !== void 0 && String(unsigned.chainId) !== expectedId) {
994
1056
  throw new Error(`swap2: claim chainId ${unsigned.chainId} does not match ${unsigned.chain}`);
995
1057
  }
1058
+ if (typeof provider.claimTargets !== "function") {
1059
+ throw new Swap2UnsupportedOperationError(`swap2: provider '${name}' declares no claimTargets \u2014 a claim may only call contracts the provider names for its chain`);
1060
+ }
1061
+ const targets = provider.claimTargets(unsigned.chain).map((a) => a.toLowerCase());
1062
+ if (!targets.includes(unsigned.to.toLowerCase())) {
1063
+ throw new Swap2ValidationError(`swap2: claim target ${unsigned.to} is not a contract '${name}' declares for ${unsigned.chain} \u2014 refused`);
1064
+ }
1065
+ const claimCall = (0, import_tx_preview_evm.decodeCall)({ to: unsigned.to, data: unsigned.data, value: 0n });
1066
+ if (claimCall.transfer || claimCall.approval) {
1067
+ throw new Swap2ValidationError(`swap2: claim calldata (${claimCall.method}) moves tokens or grants an allowance \u2014 refused`);
1068
+ }
996
1069
  const actual = await readAccountChain(signer);
997
1070
  if (actual == null) throw new Error(`swap2: destination account chain identity is unavailable for ${unsigned.chain}`);
998
1071
  const actualText = typeof actual === "string" ? actual : String(actual);
@@ -1008,7 +1081,7 @@ var BaseSwap2Protocol = class extends import_core.Swap2Protocol {
1008
1081
  });
1009
1082
  if (!sent.hash) throw new Swap2ExecutionError("swap2: account did not return a claim transaction hash");
1010
1083
  this.emitPhase("broadcast", { hash: sent.hash });
1011
- return { hash: sent.hash, chain: unsigned.chain, id };
1084
+ return { hash: sent.hash, chain: unsigned.chain, id: encodeId(name, intentId, sourceTxHash, sent.hash) };
1012
1085
  }
1013
1086
  /** The inherited bridge() has no floor field: quote first, then bind execution
1014
1087
  * to that quote's minimum so the inherited convenience API stays safe. */
@@ -1068,7 +1141,9 @@ var BaseSwap2Protocol = class extends import_core.Swap2Protocol {
1068
1141
  if ((wantFrom || wantTo) && c.caip2 !== wantFrom && c.caip2 !== wantTo) continue;
1069
1142
  for (const a of c.assets) {
1070
1143
  out.push({
1071
- token: a.assetId,
1144
+ // What quoteSwap2 takes back: the contract, or the symbol of the native coin — never the
1145
+ // provider-specific assetId, which is "" for most catalog entries (round 3, M25).
1146
+ token: a.contract ?? a.symbol,
1072
1147
  chain: c.caip2,
1073
1148
  symbol: a.symbol,
1074
1149
  decimals: a.decimals,