@blazephoenix/sdk 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { decodeErrorResult, encodeFunctionData, decodeFunctionData, custom, fallback, createPublicClient, getAddress, decodeFunctionResult, parseEventLogs, webSocket, http } from 'viem';
1
+ import { custom, fallback, createPublicClient, decodeErrorResult, encodeFunctionData, decodeFunctionData, getAddress, decodeFunctionResult, parseEventLogs, webSocket, http } from 'viem';
2
2
 
3
3
  // src/client.ts
4
4
 
@@ -4714,12 +4714,278 @@ function phoenixCheck(pv, tokenIn, tokenOut) {
4714
4714
  disclaimer: 'Deterministic checks derived from on-chain state, not financial advice. "blocked" is a real answer \u2014 we fail closed rather than guess.'
4715
4715
  };
4716
4716
  }
4717
+
4718
+ // src/constants.ts
4719
+ var API_BASE = "https://blazephoenix.xyz";
4720
+ var DEPLOYMENTS_URL = `${API_BASE}/api/deployments`;
4721
+ var SUPPORTED_CHAIN_IDS = [8453, 1, 10, 42161, 4663];
4722
+ var CHAINS = {
4723
+ 1: {
4724
+ chainId: 1,
4725
+ name: "Ethereum",
4726
+ key: "ETHEREUM",
4727
+ explorer: "https://etherscan.io",
4728
+ weth: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
4729
+ usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
4730
+ blockTime: 12
4731
+ },
4732
+ 8453: {
4733
+ chainId: 8453,
4734
+ name: "Base",
4735
+ key: "BASE",
4736
+ explorer: "https://basescan.org",
4737
+ weth: "0x4200000000000000000000000000000000000006",
4738
+ usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
4739
+ bzpx: "0x23113e72165a034265Ab8Bf2277CCB7a85Cb7483",
4740
+ staking: "0x3f60C7aa0c36a78D200405feBE143d2Cf3fA0c77",
4741
+ blockTime: 2
4742
+ },
4743
+ 10: {
4744
+ chainId: 10,
4745
+ name: "Optimism",
4746
+ key: "OPTIMISM",
4747
+ explorer: "https://optimistic.etherscan.io",
4748
+ weth: "0x4200000000000000000000000000000000000006",
4749
+ usdc: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
4750
+ blockTime: 2
4751
+ },
4752
+ 42161: {
4753
+ chainId: 42161,
4754
+ name: "Arbitrum",
4755
+ key: "ARBITRUM",
4756
+ explorer: "https://arbiscan.io",
4757
+ weth: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
4758
+ usdc: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
4759
+ blockTime: 0.25
4760
+ },
4761
+ // Robinhood Chain — the dollar asset is USDG, not USDC; the `usdc` field
4762
+ // carries the chain's canonical dollar token. Explorer is Blockscout.
4763
+ 4663: {
4764
+ chainId: 4663,
4765
+ name: "Robinhood",
4766
+ key: "ROBINHOOD",
4767
+ explorer: "https://robinhoodchain.blockscout.com",
4768
+ weth: "0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73",
4769
+ usdc: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168",
4770
+ blockTime: 2
4771
+ }
4772
+ };
4773
+ var ALIASES = {
4774
+ "1": 1,
4775
+ eth: 1,
4776
+ ethereum: 1,
4777
+ mainnet: 1,
4778
+ "8453": 8453,
4779
+ base: 8453,
4780
+ "10": 10,
4781
+ op: 10,
4782
+ optimism: 10,
4783
+ "42161": 42161,
4784
+ arb: 42161,
4785
+ arbitrum: 42161,
4786
+ "arbitrum-one": 42161,
4787
+ "4663": 4663,
4788
+ rh: 4663,
4789
+ robinhood: 4663,
4790
+ "robinhood-chain": 4663
4791
+ };
4792
+ function resolveChain(chain) {
4793
+ const id = ALIASES[String(chain).trim().toLowerCase()];
4794
+ if (!id) {
4795
+ throw new Error(
4796
+ `Unsupported chain "${chain}" \u2014 use 8453/base, 1/eth, 10/optimism, 42161/arbitrum or 4663/robinhood`
4797
+ );
4798
+ }
4799
+ return id;
4800
+ }
4801
+ function tryResolveChain(chain) {
4802
+ return ALIASES[String(chain).trim().toLowerCase()];
4803
+ }
4804
+ var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
4805
+ var NATIVE_TOKEN = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
4806
+ var PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
4807
+ var FEE_BPS = 28;
4808
+ var SWAP_TOPIC0 = "0xd6d34547c69c5ee3d2667625c188acf1006abb93e0ee7cf03925c67cf7760413";
4809
+ var SURPLUS_TOPIC0 = "0x1a3afef0f067eb51a6bfec6ab3625a4408dd8c3571b72836483e68feb70d1026";
4810
+ var EXECUTION_PROOF_TOPIC0 = "0xb5a62bcb8753e37706da20862f2a32a16831ef0144567eb15b1da49547e9f84c";
4811
+ var FEE_TOPIC0 = "0x18fa25e22e0f1f39ef2130081363ba86a1259e598e669558683dabfc15bfe731";
4812
+
4813
+ // src/rpc.ts
4814
+ var isTransport = (v) => typeof v === "function";
4815
+ var isEip1193 = (v) => !!v && typeof v === "object" && typeof v.request === "function";
4816
+ var isSource = (v) => typeof v === "string" || Array.isArray(v) || isTransport(v) || isEip1193(v);
4817
+ function isAllowedRpcUrl(url) {
4818
+ let u;
4819
+ try {
4820
+ u = new URL(url.trim());
4821
+ } catch {
4822
+ return false;
4823
+ }
4824
+ if (u.protocol === "https:" || u.protocol === "wss:") return true;
4825
+ if (u.protocol === "http:" || u.protocol === "ws:") {
4826
+ return u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]";
4827
+ }
4828
+ return false;
4829
+ }
4830
+ function urlTransport(url, o) {
4831
+ if (!isAllowedRpcUrl(url)) {
4832
+ throw new BlazeError("bad_request", `refusing RPC URL "${redact(url)}": use https:// or wss:// (http/ws only for localhost)`);
4833
+ }
4834
+ const u = url.trim();
4835
+ return u.startsWith("ws") ? webSocket(u, { timeout: o.timeoutMs, retryCount: o.retries }) : http(u, { timeout: o.timeoutMs, retryCount: o.retries, batch: o.batch });
4836
+ }
4837
+ function redact(url) {
4838
+ try {
4839
+ const u = new URL(url);
4840
+ const path = u.pathname.length > 1 ? "/\u2026" : "";
4841
+ return `${u.protocol}//${u.host}${path}`;
4842
+ } catch {
4843
+ return "<invalid url>";
4844
+ }
4845
+ }
4846
+ function scrubUrls(text) {
4847
+ return text.replace(/\b(?:https?|wss?):\/\/[^\s"'<>]+/gi, (m) => redact(m));
4848
+ }
4849
+ function transportFrom(src, opts = {}) {
4850
+ const o = {
4851
+ timeoutMs: opts.timeoutMs ?? 1e4,
4852
+ retries: opts.retries ?? 2,
4853
+ batch: opts.batch ?? false
4854
+ };
4855
+ if (isTransport(src)) return src;
4856
+ if (isEip1193(src)) return custom(src, { retryCount: o.retries });
4857
+ if (typeof src === "string") return urlTransport(src, o);
4858
+ if (Array.isArray(src)) {
4859
+ const list = src.map((u) => u.trim()).filter(Boolean);
4860
+ if (list.length === 0) throw new BlazeError("rpc_required", "empty RPC list");
4861
+ if (list.length === 1) return urlTransport(list[0], o);
4862
+ return fallback(list.map((u) => urlTransport(u, { ...o, retries: 0 })), { retryCount: o.retries });
4863
+ }
4864
+ throw new BlazeError("bad_request", "unrecognised rpc value");
4865
+ }
4866
+ var RPC_HELP = "BlazePhoenix SDK 1.x runs 100% on YOUR RPC \u2014 it ships no endpoints and never reads through our servers. Pass `rpc` (a URL, a list of URLs, a wallet provider or a viem transport), e.g. new BlazePhoenix({ rpc: { base: process.env.BASE_RPC_URL } }). Any provider's free tier is enough.";
4867
+ var RpcRouter = class {
4868
+ perChain = /* @__PURE__ */ new Map();
4869
+ single;
4870
+ opts;
4871
+ constructor(config, opts = {}) {
4872
+ this.opts = opts;
4873
+ if (config === void 0 || config === null || config === "") return;
4874
+ if (isSource(config)) {
4875
+ this.single = { source: config };
4876
+ return;
4877
+ }
4878
+ if (typeof config !== "object") throw new BlazeError("bad_request", "rpc must be a URL, a list, a provider, a transport or a per-chain map");
4879
+ for (const [k, v] of Object.entries(config)) {
4880
+ if (v === void 0 || v === null || v === "") continue;
4881
+ const id = tryResolveChain(k);
4882
+ if (!id) throw new BlazeError("bad_request", `rpc map: unknown chain "${k}"`);
4883
+ if (!isSource(v)) throw new BlazeError("bad_request", `rpc map: bad value for chain "${k}"`);
4884
+ this.perChain.set(id, { source: v, chainId: id });
4885
+ }
4886
+ }
4887
+ /** Chains with an explicitly configured RPC (a single source reports none). */
4888
+ configuredChains() {
4889
+ return SUPPORTED_CHAIN_IDS.filter((id) => this.perChain.has(id));
4890
+ }
4891
+ hasAny() {
4892
+ return !!this.single || this.perChain.size > 0;
4893
+ }
4894
+ /** The chain to use when a request names none: the only configured one. */
4895
+ async defaultChain() {
4896
+ if (this.perChain.size === 1) return [...this.perChain.keys()][0];
4897
+ if (this.single && this.perChain.size === 0) {
4898
+ const id = await this.verify(this.single);
4899
+ const c = tryResolveChain(id);
4900
+ if (!c) throw new BlazeError("rpc_chain_mismatch", `your RPC serves chain ${id}, which BlazePhoenix is not deployed on`);
4901
+ return c;
4902
+ }
4903
+ throw new BlazeError("bad_request", "this request needs `chain` (several RPCs are configured)");
4904
+ }
4905
+ slotFor(chainId) {
4906
+ const s = this.perChain.get(chainId) ?? this.single;
4907
+ if (!s) {
4908
+ throw new BlazeError(
4909
+ "rpc_required",
4910
+ `no RPC configured for ${CHAINS[chainId].name} (${chainId}). ${RPC_HELP}`,
4911
+ { details: { chainId } }
4912
+ );
4913
+ }
4914
+ return s;
4915
+ }
4916
+ clientOf(s) {
4917
+ if (!s.client) {
4918
+ s.client = createPublicClient({ transport: transportFrom(s.source, this.opts) });
4919
+ }
4920
+ return s.client;
4921
+ }
4922
+ verify(s) {
4923
+ if (!s.verified) {
4924
+ const c = this.clientOf(s);
4925
+ s.verified = c.getChainId().catch((e) => {
4926
+ s.verified = void 0;
4927
+ throw new BlazeError("rpc_error", `your RPC did not answer eth_chainId: ${scrubUrls(String(e?.message ?? e))}`, { cause: e });
4928
+ });
4929
+ }
4930
+ return s.verified;
4931
+ }
4932
+ /** A PublicClient for `chain`, over YOUR node, after checking it really is that chain. */
4933
+ async client(chain) {
4934
+ const chainId = resolveChain(chain);
4935
+ const s = this.slotFor(chainId);
4936
+ const actual = await this.verify(s);
4937
+ if (actual !== chainId) {
4938
+ throw new BlazeError(
4939
+ "rpc_chain_mismatch",
4940
+ `your RPC serves chain ${actual}, not ${CHAINS[chainId].name} (${chainId}) \u2014 configure one per chain: rpc: { base: "\u2026", eth: "\u2026" }`,
4941
+ { details: { expected: chainId, actual } }
4942
+ );
4943
+ }
4944
+ return this.clientOf(s);
4945
+ }
4946
+ };
4947
+ function rpcFromEnv(env = globalEnv()) {
4948
+ const split = (v) => {
4949
+ const list = v.split(",").map((s) => s.trim()).filter(Boolean);
4950
+ return list.length === 1 ? list[0] : list;
4951
+ };
4952
+ const map = {};
4953
+ for (const id of SUPPORTED_CHAIN_IDS) {
4954
+ const v = env[`BLAZEPHOENIX_RPC_${CHAINS[id].key}`] ?? env[`BLAZEPHOENIX_RPC_${id}`];
4955
+ if (v && v.trim()) map[String(id)] = split(v);
4956
+ }
4957
+ const single = env.BLAZEPHOENIX_RPC_URL;
4958
+ if (Object.keys(map).length > 0) {
4959
+ if (single && single.trim()) {
4960
+ throw new BlazeError(
4961
+ "bad_request",
4962
+ "set either BLAZEPHOENIX_RPC_URL (one node) or the per-chain BLAZEPHOENIX_RPC_<CHAIN> variables, not both"
4963
+ );
4964
+ }
4965
+ return map;
4966
+ }
4967
+ return single && single.trim() ? split(single) : void 0;
4968
+ }
4969
+ function globalEnv() {
4970
+ const p = globalThis.process;
4971
+ return p?.env ?? {};
4972
+ }
4973
+
4974
+ // src/errors.ts
4975
+ function scrubCause(c, depth = 0) {
4976
+ if (c === void 0 || c === null || depth > 8) return void 0;
4977
+ if (!(c instanceof Error)) return scrubUrls(String(c));
4978
+ const inner = scrubCause(c.cause, depth + 1);
4979
+ const out = new Error(scrubUrls(c.message), inner !== void 0 ? { cause: inner } : void 0);
4980
+ out.name = c.name;
4981
+ return out;
4982
+ }
4717
4983
  var BlazeError = class extends Error {
4718
4984
  code;
4719
4985
  details;
4720
4986
  revert;
4721
4987
  constructor(code, message, opts = {}) {
4722
- super(message, opts.cause !== void 0 ? { cause: opts.cause } : void 0);
4988
+ super(message, opts.cause !== void 0 ? { cause: scrubCause(opts.cause) } : void 0);
4723
4989
  this.name = "BlazeError";
4724
4990
  this.code = code;
4725
4991
  this.details = opts.details;
@@ -4882,101 +5148,6 @@ function minOutFor(netOut, slippageBps, floor) {
4882
5148
  return bySlip > floor ? bySlip : floor;
4883
5149
  }
4884
5150
 
4885
- // src/constants.ts
4886
- var API_BASE = "https://blazephoenix.xyz";
4887
- var DEPLOYMENTS_URL = `${API_BASE}/api/deployments`;
4888
- var SUPPORTED_CHAIN_IDS = [8453, 1, 10, 42161, 4663];
4889
- var CHAINS = {
4890
- 1: {
4891
- chainId: 1,
4892
- name: "Ethereum",
4893
- key: "ETHEREUM",
4894
- explorer: "https://etherscan.io",
4895
- weth: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
4896
- usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
4897
- blockTime: 12
4898
- },
4899
- 8453: {
4900
- chainId: 8453,
4901
- name: "Base",
4902
- key: "BASE",
4903
- explorer: "https://basescan.org",
4904
- weth: "0x4200000000000000000000000000000000000006",
4905
- usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
4906
- bzpx: "0x23113e72165a034265Ab8Bf2277CCB7a85Cb7483",
4907
- staking: "0x3f60C7aa0c36a78D200405feBE143d2Cf3fA0c77",
4908
- blockTime: 2
4909
- },
4910
- 10: {
4911
- chainId: 10,
4912
- name: "Optimism",
4913
- key: "OPTIMISM",
4914
- explorer: "https://optimistic.etherscan.io",
4915
- weth: "0x4200000000000000000000000000000000000006",
4916
- usdc: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
4917
- blockTime: 2
4918
- },
4919
- 42161: {
4920
- chainId: 42161,
4921
- name: "Arbitrum",
4922
- key: "ARBITRUM",
4923
- explorer: "https://arbiscan.io",
4924
- weth: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
4925
- usdc: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
4926
- blockTime: 0.25
4927
- },
4928
- // Robinhood Chain — the dollar asset is USDG, not USDC; the `usdc` field
4929
- // carries the chain's canonical dollar token. Explorer is Blockscout.
4930
- 4663: {
4931
- chainId: 4663,
4932
- name: "Robinhood",
4933
- key: "ROBINHOOD",
4934
- explorer: "https://robinhoodchain.blockscout.com",
4935
- weth: "0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73",
4936
- usdc: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168",
4937
- blockTime: 2
4938
- }
4939
- };
4940
- var ALIASES = {
4941
- "1": 1,
4942
- eth: 1,
4943
- ethereum: 1,
4944
- mainnet: 1,
4945
- "8453": 8453,
4946
- base: 8453,
4947
- "10": 10,
4948
- op: 10,
4949
- optimism: 10,
4950
- "42161": 42161,
4951
- arb: 42161,
4952
- arbitrum: 42161,
4953
- "arbitrum-one": 42161,
4954
- "4663": 4663,
4955
- rh: 4663,
4956
- robinhood: 4663,
4957
- "robinhood-chain": 4663
4958
- };
4959
- function resolveChain(chain) {
4960
- const id = ALIASES[String(chain).trim().toLowerCase()];
4961
- if (!id) {
4962
- throw new Error(
4963
- `Unsupported chain "${chain}" \u2014 use 8453/base, 1/eth, 10/optimism, 42161/arbitrum or 4663/robinhood`
4964
- );
4965
- }
4966
- return id;
4967
- }
4968
- function tryResolveChain(chain) {
4969
- return ALIASES[String(chain).trim().toLowerCase()];
4970
- }
4971
- var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
4972
- var NATIVE_TOKEN = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
4973
- var PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
4974
- var FEE_BPS = 28;
4975
- var SWAP_TOPIC0 = "0xd6d34547c69c5ee3d2667625c188acf1006abb93e0ee7cf03925c67cf7760413";
4976
- var SURPLUS_TOPIC0 = "0x1a3afef0f067eb51a6bfec6ab3625a4408dd8c3571b72836483e68feb70d1026";
4977
- var EXECUTION_PROOF_TOPIC0 = "0xb5a62bcb8753e37706da20862f2a32a16831ef0144567eb15b1da49547e9f84c";
4978
- var FEE_TOPIC0 = "0x18fa25e22e0f1f39ef2130081363ba86a1259e598e669558683dabfc15bfe731";
4979
-
4980
5151
  // src/deployments.ts
4981
5152
  var Z = ZERO_ADDRESS;
4982
5153
  var pending = () => ({ core: Z, hub: Z, solver: Z, router: Z, quoter: Z });
@@ -5183,6 +5354,7 @@ var Registry = class {
5183
5354
  fetchedAt = 0;
5184
5355
  lastError;
5185
5356
  mode;
5357
+ remoteVersions;
5186
5358
  url;
5187
5359
  ttlMs;
5188
5360
  fetchFn;
@@ -5191,6 +5363,7 @@ var Registry = class {
5191
5363
  overrides = /* @__PURE__ */ new Map();
5192
5364
  constructor(opts = {}, overrides) {
5193
5365
  this.mode = opts.mode ?? "auto";
5366
+ this.remoteVersions = opts.remoteVersions ?? false;
5194
5367
  this.url = opts.url ?? DEPLOYMENTS_URL;
5195
5368
  this.ttlMs = opts.ttlMs ?? 10 * 6e4;
5196
5369
  this.fetchFn = opts.fetchFn ?? globalThis.fetch;
@@ -5322,7 +5495,7 @@ var Registry = class {
5322
5495
  if (!isValidSelector(selector)) throw new BlazeError("bad_request", `bad version selector "${selector}"`);
5323
5496
  const ov = this.overrides.get(chainId);
5324
5497
  const sel = ov?.version && selector === "latest" ? ov.version : selector;
5325
- const candidates = this.doc.versions.filter((v) => matchesSelector(v.version, sel) && isDeployed(v.chains[chainId])).sort((a, b) => compareVersions(b.version, a.version));
5498
+ const candidates = this.doc.versions.filter((v) => matchesSelector(v.version, sel) && isDeployed(v.chains[chainId])).filter((v) => this.remoteVersions || this.isPinned(v.version, chainId)).sort((a, b) => compareVersions(b.version, a.version));
5326
5499
  const pick = candidates[0];
5327
5500
  if (ov && (ov.router || ov.quoter)) {
5328
5501
  const base = pick ? { ...pick.chains[chainId] } : {
@@ -5342,9 +5515,10 @@ var Registry = class {
5342
5515
  }
5343
5516
  if (!pick) {
5344
5517
  const deployed = this.versionsOn(chainId);
5518
+ const remoteOnly = this.remoteVersions ? [] : deployed.filter((v) => matchesSelector(v, sel) && !this.isPinned(v, chainId));
5345
5519
  throw new BlazeError(
5346
5520
  "not_deployed",
5347
- `no BlazePhoenix deployment matching version "${selector}" on chain ${chainId}` + (deployed.length ? ` (deployed: ${deployed.join(", ")})` : ""),
5521
+ `no BlazePhoenix deployment matching version "${selector}" on chain ${chainId}` + (deployed.length ? ` (deployed: ${deployed.join(", ")})` : "") + (remoteOnly.length ? ` \u2014 ${remoteOnly.join(", ")} is known only from the fetched registry; update the SDK, or set registry.remoteVersions after checking the addresses yourself` : ""),
5348
5522
  { details: { chainId, selector, deployed } }
5349
5523
  );
5350
5524
  }
@@ -5353,6 +5527,11 @@ var Registry = class {
5353
5527
  const source = src("router") === "remote" || src("quoter") === "remote" ? "remote" : "embedded";
5354
5528
  return { chainId, version: pick.version, status: pick.status, contracts: { ...set }, source };
5355
5529
  }
5530
+ /** Router and Quoter both pinned in this package (not filled by a fetched document). */
5531
+ isPinned(version, chainId) {
5532
+ const src = (k) => this.fieldSource[`${version}|${chainId}|${k}`];
5533
+ return src("router") === "embedded" && src("quoter") === "embedded";
5534
+ }
5356
5535
  latestKnownVersion(selector) {
5357
5536
  const all = this.doc.versions.map((v) => v.version).sort((a, b) => compareVersions(b, a));
5358
5537
  const m = selector ? all.find((v) => matchesSelector(v, selector)) : void 0;
@@ -5469,162 +5648,6 @@ function fromBaseUnits(v, decimals, maxDp) {
5469
5648
  frac = frac.replace(/0+$/, "");
5470
5649
  return `${neg ? "-" : ""}${whole}${frac ? `.${frac}` : ""}`;
5471
5650
  }
5472
- var isTransport = (v) => typeof v === "function";
5473
- var isEip1193 = (v) => !!v && typeof v === "object" && typeof v.request === "function";
5474
- var isSource = (v) => typeof v === "string" || Array.isArray(v) || isTransport(v) || isEip1193(v);
5475
- function isAllowedRpcUrl(url) {
5476
- let u;
5477
- try {
5478
- u = new URL(url.trim());
5479
- } catch {
5480
- return false;
5481
- }
5482
- if (u.protocol === "https:" || u.protocol === "wss:") return true;
5483
- if (u.protocol === "http:" || u.protocol === "ws:") {
5484
- return u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]";
5485
- }
5486
- return false;
5487
- }
5488
- function urlTransport(url, o) {
5489
- if (!isAllowedRpcUrl(url)) {
5490
- throw new BlazeError("bad_request", `refusing RPC URL "${redact(url)}": use https:// or wss:// (http/ws only for localhost)`);
5491
- }
5492
- const u = url.trim();
5493
- return u.startsWith("ws") ? webSocket(u, { timeout: o.timeoutMs, retryCount: o.retries }) : http(u, { timeout: o.timeoutMs, retryCount: o.retries, batch: o.batch });
5494
- }
5495
- function redact(url) {
5496
- try {
5497
- const u = new URL(url);
5498
- const path = u.pathname.length > 1 ? "/\u2026" : "";
5499
- return `${u.protocol}//${u.host}${path}`;
5500
- } catch {
5501
- return "<invalid url>";
5502
- }
5503
- }
5504
- function transportFrom(src, opts = {}) {
5505
- const o = {
5506
- timeoutMs: opts.timeoutMs ?? 1e4,
5507
- retries: opts.retries ?? 2,
5508
- batch: opts.batch ?? false
5509
- };
5510
- if (isTransport(src)) return src;
5511
- if (isEip1193(src)) return custom(src, { retryCount: o.retries });
5512
- if (typeof src === "string") return urlTransport(src, o);
5513
- if (Array.isArray(src)) {
5514
- const list = src.map((u) => u.trim()).filter(Boolean);
5515
- if (list.length === 0) throw new BlazeError("rpc_required", "empty RPC list");
5516
- if (list.length === 1) return urlTransport(list[0], o);
5517
- return fallback(list.map((u) => urlTransport(u, { ...o, retries: 0 })), { retryCount: o.retries });
5518
- }
5519
- throw new BlazeError("bad_request", "unrecognised rpc value");
5520
- }
5521
- var RPC_HELP = "BlazePhoenix SDK 1.x runs 100% on YOUR RPC \u2014 it ships no endpoints and never reads through our servers. Pass `rpc` (a URL, a list of URLs, a wallet provider or a viem transport), e.g. new BlazePhoenix({ rpc: { base: process.env.BASE_RPC_URL } }). Any provider's free tier is enough.";
5522
- var RpcRouter = class {
5523
- perChain = /* @__PURE__ */ new Map();
5524
- single;
5525
- opts;
5526
- constructor(config, opts = {}) {
5527
- this.opts = opts;
5528
- if (config === void 0 || config === null || config === "") return;
5529
- if (isSource(config)) {
5530
- this.single = { source: config };
5531
- return;
5532
- }
5533
- if (typeof config !== "object") throw new BlazeError("bad_request", "rpc must be a URL, a list, a provider, a transport or a per-chain map");
5534
- for (const [k, v] of Object.entries(config)) {
5535
- if (v === void 0 || v === null || v === "") continue;
5536
- const id = tryResolveChain(k);
5537
- if (!id) throw new BlazeError("bad_request", `rpc map: unknown chain "${k}"`);
5538
- if (!isSource(v)) throw new BlazeError("bad_request", `rpc map: bad value for chain "${k}"`);
5539
- this.perChain.set(id, { source: v, chainId: id });
5540
- }
5541
- }
5542
- /** Chains with an explicitly configured RPC (a single source reports none). */
5543
- configuredChains() {
5544
- return SUPPORTED_CHAIN_IDS.filter((id) => this.perChain.has(id));
5545
- }
5546
- hasAny() {
5547
- return !!this.single || this.perChain.size > 0;
5548
- }
5549
- /** The chain to use when a request names none: the only configured one. */
5550
- async defaultChain() {
5551
- if (this.perChain.size === 1) return [...this.perChain.keys()][0];
5552
- if (this.single && this.perChain.size === 0) {
5553
- const id = await this.verify(this.single);
5554
- const c = tryResolveChain(id);
5555
- if (!c) throw new BlazeError("rpc_chain_mismatch", `your RPC serves chain ${id}, which BlazePhoenix is not deployed on`);
5556
- return c;
5557
- }
5558
- throw new BlazeError("bad_request", "this request needs `chain` (several RPCs are configured)");
5559
- }
5560
- slotFor(chainId) {
5561
- const s = this.perChain.get(chainId) ?? this.single;
5562
- if (!s) {
5563
- throw new BlazeError(
5564
- "rpc_required",
5565
- `no RPC configured for ${CHAINS[chainId].name} (${chainId}). ${RPC_HELP}`,
5566
- { details: { chainId } }
5567
- );
5568
- }
5569
- return s;
5570
- }
5571
- clientOf(s) {
5572
- if (!s.client) {
5573
- s.client = createPublicClient({ transport: transportFrom(s.source, this.opts) });
5574
- }
5575
- return s.client;
5576
- }
5577
- verify(s) {
5578
- if (!s.verified) {
5579
- const c = this.clientOf(s);
5580
- s.verified = c.getChainId().catch((e) => {
5581
- s.verified = void 0;
5582
- throw new BlazeError("rpc_error", `your RPC did not answer eth_chainId: ${e?.message ?? e}`, { cause: e });
5583
- });
5584
- }
5585
- return s.verified;
5586
- }
5587
- /** A PublicClient for `chain`, over YOUR node, after checking it really is that chain. */
5588
- async client(chain) {
5589
- const chainId = resolveChain(chain);
5590
- const s = this.slotFor(chainId);
5591
- const actual = await this.verify(s);
5592
- if (actual !== chainId) {
5593
- throw new BlazeError(
5594
- "rpc_chain_mismatch",
5595
- `your RPC serves chain ${actual}, not ${CHAINS[chainId].name} (${chainId}) \u2014 configure one per chain: rpc: { base: "\u2026", eth: "\u2026" }`,
5596
- { details: { expected: chainId, actual } }
5597
- );
5598
- }
5599
- return this.clientOf(s);
5600
- }
5601
- };
5602
- function rpcFromEnv(env = globalEnv()) {
5603
- const split = (v) => {
5604
- const list = v.split(",").map((s) => s.trim()).filter(Boolean);
5605
- return list.length === 1 ? list[0] : list;
5606
- };
5607
- const map = {};
5608
- for (const id of SUPPORTED_CHAIN_IDS) {
5609
- const v = env[`BLAZEPHOENIX_RPC_${CHAINS[id].key}`] ?? env[`BLAZEPHOENIX_RPC_${id}`];
5610
- if (v && v.trim()) map[String(id)] = split(v);
5611
- }
5612
- const single = env.BLAZEPHOENIX_RPC_URL;
5613
- if (Object.keys(map).length > 0) {
5614
- if (single && single.trim()) {
5615
- throw new BlazeError(
5616
- "bad_request",
5617
- "set either BLAZEPHOENIX_RPC_URL (one node) or the per-chain BLAZEPHOENIX_RPC_<CHAIN> variables, not both"
5618
- );
5619
- }
5620
- return map;
5621
- }
5622
- return single && single.trim() ? split(single) : void 0;
5623
- }
5624
- function globalEnv() {
5625
- const p = globalThis.process;
5626
- return p?.env ?? {};
5627
- }
5628
5651
 
5629
5652
  // src/client.ts
5630
5653
  var HEX_ADDR2 = /^0x[0-9a-fA-F]{40}$/;
@@ -5700,6 +5723,8 @@ var BlazePhoenix = class {
5700
5723
  checks.push({ name: `code:${k}`, ok: !!code && code !== "0x", detail: code && code !== "0x" ? `${(code.length - 2) / 2} bytes` : "no code at address" });
5701
5724
  });
5702
5725
  if (featuresOf(dep.version).introspection) {
5726
+ if (isZero(c.hub)) checks.push({ name: "hub", ok: false, detail: "zero address" });
5727
+ if (isZero(c.solver)) checks.push({ name: "solver", ok: false, detail: "zero address" });
5703
5728
  const eq = (a, b) => a.toLowerCase() === b.toLowerCase();
5704
5729
  const read = async (fn) => {
5705
5730
  try {
@@ -5754,16 +5779,19 @@ var BlazePhoenix = class {
5754
5779
  return { address: c.weth, native: true };
5755
5780
  case "WETH":
5756
5781
  return { address: c.weth, native: false };
5782
+ // `usdc` holds each chain's one dollar asset: USDG on Robinhood Chain, USDC
5783
+ // elsewhere. The other symbol names a different token there, so it is refused.
5757
5784
  case "USDC":
5758
5785
  case "USDG":
5759
- return { address: c.usdc, native: false };
5786
+ if (s.toUpperCase() === (chainId === 4663 ? "USDG" : "USDC")) return { address: c.usdc, native: false };
5787
+ break;
5760
5788
  case "BZPX":
5761
5789
  if (c.bzpx) return { address: c.bzpx, native: false };
5762
5790
  break;
5763
5791
  }
5764
5792
  throw new BlazeError(
5765
5793
  "bad_request",
5766
- `${side}: "${ref}" is not a 0x address or a built-in symbol (ETH, WETH, USDC, BZPX) on ${c.name} \u2014 pass the token address`
5794
+ `${side}: "${ref}" is not a 0x address or a built-in symbol (ETH, WETH, ${chainId === 4663 ? "USDG" : "USDC"}, BZPX) on ${c.name} \u2014 pass the token address`
5767
5795
  );
5768
5796
  }
5769
5797
  async decimalsOf(client, chainId, t) {
@@ -5910,7 +5938,7 @@ var BlazePhoenix = class {
5910
5938
  async quoteBatch(reqs) {
5911
5939
  const fail = (e) => ({
5912
5940
  ok: false,
5913
- error: { code: e instanceof BlazeError ? e.code : "error", message: e?.message ?? String(e) }
5941
+ error: { code: e instanceof BlazeError ? e.code : "error", message: scrubUrls(e?.message ?? String(e)) }
5914
5942
  });
5915
5943
  const out = new Array(reqs.length);
5916
5944
  const groups = /* @__PURE__ */ new Map();
@@ -6153,6 +6181,12 @@ var BlazePhoenix = class {
6153
6181
  if (r.status !== "success") throw new BlazeError("reverted", `approval transaction reverted (${approvalHash})`);
6154
6182
  }
6155
6183
  plan = { ...plan, approval: { ...plan.approval, current: plan.approval.amount } };
6184
+ if (Date.now() - plan.quote.quotedAt > maxAge) {
6185
+ if (!req) {
6186
+ throw new BlazeError("bad_request", `plan went older than ${maxAge}ms while the approval confirmed${approvalHash ? ` (${approvalHash})` : ""} \u2014 the swap was NOT sent; rebuild the plan, or pass \`request\` so it can be rebuilt`);
6187
+ }
6188
+ plan = await this.buildSwap({ ...req, from: req.from ?? account.address });
6189
+ }
6156
6190
  }
6157
6191
  if (opts.simulate ?? true) {
6158
6192
  const sim = await this.simulate(plan, account.address);
@@ -6201,11 +6235,16 @@ var BlazePhoenix = class {
6201
6235
  const staking = CHAINS[chainId].staking;
6202
6236
  if (!staking) throw new BlazeError("not_deployed", `no BlazePhoenix staking engine on ${CHAINS[chainId].name}`);
6203
6237
  const client = await this.rpc.client(chainId);
6204
- const blockNumber = await client.getBlockNumber();
6205
- const [r, isSolvent] = await Promise.all([
6206
- client.readContract({ address: staking, abi: STAKING_SOLVENCY_ABI, functionName: "solvency", blockNumber }),
6207
- client.readContract({ address: staking, abi: STAKING_SOLVENCY_ABI, functionName: "isSolvent", blockNumber })
6208
- ]);
6238
+ let blockNumber, r, isSolvent;
6239
+ try {
6240
+ blockNumber = await client.getBlockNumber();
6241
+ [r, isSolvent] = await Promise.all([
6242
+ client.readContract({ address: staking, abi: STAKING_SOLVENCY_ABI, functionName: "solvency", blockNumber }),
6243
+ client.readContract({ address: staking, abi: STAKING_SOLVENCY_ABI, functionName: "isSolvent", blockNumber })
6244
+ ]);
6245
+ } catch (e) {
6246
+ throw new BlazeError("rpc_error", `solvency read failed on your RPC: ${scrubUrls(String(e?.message ?? e))}`, { cause: e });
6247
+ }
6209
6248
  const s = r;
6210
6249
  return {
6211
6250
  chainId,