@anywalletkit/protocol-swap2-curve 0.1.2 → 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.cjs CHANGED
@@ -222,6 +222,18 @@ function decodeExchange(family, data) {
222
222
  // src/routes.ts
223
223
  var import_viem2 = require("viem");
224
224
  var import_swap2_assets = require("@anywalletkit/swap2-assets");
225
+
226
+ // src/reasons.ts
227
+ var import_errors = require("@anywalletkit/protocol-swap2-core/errors");
228
+ var notes = (0, import_errors.refusalNotes)(256);
229
+ function noted(text, info) {
230
+ return info === void 0 ? text : notes.note(text, info);
231
+ }
232
+ function tagged(error, info) {
233
+ return info === void 0 ? error : (0, import_errors.withReason)(error, info);
234
+ }
235
+
236
+ // src/routes.ts
225
237
  var MAX_QUOTE_CANDIDATES = 30;
226
238
  var MAX_EXPLORED = 2e4;
227
239
  var lc = (a) => a.toLowerCase();
@@ -372,7 +384,7 @@ function routePools(args) {
372
384
  }
373
385
  function checkRoute(chainId, deployment, table, args, tokenIn, tokenOut) {
374
386
  const cols = deployment.family === "ng5" ? 5 : 4;
375
- 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]`;
387
+ 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");
376
388
  const index = poolIndex(chainId, deployment, table);
377
389
  const zero = lc(ZERO_ADDRESS);
378
390
  const eth = lc(deployment.eth);
@@ -380,14 +392,14 @@ function checkRoute(chainId, deployment, table, args, tokenIn, tokenOut) {
380
392
  const v = erc4626Of(deployment);
381
393
  let steps = 0;
382
394
  while (steps < 5 && lc(args.route[2 * steps + 1]) !== zero) steps++;
383
- if (steps === 0) return "the route has no step";
384
- if (steps > MAX_ROUTE_STEPS) return `the route has ${steps} steps, more than ${MAX_ROUTE_STEPS}`;
385
- for (let k = 2 * steps + 1; k < 11; k++) if (lc(args.route[k]) !== zero) return "the route names addresses after its end";
386
- 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";
387
- if (lc(args.route[0]) !== lc(tokenIn)) return `the route starts at ${args.route[0]}, not the source token`;
388
- if (lc(args.route[2 * steps]) !== lc(tokenOut)) return `the route ends at ${args.route[2 * steps]}, not the destination token`;
395
+ if (steps === 0) return noted("the route has no step", "policy_refused");
396
+ if (steps > MAX_ROUTE_STEPS) return noted(`the route has ${steps} steps, more than ${MAX_ROUTE_STEPS}`, "policy_refused");
397
+ 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");
398
+ 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");
399
+ if (lc(args.route[0]) !== lc(tokenIn)) return noted(`the route starts at ${args.route[0]}, not the source token`, "policy_refused");
400
+ 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");
389
401
  for (let k = 1; k < steps; k++) {
390
- 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`;
402
+ 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");
391
403
  }
392
404
  const pools = /* @__PURE__ */ new Set();
393
405
  let wraps = 0;
@@ -398,51 +410,52 @@ function checkRoute(chainId, deployment, table, args, tokenIn, tokenOut) {
398
410
  const b = lc(args.route[2 * k + 2]);
399
411
  const row = args.params[k];
400
412
  const type = row[2];
401
- if (a === b) return `step ${k + 1} trades a token for itself`;
413
+ if (a === b) return noted(`step ${k + 1} trades a token for itself`, "policy_refused");
402
414
  if (type === 1n || type === 2n) {
403
- 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)`;
415
+ 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");
404
416
  const pool = index.get(swap);
405
- if (!pool) return `step ${k + 1} crosses ${args.route[2 * k + 1]}, which is not in the pool allowlist`;
406
- if (pools.has(swap)) return `the route crosses ${args.route[2 * k + 1]} twice`;
417
+ if (!pool) return noted(`step ${k + 1} crosses ${args.route[2 * k + 1]}, which is not in the pool allowlist`, "vault_mismatch");
418
+ if (pools.has(swap)) return noted(`the route crosses ${args.route[2 * k + 1]} twice`, "policy_refused");
407
419
  pools.add(swap);
408
- if (!pool.swapTypes.includes(type === 1n ? 1 : 2)) return `step ${k + 1} uses swap_type ${type}, which pool ${pool.name} does not allow`;
409
- if (row[3] !== BigInt(pool.poolType)) return `step ${k + 1} names pool_type ${row[3]}, not pool ${pool.name}'s ${pool.poolType}`;
410
- if (type === 2n && deployment.family === "lite4" && pool.poolType !== 10) return `step ${k + 1}: exchange_underlying is for stable-ng metapools on this router`;
411
- if (type === 2n && !underlyingAllowed(pool, index)) return `step ${k + 1}: exchange_underlying on pool ${pool.name} needs its base pool in the allowlist`;
420
+ 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");
421
+ 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");
422
+ 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");
423
+ 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");
412
424
  const list = type === 1n ? pool.coins : pool.underlying;
413
- if (!list) return `step ${k + 1}: pool ${pool.name} lists no underlying coins`;
425
+ if (!list) return noted(`step ${k + 1}: pool ${pool.name} lists no underlying coins`, "policy_refused");
414
426
  const i = row[0], j = row[1];
415
- 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}`;
416
- 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`;
417
- if (deployment.family === "ng5" && row[4] !== BigInt(list.length)) return `step ${k + 1} names ${row[4]} coins, not pool ${pool.name}'s ${list.length}`;
418
- if (deployment.family === "lite4" && (a === eth || b === eth)) return `step ${k + 1}: this router does not pass the native coin to pools`;
419
- if (STABLE_POOL_TYPES.has(pool.poolType) && (i > 127n || j > 127n)) return `step ${k + 1}: coin index out of range`;
427
+ 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");
428
+ 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");
429
+ 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");
430
+ 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");
431
+ if (STABLE_POOL_TYPES.has(pool.poolType) && (i > 127n || j > 127n)) return noted(`step ${k + 1}: coin index out of range`, "policy_refused");
420
432
  } else if (type === 8n) {
421
- if (weth === zero) return "this chain's router has no wrapped native";
422
- if (swap !== weth) return `step ${k + 1} wraps through ${args.route[2 * k + 1]}, not the pinned wrapped native`;
423
- 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`;
424
- if (row.some((x, c) => c !== 2 && x !== 0n)) return `step ${k + 1}: a wrap carries no other swap params`;
425
- if (++wraps > 1) return "the route wraps more than once";
433
+ if (weth === zero) return noted("this chain's router has no wrapped native", "policy_refused");
434
+ if (swap !== weth) return noted(`step ${k + 1} wraps through ${args.route[2 * k + 1]}, not the pinned wrapped native`, "vault_mismatch");
435
+ 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");
436
+ if (row.some((x, c) => c !== 2 && x !== 0n)) return noted(`step ${k + 1}: a wrap carries no other swap params`, "policy_refused");
437
+ if (++wraps > 1) return noted("the route wraps more than once", "policy_refused");
426
438
  } else if (type === 9n) {
427
- if (!v) return `swap_type 9 (ERC-4626) is not available on ${deployment.label}'s router ${deployment.version}`;
428
- if (swap !== lc(v.vault)) return `step ${k + 1}: ERC-4626 hop through ${args.route[2 * k + 1]}, not the pinned vault`;
439
+ if (!v) return noted(`swap_type 9 (ERC-4626) is not available on ${deployment.label}'s router ${deployment.version}`, "policy_refused");
440
+ 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");
429
441
  const deposit = row[0] === 0n && a === lc(v.asset) && b === lc(v.vault);
430
442
  const redeem = row[0] === 1n && a === lc(v.vault) && b === lc(v.asset);
431
- 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`;
432
- if (row.some((x, c) => c > 0 && c !== 2 && x !== 0n)) return `step ${k + 1}: an ERC-4626 hop carries no other swap params`;
433
- if (++vaults > 1) return "the route crosses the vault more than once";
443
+ 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");
444
+ 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");
445
+ if (++vaults > 1) return noted("the route crosses the vault more than once", "policy_refused");
434
446
  } else {
435
- return `step ${k + 1} uses swap_type ${type}, which this module never builds`;
447
+ return noted(`step ${k + 1} uses swap_type ${type}, which this module never builds`, "policy_refused");
436
448
  }
437
449
  }
438
- if (pools.size > MAX_ROUTE_POOLS) return `the route crosses ${pools.size} pools, more than ${MAX_ROUTE_POOLS}`;
439
- if (pools.size === 0 && vaults === 0) return "the route crosses no pool";
450
+ if (pools.size > MAX_ROUTE_POOLS) return noted(`the route crosses ${pools.size} pools, more than ${MAX_ROUTE_POOLS}`, "policy_refused");
451
+ if (pools.size === 0 && vaults === 0) return noted("the route crosses no pool", "policy_refused");
440
452
  return null;
441
453
  }
442
454
 
443
455
  // src/provider.ts
444
456
  var import_viem4 = require("viem");
445
457
  var import_util5 = require("@anywalletkit/protocol-swap2-core/util");
458
+ var import_errors4 = require("@anywalletkit/protocol-swap2-core/errors");
446
459
 
447
460
  // src/activity.ts
448
461
  var import_activity = require("@anywalletkit/protocol-swap2-core/activity");
@@ -642,6 +655,7 @@ var CURVE_POOLS = Object.freeze({
642
655
  });
643
656
 
644
657
  // src/rpc.ts
658
+ var import_errors2 = require("@anywalletkit/protocol-swap2-core/errors");
645
659
  var import_util2 = require("@anywalletkit/protocol-swap2-core/util");
646
660
  var RPC_BATCH_MAX = 10;
647
661
  var RPC_RATE_LIMIT_PAUSE_MS = 1e3;
@@ -667,18 +681,18 @@ function rpcClient(rpcUrls) {
667
681
  body: JSON.stringify(calls.map((c, id) => ({ jsonrpc: "2.0", id, ...c }))),
668
682
  signal: signal ?? (0, import_util2.timeoutSignal)(12e3)
669
683
  });
670
- if (!res.ok) throw Object.assign(new Error(`HTTP ${res.status}`), { limited: res.status === 429 });
684
+ 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 });
671
685
  const body = await res.json();
672
- if (!Array.isArray(body)) throw Object.assign(new Error(body.error?.message ?? "batch refused"), { limited: isRateLimit(body.error) });
686
+ 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");
673
687
  const out = calls.map(() => null);
674
688
  const answered = /* @__PURE__ */ new Set();
675
689
  for (const r of body) {
676
690
  if (typeof r.id !== "number" || r.id < 0 || r.id >= out.length) continue;
677
- if (r.error !== void 0 && r.error !== null && !isExecutionError(r.error)) throw Object.assign(new Error(errorText(r.error)), { limited: isRateLimit(r.error) });
691
+ 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");
678
692
  answered.add(r.id);
679
693
  if (r.error === void 0 || r.error === null) out[r.id] = r.result ?? null;
680
694
  }
681
- if (answered.size !== calls.length) throw new Error(`answered ${answered.size} of ${calls.length} calls`);
695
+ if (answered.size !== calls.length) throw (0, import_errors2.refusal)("upstream_unavailable", `answered ${answered.size} of ${calls.length} calls`);
682
696
  return out;
683
697
  }
684
698
  async function batch(chainId, calls, signal) {
@@ -696,7 +710,9 @@ function rpcClient(rpcUrls) {
696
710
  }
697
711
  }
698
712
  }
699
- throw new Error(`curve: no RPC answered on chain ${chainId} (${lastErr instanceof Error ? lastErr.message : String(lastErr)})`);
713
+ const error = new Error(`curve: no RPC answered on chain ${chainId} (${lastErr instanceof Error ? lastErr.message : String(lastErr)})`);
714
+ if (lastErr === void 0 || signal?.aborted) throw error;
715
+ throw (0, import_errors2.withReason)(error, (0, import_errors2.refusalOf)(lastErr) ?? "upstream_unavailable");
700
716
  }
701
717
  return async function rpc(chainId, calls, signal) {
702
718
  if (calls.length <= RPC_BATCH_MAX) return batch(chainId, calls, signal);
@@ -756,6 +772,7 @@ function curveIntentChain(intentId) {
756
772
  var import_viem3 = require("viem");
757
773
  var import_util4 = require("@anywalletkit/protocol-swap2-core/util");
758
774
  var import_validate = require("@anywalletkit/protocol-swap2-core/validate");
775
+ var import_errors3 = require("@anywalletkit/protocol-swap2-core/errors");
759
776
  var lc2 = (a) => a.toLowerCase();
760
777
  var isAddress = (a) => /^0x[0-9a-fA-F]{40}$/.test(a);
761
778
  var own = (o, k) => Object.prototype.hasOwnProperty.call(o, k) ? o[k] : void 0;
@@ -765,19 +782,19 @@ function curveQuoteCheck(tables = {}) {
765
782
  return function verify(params, quote) {
766
783
  const chainId = chainIdOf(params.sourceChain);
767
784
  const d2 = deploymentOf(deployments, chainId);
768
- if (!d2 || params.destChain !== params.sourceChain) return "not a same-chain swap on a pinned Curve router";
785
+ if (!d2 || params.destChain !== params.sourceChain) return noted("not a same-chain swap on a pinned Curve router", "unsupported_pair");
769
786
  const tx = quote.txRequest;
770
- if (!tx?.data || !tx.to || tx.serializedTx !== void 0) return "the quote carries no router call";
771
- if (!isAddress(tx.to) || lc2(tx.to) !== lc2(d2.router)) return `the call targets ${tx.to}, not the Curve router`;
772
- if (tx.chainId !== void 0 && tx.chainId !== chainId) return `the call is for chain ${tx.chainId}`;
773
- if (curveIntentChain(quote.intentId) !== chainId) return `the quote names ${String(quote.intentId).slice(0, 40)}, not a curve:${chainId} swap`;
787
+ if (!tx?.data || !tx.to || tx.serializedTx !== void 0) return noted("the quote carries no router call", "policy_refused");
788
+ if (!isAddress(tx.to) || lc2(tx.to) !== lc2(d2.router)) return noted(`the call targets ${tx.to}, not the Curve router`, "vault_mismatch");
789
+ if (tx.chainId !== void 0 && tx.chainId !== chainId) return noted(`the call is for chain ${tx.chainId}`, "policy_refused");
790
+ if (curveIntentChain(quote.intentId) !== chainId) return noted(`the quote names ${String(quote.intentId).slice(0, 40)}, not a curve:${chainId} swap`, "policy_refused");
774
791
  const src = (0, import_util4.sourceAssetOf)(params);
775
- if (!src) return `unknown source token ${params.sourceToken}`;
792
+ if (!src) return noted(`unknown source token ${params.sourceToken}`, "unsupported_pair");
776
793
  const nativeIn = !src.contract;
777
794
  const nativeOut = /^0x0+$/.test(params.destToken);
778
- if (!nativeOut && !isAddress(params.destToken)) return "destination token must be an EVM address";
779
- if (!isAddress(params.destAddress)) return "recipient must be an EVM address";
780
- 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";
795
+ if (!nativeOut && !isAddress(params.destToken)) return noted("destination token must be an EVM address", "unsupported_pair");
796
+ if (!isAddress(params.destAddress)) return noted("recipient must be an EVM address", "recipient_mismatch");
797
+ 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");
781
798
  const tokenIn = nativeIn ? d2.eth : src.contract;
782
799
  const tokenOut = nativeOut ? d2.eth : params.destToken;
783
800
  let amountIn, minOut, value;
@@ -786,20 +803,20 @@ function curveQuoteCheck(tables = {}) {
786
803
  minOut = BigInt(quote.minDestAmount);
787
804
  value = BigInt(tx.value ?? "0");
788
805
  } catch {
789
- return "the request does not name amounts";
806
+ return noted("the request does not name amounts", "policy_refused");
790
807
  }
791
808
  if (nativeIn ? value !== amountIn || tx.approvalAddress !== void 0 : value !== 0n || !tx.approvalAddress || !isAddress(tx.approvalAddress) || lc2(tx.approvalAddress) !== lc2(d2.router)) {
792
- 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";
809
+ 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");
793
810
  }
794
811
  const call = decodeExchange(d2.family, tx.data);
795
- if (!call) return "the router call is not exchange(route, params, amount, min, receiver)";
796
- if (call.selector !== EXCHANGE_SELECTOR[d2.family]) return `selector ${call.selector} is not the explicit-receiver exchange ${EXCHANGE_SELECTOR[d2.family]}`;
797
- if (!call.receiver || lc2(call.receiver) !== lc2(params.destAddress)) return `the swap pays ${call.receiver ?? "the sender"}, not the recipient`;
798
- if (call.pools && call.pools.some((p) => lc2(p) !== lc2(ZERO_ADDRESS))) return "the call names zap pools this module never uses";
799
- if (call.amount !== amountIn) return `the swap sells ${call.amount}, not the ${amountIn} input`;
800
- if (call.minDy <= 0n) return "the swap has no minimum (min_dy 0)";
801
- if (call.minDy !== minOut) return `the swap's minimum ${call.minDy} is not the quote's ${minOut}`;
802
- if (quote.providerData?.minOut !== void 0 && quote.providerData.minOut !== minOut.toString()) return "the quote's facts disagree on the minimum";
812
+ if (!call) return noted("the router call is not exchange(route, params, amount, min, receiver)", "policy_refused");
813
+ if (call.selector !== EXCHANGE_SELECTOR[d2.family]) return noted(`selector ${call.selector} is not the explicit-receiver exchange ${EXCHANGE_SELECTOR[d2.family]}`, "policy_refused");
814
+ if (!call.receiver || lc2(call.receiver) !== lc2(params.destAddress)) return noted(`the swap pays ${call.receiver ?? "the sender"}, not the recipient`, "recipient_mismatch");
815
+ 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");
816
+ if (call.amount !== amountIn) return noted(`the swap sells ${call.amount}, not the ${amountIn} input`, "policy_refused");
817
+ if (call.minDy <= 0n) return noted("the swap has no minimum (min_dy 0)", "price_check_failed");
818
+ if (call.minDy !== minOut) return noted(`the swap's minimum ${call.minDy} is not the quote's ${minOut}`, "price_check_failed");
819
+ if (quote.providerData?.minOut !== void 0 && quote.providerData.minOut !== minOut.toString()) return noted("the quote's facts disagree on the minimum", "price_check_failed");
803
820
  return checkRoute(chainId, d2, own(pools, chainId), call, tokenIn, tokenOut);
804
821
  };
805
822
  }
@@ -807,7 +824,9 @@ var curveVerifyQuote = curveQuoteCheck();
807
824
  var curvePins = Object.freeze({
808
825
  callTargets: (chain) => routerFor(CURVE_DEPLOYMENTS, chain),
809
826
  spenders: (chain) => routerFor(CURVE_DEPLOYMENTS, chain),
810
- verifyQuote: curveVerifyQuote
827
+ verifyQuote: curveVerifyQuote,
828
+ // The reason of each text verifyQuote returned (remoteProvider puts it on its refusal).
829
+ reasonOf: notes.reasonOf
811
830
  });
812
831
  function routerFor(deployments, chain) {
813
832
  const d2 = deploymentOf(deployments, chainIdOf(chain));
@@ -824,21 +843,21 @@ function curveOnChain(opts) {
824
843
  if (!p) {
825
844
  const d2 = deploymentOf(deployments, chainId);
826
845
  p = (async () => {
827
- if (!d2) throw new Error(`curve: no pinned router on chain ${chainId}`);
846
+ if (!d2) throw (0, import_errors3.refusal)("unsupported_pair", `curve: no pinned router on chain ${chainId}`);
828
847
  const [code, version, listed] = await rpc(chainId, [
829
848
  { method: "eth_getCode", params: [d2.router, "latest"] },
830
849
  ethCall(d2.router, (0, import_viem3.encodeFunctionData)({ abi: VERSION_ABI, functionName: "version" })),
831
850
  ethCall(d2.addressProvider, (0, import_viem3.encodeFunctionData)({ abi: ADDRESS_PROVIDER_ABI, functionName: "get_address", args: [2n] }))
832
851
  ]);
833
852
  if (typeof code !== "string" || code.length <= 2 || lc2((0, import_viem3.keccak256)(code)) !== lc2(d2.codeHash)) {
834
- throw new Error(`curve: the router on chain ${chainId} is not the pinned code (code hash mismatch) \u2014 refusing`);
853
+ throw (0, import_errors3.refusal)("vault_mismatch", `curve: the router on chain ${chainId} is not the pinned code (code hash mismatch) \u2014 refusing`);
835
854
  }
836
855
  let v = "";
837
856
  try {
838
857
  v = typeof version === "string" ? (0, import_viem3.decodeFunctionResult)({ abi: VERSION_ABI, functionName: "version", data: version }) : "";
839
858
  } catch {
840
859
  }
841
- if (v !== d2.version) throw new Error(`curve: the router on chain ${chainId} reports version ${v || "none"}, not ${d2.version} \u2014 refusing`);
860
+ 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`);
842
861
  const ap = wordAddress(listed);
843
862
  if (ap && ap !== lc2(d2.addressProviderRouter) && ap !== lc2(d2.router)) {
844
863
  warn(`curve: the AddressProvider on ${d2.label} lists router ${ap} (id 2); this module keeps the pinned ${d2.router}`);
@@ -856,17 +875,17 @@ function curveOnChain(opts) {
856
875
  async function inspectSealed(quote, tx, probe) {
857
876
  const chainId = curveIntentChain(quote.intentId);
858
877
  const d2 = chainId === null ? void 0 : deploymentOf(deployments, chainId);
859
- if (chainId === null || !d2) return "the quote is not a Curve swap on a pinned chain";
878
+ if (chainId === null || !d2) return noted("the quote is not a Curve swap on a pinned chain", "policy_refused");
860
879
  const quoted = quote.txRequest;
861
- if (!quoted?.data || !quoted.to || !tx.data) return "the quote or the transaction carries no router call";
862
- if (tx.data.length !== quoted.data.length || lc2(tx.data) !== lc2(quoted.data)) return "the transaction is not the quoted router call";
863
- 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`;
864
- if ((tx.value ?? "0") !== (quoted.value ?? "0")) return "the transaction's value is not the quote's";
865
- if (tx.chainId !== void 0 && tx.chainId !== chainId || quoted.chainId !== void 0 && quoted.chainId !== chainId) return `the transaction is not for chain ${chainId}`;
866
- if ((tx.approvalAddress ?? "").toLowerCase() !== (quoted.approvalAddress ?? "").toLowerCase()) return "the transaction's approval is not the quote's";
880
+ if (!quoted?.data || !quoted.to || !tx.data) return noted("the quote or the transaction carries no router call", "policy_refused");
881
+ 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");
882
+ 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");
883
+ if ((tx.value ?? "0") !== (quoted.value ?? "0")) return noted("the transaction's value is not the quote's", "policy_refused");
884
+ 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");
885
+ if ((tx.approvalAddress ?? "").toLowerCase() !== (quoted.approvalAddress ?? "").toLowerCase()) return noted("the transaction's approval is not the quote's", "vault_mismatch");
867
886
  const call = decodeExchange(d2.family, quoted.data);
868
- if (!call || call.selector !== EXCHANGE_SELECTOR[d2.family]) return "the quoted call is not the explicit-receiver exchange";
869
- if (call.minDy <= 0n) return "the swap has no minimum (min_dy 0)";
887
+ if (!call || call.selector !== EXCHANGE_SELECTOR[d2.family]) return noted("the quoted call is not the explicit-receiver exchange", "policy_refused");
888
+ if (call.minDy <= 0n) return noted("the swap has no minimum (min_dy 0)", "price_check_failed");
870
889
  const last = lastToken(call);
871
890
  const reason = checkRoute(chainId, d2, own(pools, chainId), call, call.route[0], last);
872
891
  if (reason) return reason;
@@ -882,8 +901,8 @@ function curveOnChain(opts) {
882
901
  const poolReason = await poolsReason(chainId, hops, res.slice(1, 1 + reads.length));
883
902
  if (poolReason) return poolReason;
884
903
  const out = wordUint(res[0]);
885
- if (out === null) return "the router no longer quotes this route";
886
- if (out < call.minDy) return `price moved \u2014 the pools now pay ${out}, under your minimum ${call.minDy}`;
904
+ if (out === null) return noted("the router no longer quotes this route", "simulation_failed");
905
+ if (out < call.minDy) return noted(`price moved \u2014 the pools now pay ${out}, under your minimum ${call.minDy}`, "price_check_failed");
887
906
  return { out, probeOut: probe && probeIn > 0n ? wordUint(res[probeAt]) : null, amount: call.amount, minDy: call.minDy };
888
907
  }
889
908
  function poolCalls(list) {
@@ -897,19 +916,19 @@ function curveOnChain(opts) {
897
916
  const retry = [];
898
917
  for (const pool of list) {
899
918
  const code = res[at++];
900
- if (typeof code !== "string") return `could not read pool ${pool.name} (${pool.address}) over RPC`;
901
- if (code.length <= 2 || lc2((0, import_viem3.keccak256)(code)) !== lc2(pool.codeHash)) return `pool ${pool.name} (${pool.address}) is not the pinned code`;
919
+ if (typeof code !== "string") return noted(`could not read pool ${pool.name} (${pool.address}) over RPC`, "upstream_unavailable");
920
+ 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");
902
921
  for (let k = 0; k < pool.coins.length; k++) {
903
922
  const coin = wordAddress(res[at++]);
904
923
  if (coin === null) retry.push({ pool, k });
905
- else if (coin !== lc2(pool.coins[k])) return `pool ${pool.name} now reports coin ${k} = ${coin}, not the pinned ${pool.coins[k]}`;
924
+ 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");
906
925
  }
907
926
  }
908
927
  if (retry.length) {
909
928
  const again = await rpc(chainId, retry.map(({ pool, k }) => ethCall(pool.address, `${COINS_INT}${k.toString(16).padStart(64, "0")}`)));
910
929
  for (const [n, { pool, k }] of retry.entries()) {
911
930
  const coin = wordAddress(again[n]);
912
- if (coin !== lc2(pool.coins[k])) return `pool ${pool.name} does not report its pinned coin ${k}`;
931
+ if (coin !== lc2(pool.coins[k])) return noted(`pool ${pool.name} does not report its pinned coin ${k}`, "vault_mismatch");
913
932
  }
914
933
  }
915
934
  return null;
@@ -935,8 +954,10 @@ function curveRemotePins(opts) {
935
954
  callTargets: (c) => routerFor(deployments, c),
936
955
  spenders: (c) => routerFor(deployments, c),
937
956
  verifyQuote,
957
+ // The reason of each text verifyQuote / verifyBuild returned (remoteProvider puts it on its refusal).
958
+ reasonOf: notes.reasonOf,
938
959
  async verifyBuild(quote, tx, params) {
939
- if (!params) return "the request this quote answered is unknown";
960
+ if (!params) return noted("the request this quote answered is unknown", "policy_refused");
940
961
  const reason = verifyQuote(params, quote);
941
962
  if (reason) return reason;
942
963
  try {
@@ -948,15 +969,15 @@ function curveRemotePins(opts) {
948
969
  try {
949
970
  dest = BigInt(quote.destAmount);
950
971
  } catch {
951
- return "the quote names no output";
972
+ return noted("the quote names no output", "upstream_unavailable");
952
973
  }
953
974
  const bps = params.slippageBps ?? fixedSlippage ?? Math.min(9999, curveAutoSlippageBps(params, curveImpactBps(amount, out, probeOut)) + CURVE_AUTO_SLIPPAGE_DRIFT_BPS);
954
- if ((0, import_validate.slippageFloor)(dest, bps) > out) return `the quote promises ${dest}, more than ${bps} bps above the ${out} the pools pay now`;
975
+ if ((0, import_validate.slippageFloor)(dest, bps) > out) return noted(`the quote promises ${dest}, more than ${bps} bps above the ${out} the pools pay now`, "price_check_failed");
955
976
  const fresh = dest < out ? dest : out;
956
- if (minDy < (0, import_validate.slippageFloor)(fresh, bps)) return `the swap's minimum ${minDy} is more than ${bps} bps under the ${fresh} the pools pay now`;
977
+ if (minDy < (0, import_validate.slippageFloor)(fresh, bps)) return noted(`the swap's minimum ${minDy} is more than ${bps} bps under the ${fresh} the pools pay now`, "price_check_failed");
957
978
  return null;
958
979
  } catch (e) {
959
- return e instanceof Error ? e.message.replace(/^curve: /, "") : String(e);
980
+ return noted(e instanceof Error ? e.message.replace(/^curve: /, "") : String(e), (0, import_errors3.refusalOf)(e));
960
981
  }
961
982
  }
962
983
  });
@@ -994,46 +1015,47 @@ function curveProvider(config) {
994
1015
  return from.chain === to.chain && chainIdOf2(from.chain) !== null;
995
1016
  },
996
1017
  async getQuote(params, signal) {
997
- const refuse = (why) => {
998
- params.onRefusal?.(`curve: ${why}`);
1018
+ const refuse = (why, info) => {
1019
+ if (info === void 0) params.onRefusal?.(`curve: ${why}`);
1020
+ else params.onRefusal?.(`curve: ${why}`, typeof info === "string" ? { reason: info } : info);
999
1021
  return null;
1000
1022
  };
1001
- if (params.flex) return refuse("amount-flexible deposits are not supported");
1002
- if (params.sourceChain !== params.destChain) return refuse("same-chain swaps only");
1023
+ if (params.flex) return refuse("amount-flexible deposits are not supported", "unsupported_pair");
1024
+ if (params.sourceChain !== params.destChain) return refuse("same-chain swaps only", "unsupported_pair");
1003
1025
  const chainId = chainIdOf2(params.sourceChain);
1004
- if (chainId === null) return refuse(`no Curve router, pool allowlist or RPC for ${params.sourceChain}`);
1026
+ if (chainId === null) return refuse(`no Curve router, pool allowlist or RPC for ${params.sourceChain}`, "unsupported_pair");
1005
1027
  const d2 = deploymentOf(deployments, chainId);
1006
1028
  const src = (0, import_util5.sourceAssetOf)(params);
1007
- if (!src) return refuse(`unknown source token ${params.sourceToken}`);
1029
+ if (!src) return refuse(`unknown source token ${params.sourceToken}`, "unsupported_pair");
1008
1030
  const nativeIn = !src.contract;
1009
1031
  const nativeOut = /^0x0+$/.test(params.destToken);
1010
- if (!nativeOut && !/^0x[0-9a-fA-F]{40}$/.test(params.destToken)) return refuse("destination token must be an EVM address");
1011
- if (!/^0x[0-9a-fA-F]{40}$/.test(params.destAddress)) return refuse("recipient must be an EVM address");
1032
+ if (!nativeOut && !/^0x[0-9a-fA-F]{40}$/.test(params.destToken)) return refuse("destination token must be an EVM address", "unsupported_pair");
1033
+ if (!/^0x[0-9a-fA-F]{40}$/.test(params.destAddress)) return refuse("recipient must be an EVM address", "recipient_mismatch");
1012
1034
  let amountIn;
1013
1035
  try {
1014
1036
  amountIn = BigInt(params.amountIn);
1015
1037
  } catch {
1016
1038
  return refuse("amount is not an integer");
1017
1039
  }
1018
- if (amountIn <= 0n) return refuse("amount must be positive");
1040
+ if (amountIn <= 0n) return refuse("amount must be positive", "amount_too_small");
1019
1041
  if (params.slippageBps !== void 0 && (!Number.isInteger(params.slippageBps) || params.slippageBps < 0 || params.slippageBps >= 1e4)) return refuse("slippage must be 0..9999 bps");
1020
- 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");
1021
- if (!nativeOut && params.destToken.toLowerCase() === d2.eth.toLowerCase()) return refuse("name a native output with the zero address, not the router's 0xEeee sentinel");
1042
+ 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");
1043
+ 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");
1022
1044
  const tokenIn = (0, import_viem4.getAddress)(nativeIn ? d2.eth : src.contract);
1023
1045
  const tokenOut = (0, import_viem4.getAddress)(nativeOut ? d2.eth : params.destToken);
1024
- if (tokenIn === tokenOut) return refuse("same token in and out");
1046
+ if (tokenIn === tokenOut) return refuse("same token in and out", "unsupported_pair");
1025
1047
  const wrapPair = [(0, import_viem4.getAddress)(d2.eth), (0, import_viem4.getAddress)(d2.weth)];
1026
- if (wrapPair.includes(tokenIn) && wrapPair.includes(tokenOut)) return refuse("that is a wrap/unwrap, not a swap");
1048
+ if (wrapPair.includes(tokenIn) && wrapPair.includes(tokenOut)) return refuse("that is a wrap/unwrap, not a swap", "unsupported_pair");
1027
1049
  try {
1028
1050
  await chain.verifyDeployment(chainId);
1029
1051
  const { routes, truncated } = findRoutes(chainId, d2, tableOf(chainId), tokenIn, tokenOut);
1030
- if (!routes.length) return refuse("no allowlisted Curve pool route for this pair");
1052
+ if (!routes.length) return refuse("no allowlisted Curve pool route for this pair", "no_route");
1031
1053
  const outs = await price(chainId, d2, routes, amountIn, signal);
1032
1054
  let best = null;
1033
1055
  outs.forEach((out2, i) => {
1034
1056
  if (out2 !== null && out2 > 0n && (!best || out2 > best.out)) best = { route: routes[i], out: out2 };
1035
1057
  });
1036
- if (!best) return refuse("no allowlisted Curve pool quotes this pair and amount");
1058
+ if (!best) return refuse("no allowlisted Curve pool quotes this pair and amount", "no_route");
1037
1059
  const { route, out } = best;
1038
1060
  let priceImpactBps;
1039
1061
  const probeIn = amountIn / 1000n;
@@ -1046,9 +1068,9 @@ function curveProvider(config) {
1046
1068
  }
1047
1069
  const bps = params.slippageBps ?? fixedSlippage ?? curveAutoSlippageBps(params, priceImpactBps);
1048
1070
  const minOut = out * BigInt(1e4 - bps) / 10000n;
1049
- if (minOut <= 0n) return refuse("the minimum rounds to zero for this amount");
1071
+ if (minOut <= 0n) return refuse("the minimum rounds to zero for this amount", "amount_too_small");
1050
1072
  if (params.minAmountOut !== void 0 && minOut < BigInt(params.minAmountOut)) {
1051
- return refuse(`floor breach (min=${minOut} < floor=${params.minAmountOut})`);
1073
+ return refuse(`floor breach (min=${minOut} < floor=${params.minAmountOut})`, "price_check_failed");
1052
1074
  }
1053
1075
  const args = routeArgs(d2.family, route);
1054
1076
  const recipient = (0, import_viem4.getAddress)(params.destAddress);
@@ -1090,10 +1112,10 @@ function curveProvider(config) {
1090
1112
  }
1091
1113
  };
1092
1114
  const why = selfCheck(params, quote);
1093
- if (why) return refuse(`built a call its own check refuses (${why})`);
1115
+ if (why) return refuse(`built a call its own check refuses (${why})`, notes.reasonOf(why) ?? "policy_refused");
1094
1116
  return quote;
1095
1117
  } catch (e) {
1096
- return refuse(e instanceof Error ? e.message.replace(/^curve: /, "") : String(e));
1118
+ return refuse(e instanceof Error ? e.message.replace(/^curve: /, "") : String(e), (0, import_errors4.refusalOf)(e));
1097
1119
  }
1098
1120
  },
1099
1121
  /**
@@ -1103,11 +1125,11 @@ function curveProvider(config) {
1103
1125
  */
1104
1126
  async preflightDeposit(quote, tx) {
1105
1127
  const reason = await chain.checkSealed(quote, tx);
1106
- if (reason) throw new Error(`curve: ${reason}. Nothing was sent; refresh the quote.`);
1128
+ if (reason) throw tagged(new Error(`curve: ${reason}. Nothing was sent; refresh the quote.`), notes.reasonOf(reason));
1107
1129
  },
1108
1130
  async buildDeposit(quote) {
1109
1131
  const tx = quote.txRequest;
1110
- if (!tx?.to || !tx.data) throw new Error("swap2: Curve quote has no transaction request");
1132
+ if (!tx?.to || !tx.data) throw (0, import_errors4.refusal)("policy_refused", "swap2: Curve quote has no transaction request");
1111
1133
  return {
1112
1134
  chain: "",
1113
1135
  to: tx.to,