@anywalletkit/protocol-swap2-curve 0.1.3 → 0.1.4

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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  CurveSwap2Protocol
3
- } from "./chunk-JCWXGKPJ.js";
3
+ } from "./chunk-7AXQMAHJ.js";
4
4
  import {
5
5
  CURVE_AUTO_SLIPPAGE_DRIFT_BPS,
6
6
  CURVE_POOLS,
@@ -19,7 +19,7 @@ import {
19
19
  routeArgs,
20
20
  routePools,
21
21
  underlyingAllowed
22
- } from "./chunk-IFREGXMV.js";
22
+ } from "./chunk-NW2OLRIK.js";
23
23
  import {
24
24
  ADDRESS_PROVIDER_ABI,
25
25
  COINS_INT,
package/dist/protocol.cjs CHANGED
@@ -250,6 +250,7 @@ var CURVE_POOLS = Object.freeze({
250
250
  // src/provider.ts
251
251
  var import_viem4 = require("viem");
252
252
  var import_util5 = require("@anywalletkit/protocol-swap2-core/util");
253
+ var import_errors4 = require("@anywalletkit/protocol-swap2-core/errors");
253
254
 
254
255
  // src/abi.ts
255
256
  var import_viem = require("viem");
@@ -380,6 +381,18 @@ function describeCurveActivity(tx, ctx, deployments = CURVE_DEPLOYMENTS) {
380
381
  // src/routes.ts
381
382
  var import_viem2 = require("viem");
382
383
  var import_swap2_assets = require("@anywalletkit/swap2-assets");
384
+
385
+ // src/reasons.ts
386
+ var import_errors = require("@anywalletkit/protocol-swap2-core/errors");
387
+ var notes = (0, import_errors.refusalNotes)(256);
388
+ function noted(text, info) {
389
+ return info === void 0 ? text : notes.note(text, info);
390
+ }
391
+ function tagged(error, info) {
392
+ return info === void 0 ? error : (0, import_errors.withReason)(error, info);
393
+ }
394
+
395
+ // src/routes.ts
383
396
  var MAX_QUOTE_CANDIDATES = 30;
384
397
  var MAX_EXPLORED = 2e4;
385
398
  var lc = (a) => a.toLowerCase();
@@ -530,7 +543,7 @@ function routePools(args) {
530
543
  }
531
544
  function checkRoute(chainId, deployment, table, args, tokenIn, tokenOut) {
532
545
  const cols = deployment.family === "ng5" ? 5 : 4;
533
- if (args.route.length !== 11 || args.params.length !== 5 || args.params.some((r) => r.length !== cols)) return `the route is not address[11] with uint256[${cols}][5]`;
546
+ if (args.route.length !== 11 || args.params.length !== 5 || args.params.some((r) => r.length !== cols)) return noted(`the route is not address[11] with uint256[${cols}][5]`, "policy_refused");
534
547
  const index = poolIndex(chainId, deployment, table);
535
548
  const zero = lc(ZERO_ADDRESS);
536
549
  const eth = lc(deployment.eth);
@@ -538,14 +551,14 @@ function checkRoute(chainId, deployment, table, args, tokenIn, tokenOut) {
538
551
  const v = erc4626Of(deployment);
539
552
  let steps = 0;
540
553
  while (steps < 5 && lc(args.route[2 * steps + 1]) !== zero) steps++;
541
- if (steps === 0) return "the route has no step";
542
- if (steps > MAX_ROUTE_STEPS) return `the route has ${steps} steps, more than ${MAX_ROUTE_STEPS}`;
543
- for (let k = 2 * steps + 1; k < 11; k++) if (lc(args.route[k]) !== zero) return "the route names addresses after its end";
544
- for (let k = steps; k < 5; k++) if (args.params[k].some((x) => x !== 0n)) return "the swap params carry rows after the route's end";
545
- if (lc(args.route[0]) !== lc(tokenIn)) return `the route starts at ${args.route[0]}, not the source token`;
546
- if (lc(args.route[2 * steps]) !== lc(tokenOut)) return `the route ends at ${args.route[2 * steps]}, not the destination token`;
554
+ if (steps === 0) return noted("the route has no step", "policy_refused");
555
+ if (steps > MAX_ROUTE_STEPS) return noted(`the route has ${steps} steps, more than ${MAX_ROUTE_STEPS}`, "policy_refused");
556
+ for (let k = 2 * steps + 1; k < 11; k++) if (lc(args.route[k]) !== zero) return noted("the route names addresses after its end", "policy_refused");
557
+ for (let k = steps; k < 5; k++) if (args.params[k].some((x) => x !== 0n)) return noted("the swap params carry rows after the route's end", "policy_refused");
558
+ if (lc(args.route[0]) !== lc(tokenIn)) return noted(`the route starts at ${args.route[0]}, not the source token`, "policy_refused");
559
+ if (lc(args.route[2 * steps]) !== lc(tokenOut)) return noted(`the route ends at ${args.route[2 * steps]}, not the destination token`, "price_check_failed");
547
560
  for (let k = 1; k < steps; k++) {
548
- if (!intermediateAllowed(chainId, deployment, args.route[2 * k])) return `step ${k} hands the swap through ${args.route[2 * k]}, a token the catalog does not list on this chain`;
561
+ if (!intermediateAllowed(chainId, deployment, args.route[2 * k])) return noted(`step ${k} hands the swap through ${args.route[2 * k]}, a token the catalog does not list on this chain`, "policy_refused");
549
562
  }
550
563
  const pools = /* @__PURE__ */ new Set();
551
564
  let wraps = 0;
@@ -556,49 +569,50 @@ function checkRoute(chainId, deployment, table, args, tokenIn, tokenOut) {
556
569
  const b = lc(args.route[2 * k + 2]);
557
570
  const row = args.params[k];
558
571
  const type = row[2];
559
- if (a === b) return `step ${k + 1} trades a token for itself`;
572
+ if (a === b) return noted(`step ${k + 1} trades a token for itself`, "policy_refused");
560
573
  if (type === 1n || type === 2n) {
561
- if (CURVE_DENIED_POOLS[chainId]?.has(swap)) return `step ${k + 1} crosses ${args.route[2 * k + 1]}, a pool this module refuses (fake coin, drained in 2023, or over a dead venue)`;
574
+ if (CURVE_DENIED_POOLS[chainId]?.has(swap)) return noted(`step ${k + 1} crosses ${args.route[2 * k + 1]}, a pool this module refuses (fake coin, drained in 2023, or over a dead venue)`, "vault_mismatch");
562
575
  const pool = index.get(swap);
563
- if (!pool) return `step ${k + 1} crosses ${args.route[2 * k + 1]}, which is not in the pool allowlist`;
564
- if (pools.has(swap)) return `the route crosses ${args.route[2 * k + 1]} twice`;
576
+ if (!pool) return noted(`step ${k + 1} crosses ${args.route[2 * k + 1]}, which is not in the pool allowlist`, "vault_mismatch");
577
+ if (pools.has(swap)) return noted(`the route crosses ${args.route[2 * k + 1]} twice`, "policy_refused");
565
578
  pools.add(swap);
566
- if (!pool.swapTypes.includes(type === 1n ? 1 : 2)) return `step ${k + 1} uses swap_type ${type}, which pool ${pool.name} does not allow`;
567
- if (row[3] !== BigInt(pool.poolType)) return `step ${k + 1} names pool_type ${row[3]}, not pool ${pool.name}'s ${pool.poolType}`;
568
- if (type === 2n && deployment.family === "lite4" && pool.poolType !== 10) return `step ${k + 1}: exchange_underlying is for stable-ng metapools on this router`;
569
- if (type === 2n && !underlyingAllowed(pool, index)) return `step ${k + 1}: exchange_underlying on pool ${pool.name} needs its base pool in the allowlist`;
579
+ if (!pool.swapTypes.includes(type === 1n ? 1 : 2)) return noted(`step ${k + 1} uses swap_type ${type}, which pool ${pool.name} does not allow`, "policy_refused");
580
+ if (row[3] !== BigInt(pool.poolType)) return noted(`step ${k + 1} names pool_type ${row[3]}, not pool ${pool.name}'s ${pool.poolType}`, "policy_refused");
581
+ if (type === 2n && deployment.family === "lite4" && pool.poolType !== 10) return noted(`step ${k + 1}: exchange_underlying is for stable-ng metapools on this router`, "policy_refused");
582
+ if (type === 2n && !underlyingAllowed(pool, index)) return noted(`step ${k + 1}: exchange_underlying on pool ${pool.name} needs its base pool in the allowlist`, "policy_refused");
570
583
  const list = type === 1n ? pool.coins : pool.underlying;
571
- if (!list) return `step ${k + 1}: pool ${pool.name} lists no underlying coins`;
584
+ if (!list) return noted(`step ${k + 1}: pool ${pool.name} lists no underlying coins`, "policy_refused");
572
585
  const i = row[0], j = row[1];
573
- if (i >= BigInt(list.length) || j >= BigInt(list.length) || i === j) return `step ${k + 1}: coin indexes ${i}/${j} are not two coins of pool ${pool.name}`;
574
- if (lc(list[Number(i.toString())]) !== a || lc(list[Number(j.toString())]) !== b) return `step ${k + 1}: pool ${pool.name}'s coins ${i}/${j} are not the hop's tokens`;
575
- if (deployment.family === "ng5" && row[4] !== BigInt(list.length)) return `step ${k + 1} names ${row[4]} coins, not pool ${pool.name}'s ${list.length}`;
576
- if (deployment.family === "lite4" && (a === eth || b === eth)) return `step ${k + 1}: this router does not pass the native coin to pools`;
577
- if (STABLE_POOL_TYPES.has(pool.poolType) && (i > 127n || j > 127n)) return `step ${k + 1}: coin index out of range`;
586
+ if (i >= BigInt(list.length) || j >= BigInt(list.length) || i === j) return noted(`step ${k + 1}: coin indexes ${i}/${j} are not two coins of pool ${pool.name}`, "policy_refused");
587
+ if (lc(list[Number(i.toString())]) !== a || lc(list[Number(j.toString())]) !== b) return noted(`step ${k + 1}: pool ${pool.name}'s coins ${i}/${j} are not the hop's tokens`, "policy_refused");
588
+ if (deployment.family === "ng5" && row[4] !== BigInt(list.length)) return noted(`step ${k + 1} names ${row[4]} coins, not pool ${pool.name}'s ${list.length}`, "policy_refused");
589
+ if (deployment.family === "lite4" && (a === eth || b === eth)) return noted(`step ${k + 1}: this router does not pass the native coin to pools`, "policy_refused");
590
+ if (STABLE_POOL_TYPES.has(pool.poolType) && (i > 127n || j > 127n)) return noted(`step ${k + 1}: coin index out of range`, "policy_refused");
578
591
  } else if (type === 8n) {
579
- if (weth === zero) return "this chain's router has no wrapped native";
580
- if (swap !== weth) return `step ${k + 1} wraps through ${args.route[2 * k + 1]}, not the pinned wrapped native`;
581
- if (!(a === eth && b === weth || a === weth && b === eth)) return `step ${k + 1}: swap_type 8 is only native \u2194 the pinned wrapped native here`;
582
- if (row.some((x, c) => c !== 2 && x !== 0n)) return `step ${k + 1}: a wrap carries no other swap params`;
583
- if (++wraps > 1) return "the route wraps more than once";
592
+ if (weth === zero) return noted("this chain's router has no wrapped native", "policy_refused");
593
+ if (swap !== weth) return noted(`step ${k + 1} wraps through ${args.route[2 * k + 1]}, not the pinned wrapped native`, "vault_mismatch");
594
+ if (!(a === eth && b === weth || a === weth && b === eth)) return noted(`step ${k + 1}: swap_type 8 is only native \u2194 the pinned wrapped native here`, "policy_refused");
595
+ if (row.some((x, c) => c !== 2 && x !== 0n)) return noted(`step ${k + 1}: a wrap carries no other swap params`, "policy_refused");
596
+ if (++wraps > 1) return noted("the route wraps more than once", "policy_refused");
584
597
  } else if (type === 9n) {
585
- if (!v) return `swap_type 9 (ERC-4626) is not available on ${deployment.label}'s router ${deployment.version}`;
586
- if (swap !== lc(v.vault)) return `step ${k + 1}: ERC-4626 hop through ${args.route[2 * k + 1]}, not the pinned vault`;
598
+ if (!v) return noted(`swap_type 9 (ERC-4626) is not available on ${deployment.label}'s router ${deployment.version}`, "policy_refused");
599
+ if (swap !== lc(v.vault)) return noted(`step ${k + 1}: ERC-4626 hop through ${args.route[2 * k + 1]}, not the pinned vault`, "vault_mismatch");
587
600
  const deposit = row[0] === 0n && a === lc(v.asset) && b === lc(v.vault);
588
601
  const redeem = row[0] === 1n && a === lc(v.vault) && b === lc(v.asset);
589
- if (!deposit && !redeem) return `step ${k + 1}: the ERC-4626 hop is not a deposit of the vault's asset or a redeem of its shares`;
590
- if (row.some((x, c) => c > 0 && c !== 2 && x !== 0n)) return `step ${k + 1}: an ERC-4626 hop carries no other swap params`;
591
- if (++vaults > 1) return "the route crosses the vault more than once";
602
+ if (!deposit && !redeem) return noted(`step ${k + 1}: the ERC-4626 hop is not a deposit of the vault's asset or a redeem of its shares`, "policy_refused");
603
+ if (row.some((x, c) => c > 0 && c !== 2 && x !== 0n)) return noted(`step ${k + 1}: an ERC-4626 hop carries no other swap params`, "policy_refused");
604
+ if (++vaults > 1) return noted("the route crosses the vault more than once", "policy_refused");
592
605
  } else {
593
- return `step ${k + 1} uses swap_type ${type}, which this module never builds`;
606
+ return noted(`step ${k + 1} uses swap_type ${type}, which this module never builds`, "policy_refused");
594
607
  }
595
608
  }
596
- if (pools.size > MAX_ROUTE_POOLS) return `the route crosses ${pools.size} pools, more than ${MAX_ROUTE_POOLS}`;
597
- if (pools.size === 0 && vaults === 0) return "the route crosses no pool";
609
+ if (pools.size > MAX_ROUTE_POOLS) return noted(`the route crosses ${pools.size} pools, more than ${MAX_ROUTE_POOLS}`, "policy_refused");
610
+ if (pools.size === 0 && vaults === 0) return noted("the route crosses no pool", "policy_refused");
598
611
  return null;
599
612
  }
600
613
 
601
614
  // src/rpc.ts
615
+ var import_errors2 = require("@anywalletkit/protocol-swap2-core/errors");
602
616
  var import_util2 = require("@anywalletkit/protocol-swap2-core/util");
603
617
  var RPC_BATCH_MAX = 10;
604
618
  var RPC_RATE_LIMIT_PAUSE_MS = 1e3;
@@ -624,18 +638,18 @@ function rpcClient(rpcUrls) {
624
638
  body: JSON.stringify(calls.map((c, id) => ({ jsonrpc: "2.0", id, ...c }))),
625
639
  signal: signal ?? (0, import_util2.timeoutSignal)(12e3)
626
640
  });
627
- if (!res.ok) throw Object.assign(new Error(`HTTP ${res.status}`), { limited: res.status === 429 });
641
+ if (!res.ok) throw (0, import_errors2.withReason)(Object.assign(new Error(`HTTP ${res.status}`), { limited: res.status === 429 }), (0, import_errors2.httpRefusal)(res.status, res.headers?.get?.("retry-after")) ?? { reason: "upstream_unavailable", httpStatus: res.status });
628
642
  const body = await res.json();
629
- if (!Array.isArray(body)) throw Object.assign(new Error(body.error?.message ?? "batch refused"), { limited: isRateLimit(body.error) });
643
+ if (!Array.isArray(body)) throw (0, import_errors2.withReason)(Object.assign(new Error(body.error?.message ?? "batch refused"), { limited: isRateLimit(body.error) }), isRateLimit(body.error) ? "rate_limited" : "upstream_unavailable");
630
644
  const out = calls.map(() => null);
631
645
  const answered = /* @__PURE__ */ new Set();
632
646
  for (const r of body) {
633
647
  if (typeof r.id !== "number" || r.id < 0 || r.id >= out.length) continue;
634
- if (r.error !== void 0 && r.error !== null && !isExecutionError(r.error)) throw Object.assign(new Error(errorText(r.error)), { limited: isRateLimit(r.error) });
648
+ if (r.error !== void 0 && r.error !== null && !isExecutionError(r.error)) throw (0, import_errors2.withReason)(Object.assign(new Error(errorText(r.error)), { limited: isRateLimit(r.error) }), isRateLimit(r.error) ? "rate_limited" : "upstream_unavailable");
635
649
  answered.add(r.id);
636
650
  if (r.error === void 0 || r.error === null) out[r.id] = r.result ?? null;
637
651
  }
638
- if (answered.size !== calls.length) throw new Error(`answered ${answered.size} of ${calls.length} calls`);
652
+ if (answered.size !== calls.length) throw (0, import_errors2.refusal)("upstream_unavailable", `answered ${answered.size} of ${calls.length} calls`);
639
653
  return out;
640
654
  }
641
655
  async function batch(chainId, calls, signal) {
@@ -653,7 +667,9 @@ function rpcClient(rpcUrls) {
653
667
  }
654
668
  }
655
669
  }
656
- throw new Error(`curve: no RPC answered on chain ${chainId} (${lastErr instanceof Error ? lastErr.message : String(lastErr)})`);
670
+ const error = new Error(`curve: no RPC answered on chain ${chainId} (${lastErr instanceof Error ? lastErr.message : String(lastErr)})`);
671
+ if (lastErr === void 0 || signal?.aborted) throw error;
672
+ throw (0, import_errors2.withReason)(error, (0, import_errors2.refusalOf)(lastErr) ?? "upstream_unavailable");
657
673
  }
658
674
  return async function rpc(chainId, calls, signal) {
659
675
  if (calls.length <= RPC_BATCH_MAX) return batch(chainId, calls, signal);
@@ -713,6 +729,7 @@ function curveIntentChain(intentId) {
713
729
  var import_viem3 = require("viem");
714
730
  var import_util4 = require("@anywalletkit/protocol-swap2-core/util");
715
731
  var import_validate = require("@anywalletkit/protocol-swap2-core/validate");
732
+ var import_errors3 = require("@anywalletkit/protocol-swap2-core/errors");
716
733
  var lc2 = (a) => a.toLowerCase();
717
734
  var isAddress = (a) => /^0x[0-9a-fA-F]{40}$/.test(a);
718
735
  var own = (o, k) => Object.prototype.hasOwnProperty.call(o, k) ? o[k] : void 0;
@@ -722,19 +739,19 @@ function curveQuoteCheck(tables = {}) {
722
739
  return function verify(params, quote) {
723
740
  const chainId = chainIdOf(params.sourceChain);
724
741
  const d2 = deploymentOf(deployments, chainId);
725
- if (!d2 || params.destChain !== params.sourceChain) return "not a same-chain swap on a pinned Curve router";
742
+ if (!d2 || params.destChain !== params.sourceChain) return noted("not a same-chain swap on a pinned Curve router", "unsupported_pair");
726
743
  const tx = quote.txRequest;
727
- if (!tx?.data || !tx.to || tx.serializedTx !== void 0) return "the quote carries no router call";
728
- if (!isAddress(tx.to) || lc2(tx.to) !== lc2(d2.router)) return `the call targets ${tx.to}, not the Curve router`;
729
- if (tx.chainId !== void 0 && tx.chainId !== chainId) return `the call is for chain ${tx.chainId}`;
730
- if (curveIntentChain(quote.intentId) !== chainId) return `the quote names ${String(quote.intentId).slice(0, 40)}, not a curve:${chainId} swap`;
744
+ if (!tx?.data || !tx.to || tx.serializedTx !== void 0) return noted("the quote carries no router call", "policy_refused");
745
+ if (!isAddress(tx.to) || lc2(tx.to) !== lc2(d2.router)) return noted(`the call targets ${tx.to}, not the Curve router`, "vault_mismatch");
746
+ if (tx.chainId !== void 0 && tx.chainId !== chainId) return noted(`the call is for chain ${tx.chainId}`, "policy_refused");
747
+ if (curveIntentChain(quote.intentId) !== chainId) return noted(`the quote names ${String(quote.intentId).slice(0, 40)}, not a curve:${chainId} swap`, "policy_refused");
731
748
  const src = (0, import_util4.sourceAssetOf)(params);
732
- if (!src) return `unknown source token ${params.sourceToken}`;
749
+ if (!src) return noted(`unknown source token ${params.sourceToken}`, "unsupported_pair");
733
750
  const nativeIn = !src.contract;
734
751
  const nativeOut = /^0x0+$/.test(params.destToken);
735
- if (!nativeOut && !isAddress(params.destToken)) return "destination token must be an EVM address";
736
- if (!isAddress(params.destAddress)) return "recipient must be an EVM address";
737
- if (!nativeIn && lc2(src.contract) === lc2(d2.eth) || !nativeOut && lc2(params.destToken) === lc2(d2.eth)) return "the router's native sentinel is not a token";
752
+ if (!nativeOut && !isAddress(params.destToken)) return noted("destination token must be an EVM address", "unsupported_pair");
753
+ if (!isAddress(params.destAddress)) return noted("recipient must be an EVM address", "recipient_mismatch");
754
+ if (!nativeIn && lc2(src.contract) === lc2(d2.eth) || !nativeOut && lc2(params.destToken) === lc2(d2.eth)) return noted("the router's native sentinel is not a token", "unsupported_pair");
738
755
  const tokenIn = nativeIn ? d2.eth : src.contract;
739
756
  const tokenOut = nativeOut ? d2.eth : params.destToken;
740
757
  let amountIn, minOut, value;
@@ -743,20 +760,20 @@ function curveQuoteCheck(tables = {}) {
743
760
  minOut = BigInt(quote.minDestAmount);
744
761
  value = BigInt(tx.value ?? "0");
745
762
  } catch {
746
- return "the request does not name amounts";
763
+ return noted("the request does not name amounts", "policy_refused");
747
764
  }
748
765
  if (nativeIn ? value !== amountIn || tx.approvalAddress !== void 0 : value !== 0n || !tx.approvalAddress || !isAddress(tx.approvalAddress) || lc2(tx.approvalAddress) !== lc2(d2.router)) {
749
- return nativeIn ? "a native input is paid as exactly the input in value, with no approval" : "a token input attaches no value and approves exactly the Curve router";
766
+ return noted(nativeIn ? "a native input is paid as exactly the input in value, with no approval" : "a token input attaches no value and approves exactly the Curve router", "policy_refused");
750
767
  }
751
768
  const call = decodeExchange(d2.family, tx.data);
752
- if (!call) return "the router call is not exchange(route, params, amount, min, receiver)";
753
- if (call.selector !== EXCHANGE_SELECTOR[d2.family]) return `selector ${call.selector} is not the explicit-receiver exchange ${EXCHANGE_SELECTOR[d2.family]}`;
754
- if (!call.receiver || lc2(call.receiver) !== lc2(params.destAddress)) return `the swap pays ${call.receiver ?? "the sender"}, not the recipient`;
755
- if (call.pools && call.pools.some((p) => lc2(p) !== lc2(ZERO_ADDRESS))) return "the call names zap pools this module never uses";
756
- if (call.amount !== amountIn) return `the swap sells ${call.amount}, not the ${amountIn} input`;
757
- if (call.minDy <= 0n) return "the swap has no minimum (min_dy 0)";
758
- if (call.minDy !== minOut) return `the swap's minimum ${call.minDy} is not the quote's ${minOut}`;
759
- if (quote.providerData?.minOut !== void 0 && quote.providerData.minOut !== minOut.toString()) return "the quote's facts disagree on the minimum";
769
+ if (!call) return noted("the router call is not exchange(route, params, amount, min, receiver)", "policy_refused");
770
+ if (call.selector !== EXCHANGE_SELECTOR[d2.family]) return noted(`selector ${call.selector} is not the explicit-receiver exchange ${EXCHANGE_SELECTOR[d2.family]}`, "policy_refused");
771
+ if (!call.receiver || lc2(call.receiver) !== lc2(params.destAddress)) return noted(`the swap pays ${call.receiver ?? "the sender"}, not the recipient`, "recipient_mismatch");
772
+ if (call.pools && call.pools.some((p) => lc2(p) !== lc2(ZERO_ADDRESS))) return noted("the call names zap pools this module never uses", "policy_refused");
773
+ if (call.amount !== amountIn) return noted(`the swap sells ${call.amount}, not the ${amountIn} input`, "policy_refused");
774
+ if (call.minDy <= 0n) return noted("the swap has no minimum (min_dy 0)", "price_check_failed");
775
+ if (call.minDy !== minOut) return noted(`the swap's minimum ${call.minDy} is not the quote's ${minOut}`, "price_check_failed");
776
+ if (quote.providerData?.minOut !== void 0 && quote.providerData.minOut !== minOut.toString()) return noted("the quote's facts disagree on the minimum", "price_check_failed");
760
777
  return checkRoute(chainId, d2, own(pools, chainId), call, tokenIn, tokenOut);
761
778
  };
762
779
  }
@@ -764,7 +781,9 @@ var curveVerifyQuote = curveQuoteCheck();
764
781
  var curvePins = Object.freeze({
765
782
  callTargets: (chain) => routerFor(CURVE_DEPLOYMENTS, chain),
766
783
  spenders: (chain) => routerFor(CURVE_DEPLOYMENTS, chain),
767
- verifyQuote: curveVerifyQuote
784
+ verifyQuote: curveVerifyQuote,
785
+ // The reason of each text verifyQuote returned (remoteProvider puts it on its refusal).
786
+ reasonOf: notes.reasonOf
768
787
  });
769
788
  function routerFor(deployments, chain) {
770
789
  const d2 = deploymentOf(deployments, chainIdOf(chain));
@@ -781,21 +800,21 @@ function curveOnChain(opts) {
781
800
  if (!p) {
782
801
  const d2 = deploymentOf(deployments, chainId);
783
802
  p = (async () => {
784
- if (!d2) throw new Error(`curve: no pinned router on chain ${chainId}`);
803
+ if (!d2) throw (0, import_errors3.refusal)("unsupported_pair", `curve: no pinned router on chain ${chainId}`);
785
804
  const [code, version, listed] = await rpc(chainId, [
786
805
  { method: "eth_getCode", params: [d2.router, "latest"] },
787
806
  ethCall(d2.router, (0, import_viem3.encodeFunctionData)({ abi: VERSION_ABI, functionName: "version" })),
788
807
  ethCall(d2.addressProvider, (0, import_viem3.encodeFunctionData)({ abi: ADDRESS_PROVIDER_ABI, functionName: "get_address", args: [2n] }))
789
808
  ]);
790
809
  if (typeof code !== "string" || code.length <= 2 || lc2((0, import_viem3.keccak256)(code)) !== lc2(d2.codeHash)) {
791
- throw new Error(`curve: the router on chain ${chainId} is not the pinned code (code hash mismatch) \u2014 refusing`);
810
+ throw (0, import_errors3.refusal)("vault_mismatch", `curve: the router on chain ${chainId} is not the pinned code (code hash mismatch) \u2014 refusing`);
792
811
  }
793
812
  let v = "";
794
813
  try {
795
814
  v = typeof version === "string" ? (0, import_viem3.decodeFunctionResult)({ abi: VERSION_ABI, functionName: "version", data: version }) : "";
796
815
  } catch {
797
816
  }
798
- if (v !== d2.version) throw new Error(`curve: the router on chain ${chainId} reports version ${v || "none"}, not ${d2.version} \u2014 refusing`);
817
+ if (v !== d2.version) throw (0, import_errors3.refusal)("vault_mismatch", `curve: the router on chain ${chainId} reports version ${v || "none"}, not ${d2.version} \u2014 refusing`);
799
818
  const ap = wordAddress(listed);
800
819
  if (ap && ap !== lc2(d2.addressProviderRouter) && ap !== lc2(d2.router)) {
801
820
  warn(`curve: the AddressProvider on ${d2.label} lists router ${ap} (id 2); this module keeps the pinned ${d2.router}`);
@@ -813,17 +832,17 @@ function curveOnChain(opts) {
813
832
  async function inspectSealed(quote, tx, probe) {
814
833
  const chainId = curveIntentChain(quote.intentId);
815
834
  const d2 = chainId === null ? void 0 : deploymentOf(deployments, chainId);
816
- if (chainId === null || !d2) return "the quote is not a Curve swap on a pinned chain";
835
+ if (chainId === null || !d2) return noted("the quote is not a Curve swap on a pinned chain", "policy_refused");
817
836
  const quoted = quote.txRequest;
818
- if (!quoted?.data || !quoted.to || !tx.data) return "the quote or the transaction carries no router call";
819
- if (tx.data.length !== quoted.data.length || lc2(tx.data) !== lc2(quoted.data)) return "the transaction is not the quoted router call";
820
- if (!isAddress(tx.to) || lc2(tx.to) !== lc2(d2.router) || lc2(quoted.to) !== lc2(d2.router)) return `the transaction calls ${tx.to}, not the Curve router`;
821
- if ((tx.value ?? "0") !== (quoted.value ?? "0")) return "the transaction's value is not the quote's";
822
- if (tx.chainId !== void 0 && tx.chainId !== chainId || quoted.chainId !== void 0 && quoted.chainId !== chainId) return `the transaction is not for chain ${chainId}`;
823
- if ((tx.approvalAddress ?? "").toLowerCase() !== (quoted.approvalAddress ?? "").toLowerCase()) return "the transaction's approval is not the quote's";
837
+ if (!quoted?.data || !quoted.to || !tx.data) return noted("the quote or the transaction carries no router call", "policy_refused");
838
+ if (tx.data.length !== quoted.data.length || lc2(tx.data) !== lc2(quoted.data)) return noted("the transaction is not the quoted router call", "policy_refused");
839
+ if (!isAddress(tx.to) || lc2(tx.to) !== lc2(d2.router) || lc2(quoted.to) !== lc2(d2.router)) return noted(`the transaction calls ${tx.to}, not the Curve router`, "vault_mismatch");
840
+ if ((tx.value ?? "0") !== (quoted.value ?? "0")) return noted("the transaction's value is not the quote's", "policy_refused");
841
+ if (tx.chainId !== void 0 && tx.chainId !== chainId || quoted.chainId !== void 0 && quoted.chainId !== chainId) return noted(`the transaction is not for chain ${chainId}`, "policy_refused");
842
+ if ((tx.approvalAddress ?? "").toLowerCase() !== (quoted.approvalAddress ?? "").toLowerCase()) return noted("the transaction's approval is not the quote's", "vault_mismatch");
824
843
  const call = decodeExchange(d2.family, quoted.data);
825
- if (!call || call.selector !== EXCHANGE_SELECTOR[d2.family]) return "the quoted call is not the explicit-receiver exchange";
826
- if (call.minDy <= 0n) return "the swap has no minimum (min_dy 0)";
844
+ if (!call || call.selector !== EXCHANGE_SELECTOR[d2.family]) return noted("the quoted call is not the explicit-receiver exchange", "policy_refused");
845
+ if (call.minDy <= 0n) return noted("the swap has no minimum (min_dy 0)", "price_check_failed");
827
846
  const last = lastToken(call);
828
847
  const reason = checkRoute(chainId, d2, own(pools, chainId), call, call.route[0], last);
829
848
  if (reason) return reason;
@@ -839,8 +858,8 @@ function curveOnChain(opts) {
839
858
  const poolReason = await poolsReason(chainId, hops, res.slice(1, 1 + reads.length));
840
859
  if (poolReason) return poolReason;
841
860
  const out = wordUint(res[0]);
842
- if (out === null) return "the router no longer quotes this route";
843
- if (out < call.minDy) return `price moved \u2014 the pools now pay ${out}, under your minimum ${call.minDy}`;
861
+ if (out === null) return noted("the router no longer quotes this route", "simulation_failed");
862
+ if (out < call.minDy) return noted(`price moved \u2014 the pools now pay ${out}, under your minimum ${call.minDy}`, "price_check_failed");
844
863
  return { out, probeOut: probe && probeIn > 0n ? wordUint(res[probeAt]) : null, amount: call.amount, minDy: call.minDy };
845
864
  }
846
865
  function poolCalls(list) {
@@ -854,19 +873,19 @@ function curveOnChain(opts) {
854
873
  const retry = [];
855
874
  for (const pool of list) {
856
875
  const code = res[at++];
857
- if (typeof code !== "string") return `could not read pool ${pool.name} (${pool.address}) over RPC`;
858
- if (code.length <= 2 || lc2((0, import_viem3.keccak256)(code)) !== lc2(pool.codeHash)) return `pool ${pool.name} (${pool.address}) is not the pinned code`;
876
+ if (typeof code !== "string") return noted(`could not read pool ${pool.name} (${pool.address}) over RPC`, "upstream_unavailable");
877
+ if (code.length <= 2 || lc2((0, import_viem3.keccak256)(code)) !== lc2(pool.codeHash)) return noted(`pool ${pool.name} (${pool.address}) is not the pinned code`, "vault_mismatch");
859
878
  for (let k = 0; k < pool.coins.length; k++) {
860
879
  const coin = wordAddress(res[at++]);
861
880
  if (coin === null) retry.push({ pool, k });
862
- else if (coin !== lc2(pool.coins[k])) return `pool ${pool.name} now reports coin ${k} = ${coin}, not the pinned ${pool.coins[k]}`;
881
+ else if (coin !== lc2(pool.coins[k])) return noted(`pool ${pool.name} now reports coin ${k} = ${coin}, not the pinned ${pool.coins[k]}`, "vault_mismatch");
863
882
  }
864
883
  }
865
884
  if (retry.length) {
866
885
  const again = await rpc(chainId, retry.map(({ pool, k }) => ethCall(pool.address, `${COINS_INT}${k.toString(16).padStart(64, "0")}`)));
867
886
  for (const [n, { pool, k }] of retry.entries()) {
868
887
  const coin = wordAddress(again[n]);
869
- if (coin !== lc2(pool.coins[k])) return `pool ${pool.name} does not report its pinned coin ${k}`;
888
+ if (coin !== lc2(pool.coins[k])) return noted(`pool ${pool.name} does not report its pinned coin ${k}`, "vault_mismatch");
870
889
  }
871
890
  }
872
891
  return null;
@@ -914,46 +933,47 @@ function curveProvider(config) {
914
933
  return from.chain === to.chain && chainIdOf2(from.chain) !== null;
915
934
  },
916
935
  async getQuote(params, signal) {
917
- const refuse = (why) => {
918
- params.onRefusal?.(`curve: ${why}`);
936
+ const refuse = (why, info) => {
937
+ if (info === void 0) params.onRefusal?.(`curve: ${why}`);
938
+ else params.onRefusal?.(`curve: ${why}`, typeof info === "string" ? { reason: info } : info);
919
939
  return null;
920
940
  };
921
- if (params.flex) return refuse("amount-flexible deposits are not supported");
922
- if (params.sourceChain !== params.destChain) return refuse("same-chain swaps only");
941
+ if (params.flex) return refuse("amount-flexible deposits are not supported", "unsupported_pair");
942
+ if (params.sourceChain !== params.destChain) return refuse("same-chain swaps only", "unsupported_pair");
923
943
  const chainId = chainIdOf2(params.sourceChain);
924
- if (chainId === null) return refuse(`no Curve router, pool allowlist or RPC for ${params.sourceChain}`);
944
+ if (chainId === null) return refuse(`no Curve router, pool allowlist or RPC for ${params.sourceChain}`, "unsupported_pair");
925
945
  const d2 = deploymentOf(deployments, chainId);
926
946
  const src = (0, import_util5.sourceAssetOf)(params);
927
- if (!src) return refuse(`unknown source token ${params.sourceToken}`);
947
+ if (!src) return refuse(`unknown source token ${params.sourceToken}`, "unsupported_pair");
928
948
  const nativeIn = !src.contract;
929
949
  const nativeOut = /^0x0+$/.test(params.destToken);
930
- if (!nativeOut && !/^0x[0-9a-fA-F]{40}$/.test(params.destToken)) return refuse("destination token must be an EVM address");
931
- if (!/^0x[0-9a-fA-F]{40}$/.test(params.destAddress)) return refuse("recipient must be an EVM address");
950
+ if (!nativeOut && !/^0x[0-9a-fA-F]{40}$/.test(params.destToken)) return refuse("destination token must be an EVM address", "unsupported_pair");
951
+ if (!/^0x[0-9a-fA-F]{40}$/.test(params.destAddress)) return refuse("recipient must be an EVM address", "recipient_mismatch");
932
952
  let amountIn;
933
953
  try {
934
954
  amountIn = BigInt(params.amountIn);
935
955
  } catch {
936
956
  return refuse("amount is not an integer");
937
957
  }
938
- if (amountIn <= 0n) return refuse("amount must be positive");
958
+ if (amountIn <= 0n) return refuse("amount must be positive", "amount_too_small");
939
959
  if (params.slippageBps !== void 0 && (!Number.isInteger(params.slippageBps) || params.slippageBps < 0 || params.slippageBps >= 1e4)) return refuse("slippage must be 0..9999 bps");
940
- if (!nativeIn && src.contract.toLowerCase() === d2.eth.toLowerCase()) return refuse("the native coin is named by the catalog, not by the router's 0xEeee sentinel");
941
- if (!nativeOut && params.destToken.toLowerCase() === d2.eth.toLowerCase()) return refuse("name a native output with the zero address, not the router's 0xEeee sentinel");
960
+ if (!nativeIn && src.contract.toLowerCase() === d2.eth.toLowerCase()) return refuse("the native coin is named by the catalog, not by the router's 0xEeee sentinel", "unsupported_pair");
961
+ if (!nativeOut && params.destToken.toLowerCase() === d2.eth.toLowerCase()) return refuse("name a native output with the zero address, not the router's 0xEeee sentinel", "unsupported_pair");
942
962
  const tokenIn = (0, import_viem4.getAddress)(nativeIn ? d2.eth : src.contract);
943
963
  const tokenOut = (0, import_viem4.getAddress)(nativeOut ? d2.eth : params.destToken);
944
- if (tokenIn === tokenOut) return refuse("same token in and out");
964
+ if (tokenIn === tokenOut) return refuse("same token in and out", "unsupported_pair");
945
965
  const wrapPair = [(0, import_viem4.getAddress)(d2.eth), (0, import_viem4.getAddress)(d2.weth)];
946
- if (wrapPair.includes(tokenIn) && wrapPair.includes(tokenOut)) return refuse("that is a wrap/unwrap, not a swap");
966
+ if (wrapPair.includes(tokenIn) && wrapPair.includes(tokenOut)) return refuse("that is a wrap/unwrap, not a swap", "unsupported_pair");
947
967
  try {
948
968
  await chain.verifyDeployment(chainId);
949
969
  const { routes, truncated } = findRoutes(chainId, d2, tableOf(chainId), tokenIn, tokenOut);
950
- if (!routes.length) return refuse("no allowlisted Curve pool route for this pair");
970
+ if (!routes.length) return refuse("no allowlisted Curve pool route for this pair", "no_route");
951
971
  const outs = await price(chainId, d2, routes, amountIn, signal);
952
972
  let best = null;
953
973
  outs.forEach((out2, i) => {
954
974
  if (out2 !== null && out2 > 0n && (!best || out2 > best.out)) best = { route: routes[i], out: out2 };
955
975
  });
956
- if (!best) return refuse("no allowlisted Curve pool quotes this pair and amount");
976
+ if (!best) return refuse("no allowlisted Curve pool quotes this pair and amount", "no_route");
957
977
  const { route, out } = best;
958
978
  let priceImpactBps;
959
979
  const probeIn = amountIn / 1000n;
@@ -966,9 +986,9 @@ function curveProvider(config) {
966
986
  }
967
987
  const bps = params.slippageBps ?? fixedSlippage ?? curveAutoSlippageBps(params, priceImpactBps);
968
988
  const minOut = out * BigInt(1e4 - bps) / 10000n;
969
- if (minOut <= 0n) return refuse("the minimum rounds to zero for this amount");
989
+ if (minOut <= 0n) return refuse("the minimum rounds to zero for this amount", "amount_too_small");
970
990
  if (params.minAmountOut !== void 0 && minOut < BigInt(params.minAmountOut)) {
971
- return refuse(`floor breach (min=${minOut} < floor=${params.minAmountOut})`);
991
+ return refuse(`floor breach (min=${minOut} < floor=${params.minAmountOut})`, "price_check_failed");
972
992
  }
973
993
  const args = routeArgs(d2.family, route);
974
994
  const recipient = (0, import_viem4.getAddress)(params.destAddress);
@@ -1010,10 +1030,10 @@ function curveProvider(config) {
1010
1030
  }
1011
1031
  };
1012
1032
  const why = selfCheck(params, quote);
1013
- if (why) return refuse(`built a call its own check refuses (${why})`);
1033
+ if (why) return refuse(`built a call its own check refuses (${why})`, notes.reasonOf(why) ?? "policy_refused");
1014
1034
  return quote;
1015
1035
  } catch (e) {
1016
- return refuse(e instanceof Error ? e.message.replace(/^curve: /, "") : String(e));
1036
+ return refuse(e instanceof Error ? e.message.replace(/^curve: /, "") : String(e), (0, import_errors4.refusalOf)(e));
1017
1037
  }
1018
1038
  },
1019
1039
  /**
@@ -1023,11 +1043,11 @@ function curveProvider(config) {
1023
1043
  */
1024
1044
  async preflightDeposit(quote, tx) {
1025
1045
  const reason = await chain.checkSealed(quote, tx);
1026
- if (reason) throw new Error(`curve: ${reason}. Nothing was sent; refresh the quote.`);
1046
+ if (reason) throw tagged(new Error(`curve: ${reason}. Nothing was sent; refresh the quote.`), notes.reasonOf(reason));
1027
1047
  },
1028
1048
  async buildDeposit(quote) {
1029
1049
  const tx = quote.txRequest;
1030
- if (!tx?.to || !tx.data) throw new Error("swap2: Curve quote has no transaction request");
1050
+ if (!tx?.to || !tx.data) throw (0, import_errors4.refusal)("policy_refused", "swap2: Curve quote has no transaction request");
1031
1051
  return {
1032
1052
  chain: "",
1033
1053
  to: tx.to,