@blazephoenix/sdk 1.1.0 → 1.2.1

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/README.md CHANGED
@@ -58,9 +58,11 @@ const plan = await blaze.buildSwap({
58
58
  chain: 'base', tokenIn: 'WETH', tokenOut: 'USDC', amount: '1.5',
59
59
  recipient: me, from: me, // `from` enables the allowance check
60
60
  slippageBps: 50, // default 50; never below the on-chain floor
61
+ exact: true, // optional: minOut from previewPlanExact (one more eth_call)
61
62
  });
62
63
  plan.steps; // [approve?, swap] — { chainId, to, data, value }
63
64
  plan.minOut; // the minimum the Router will enforce for you
65
+ // (your userMinOut as given, when set; refused below the quote net of slippage)
64
66
  plan.encodedBy; // 'quoter' on 2.x (the Quoter's own bytes, verified) · 'sdk' on 1.x
65
67
 
66
68
  // 3) optional: dry-run on your node, then execute with YOUR wallet
@@ -161,7 +163,7 @@ await blaze.quoteBatch([req1, req2]); // per-item { ok, quote } | { ok:false, er
161
163
  await blaze.getFills({ chain: 'base', lookbackBlocks: 5_000n }); // Swap (+ ExecutionProof on 2.x)
162
164
  const stop = await blaze.watchFills({ chain: 'base', onFill: console.log });
163
165
  await blaze.tokenInfo('base', '0x…'); // symbol / name / decimals
164
- await blaze.solvency(); // the staking engine's proof-of-solvency (Base)
166
+ await blaze.solvency(); // staking proof-of-solvency; `not_deployed` where no engine is registered
165
167
  toJSON(quote); // bigint → string, for HTTP / queues / LLMs
166
168
  ```
167
169
 
@@ -186,6 +188,8 @@ Every failure, `solvency()` included, is a `BlazeError` with a stable `code`; pr
186
188
  | `no_route` | the Solver found no executable path (`SolverE(5)`) or the quote reverted |
187
189
  | `not_executable` | the Quoter says the route cannot settle now (`canExecute = false`) |
188
190
  | `calldata_mismatch` | Quoter bytes did not match your request — refused |
191
+ | `min_implausible` | `buildSwap`: `userMinOut` below the quote net of `slippageBps` (or the on-chain floor) |
192
+ | `min_above_exact` | `buildSwap({ exact: true })`: `userMinOut` above `exactOut` less the 50 bps safety margin |
189
193
  | `deployment_unverified` | a registry deployment failed on-chain verification |
190
194
  | `unsupported_by_version` | e.g. native ETH input on a 1.x Router |
191
195
  | `not_deployed` / `bad_request` / `wallet_chain_mismatch` / `reverted` | as named |
package/dist/index.cjs CHANGED
@@ -4738,8 +4738,7 @@ var CHAINS = {
4738
4738
  explorer: "https://basescan.org",
4739
4739
  weth: "0x4200000000000000000000000000000000000006",
4740
4740
  usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
4741
- bzpx: "0x23113e72165a034265Ab8Bf2277CCB7a85Cb7483",
4742
- staking: "0x3f60C7aa0c36a78D200405feBE143d2Cf3fA0c77",
4741
+ bzpx: "0x053D75c00FdD571538492e3B1686Bd963a0911A7",
4743
4742
  blockTime: 2
4744
4743
  },
4745
4744
  10: {
@@ -4769,7 +4768,8 @@ var CHAINS = {
4769
4768
  explorer: "https://robinhoodchain.blockscout.com",
4770
4769
  weth: "0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73",
4771
4770
  usdc: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168",
4772
- blockTime: 2
4771
+ blockTime: 0.104
4772
+ // measured ~0.104 s/block
4773
4773
  }
4774
4774
  };
4775
4775
  var ALIASES = {
@@ -4834,7 +4834,7 @@ function urlTransport(url, o) {
4834
4834
  throw new BlazeError("bad_request", `refusing RPC URL "${redact(url)}": use https:// or wss:// (http/ws only for localhost)`);
4835
4835
  }
4836
4836
  const u = url.trim();
4837
- return u.startsWith("ws") ? viem.webSocket(u, { timeout: o.timeoutMs, retryCount: o.retries }) : viem.http(u, { timeout: o.timeoutMs, retryCount: o.retries, batch: o.batch });
4837
+ return u.startsWith("ws") ? viem.webSocket(u, { timeout: o.timeoutMs, retryCount: o.retries }) : viem.http(u, { timeout: o.timeoutMs, retryCount: o.retries, batch: o.batch, fetchOptions: { redirect: "error" } });
4838
4838
  }
4839
4839
  function redact(url) {
4840
4840
  try {
@@ -4982,6 +4982,9 @@ function scrubCause(c, depth = 0) {
4982
4982
  out.name = c.name;
4983
4983
  return out;
4984
4984
  }
4985
+ function untrustedText(t, max = 120) {
4986
+ return (t ?? "").replace(/[^\x20-\x7e]+/g, " ").replace(/\s+/g, " ").trim().slice(0, max);
4987
+ }
4985
4988
  var BlazeError = class extends Error {
4986
4989
  code;
4987
4990
  details;
@@ -5813,6 +5816,20 @@ var BlazePhoenix = class {
5813
5816
  if (typeof req.amountIn === "bigint") v = req.amountIn;
5814
5817
  else if (INT.test(String(req.amountIn).trim())) v = BigInt(String(req.amountIn).trim());
5815
5818
  else throw new BlazeError("bad_request", 'amountIn must be an integer in base units \u2014 use `amount` for human units ("1.5")');
5819
+ if (req.amount !== void 0 && req.amount !== null && req.amount !== "") {
5820
+ const d = await this.decimalsOf(client, chainId, tIn).catch((e) => {
5821
+ throw new BlazeError("bad_request", "amount and amountIn are both set and the decimals could not be read to compare them \u2014 pass only one", { cause: e });
5822
+ });
5823
+ let human;
5824
+ try {
5825
+ human = toBaseUnits(req.amount, d);
5826
+ } catch (e) {
5827
+ throw new BlazeError("bad_request", e.message);
5828
+ }
5829
+ if (human !== v) {
5830
+ throw new BlazeError("bad_request", `amount and amountIn disagree (${human} vs ${v} base units): pass only one`);
5831
+ }
5832
+ }
5816
5833
  } else if (req.amount !== void 0) {
5817
5834
  const d = await this.decimalsOf(client, chainId, tIn).catch((e) => {
5818
5835
  throw new BlazeError("bad_request", `could not read decimals of ${tIn.address} on your RPC \u2014 pass amountIn in base units`, { cause: e });
@@ -5920,13 +5937,15 @@ var BlazePhoenix = class {
5920
5937
  args: [tIn.address, tOut.address, amountIn],
5921
5938
  ...req.blockNumber !== void 0 ? { blockNumber: req.blockNumber } : {}
5922
5939
  });
5940
+ const net = featuresOf(ctx.dep.version).previewAndEncode ? exactOut : exactOut - (exactOut * BigInt(FEE_BPS) + 9999n) / 10000n;
5923
5941
  return {
5924
5942
  chainId: ctx.chainId,
5925
5943
  version: ctx.dep.version,
5926
5944
  tokenIn: tIn.address,
5927
5945
  tokenOut: tOut.address,
5928
5946
  amountIn,
5929
- exactOut,
5947
+ exactOut: net,
5948
+ quoterOut: exactOut,
5930
5949
  route,
5931
5950
  quotedAt: Date.now()
5932
5951
  };
@@ -6035,6 +6054,60 @@ var BlazePhoenix = class {
6035
6054
  let encodedBy = "sdk";
6036
6055
  let entry;
6037
6056
  let minOut;
6057
+ let at;
6058
+ if (req.exact) {
6059
+ try {
6060
+ at = await ctx.client.getBlockNumber();
6061
+ } catch (e) {
6062
+ this.wrapReadError(e, "block number");
6063
+ }
6064
+ }
6065
+ const pin = at !== void 0 ? { blockNumber: at } : {};
6066
+ let exactOut;
6067
+ const exactRead = async (route) => {
6068
+ let ex;
6069
+ try {
6070
+ ex = await ctx.client.readContract({
6071
+ address: ctx.dep.contracts.quoter,
6072
+ abi: QUOTER_ABI,
6073
+ functionName: "previewPlanExact",
6074
+ args: [tIn.address, tOut.address, amountIn],
6075
+ ...pin
6076
+ });
6077
+ } catch (e) {
6078
+ this.wrapReadError(e, "exact quote");
6079
+ }
6080
+ if (!samePath(ex[0], route)) {
6081
+ throw new BlazeError("calldata_mismatch", "previewPlanExact priced a different route than the one to execute");
6082
+ }
6083
+ exactOut = ex[1];
6084
+ return ex[1];
6085
+ };
6086
+ const settleMin = async (p) => {
6087
+ let floor = minOutFor(p.netOut, slippageBps, p.ironFloor);
6088
+ let ceil;
6089
+ if (req.exact) {
6090
+ const ex = await exactRead(p.route);
6091
+ ceil = minOutFor(ex, EXACT_MARGIN_BPS, 0n);
6092
+ floor = maxBig(floor, minOutFor(ceil, slippageBps, 0n));
6093
+ }
6094
+ if (userMinOut === 0n) return floor;
6095
+ if (userMinOut < floor) {
6096
+ throw new BlazeError(
6097
+ "min_implausible",
6098
+ `userMinOut is below ${floor} (this quote net of slippageBps): set it from a price you trust, or raise slippageBps if a lower minimum is intended`,
6099
+ { details: { userMinOut: userMinOut.toString(), floor: floor.toString() } }
6100
+ );
6101
+ }
6102
+ if (ceil !== void 0 && userMinOut > ceil && userMinOut > floor) {
6103
+ throw new BlazeError(
6104
+ "min_above_exact",
6105
+ `userMinOut is above ${ceil} (exactOut less ${EXACT_MARGIN_BPS} bps): an honest swap at this minimum may revert`,
6106
+ { details: { userMinOut: userMinOut.toString(), ceiling: ceil.toString() } }
6107
+ );
6108
+ }
6109
+ return userMinOut;
6110
+ };
6038
6111
  if (features.previewAndEncode && !tIn.native && mode === "route") {
6039
6112
  let call;
6040
6113
  try {
@@ -6042,19 +6115,21 @@ var BlazePhoenix = class {
6042
6115
  address: ctx.dep.contracts.quoter,
6043
6116
  abi: QUOTER_ABI,
6044
6117
  functionName: "previewAndEncodeWithMinOut",
6045
- args: [tIn.address, tOut.address, amountIn, userMinOut, recipient, deadline]
6118
+ args: [tIn.address, tOut.address, amountIn, userMinOut, recipient, deadline],
6119
+ ...pin
6046
6120
  }) : await ctx.client.readContract({
6047
6121
  address: ctx.dep.contracts.quoter,
6048
6122
  abi: QUOTER_ABI,
6049
6123
  functionName: "previewAndEncode",
6050
- args: [tIn.address, tOut.address, amountIn, recipient, deadline]
6124
+ args: [tIn.address, tOut.address, amountIn, recipient, deadline],
6125
+ ...pin
6051
6126
  });
6052
6127
  [pv, call] = r;
6053
6128
  } catch (e) {
6054
6129
  this.wrapReadError(e, "previewAndEncode");
6055
6130
  }
6056
6131
  assertExecutable(pv, tIn.address, tOut.address, call);
6057
- minOut = maxBig(minOutFor(pv.netOut, slippageBps, pv.effectiveMinOut), userMinOut);
6132
+ minOut = await settleMin(pv);
6058
6133
  verifySwapExactIn(call, {
6059
6134
  tokenIn: tIn.address,
6060
6135
  tokenOut: tOut.address,
@@ -6068,9 +6143,9 @@ var BlazePhoenix = class {
6068
6143
  encodedBy = "quoter";
6069
6144
  entry = "swapExactIn";
6070
6145
  } else {
6071
- ({ pv, fb } = await this.readPreview(ctx, tIn.address, tOut.address, amountIn, userMinOut));
6146
+ ({ pv, fb } = await this.readPreview(ctx, tIn.address, tOut.address, amountIn, userMinOut, at));
6072
6147
  assertExecutable(pv, tIn.address, tOut.address);
6073
- minOut = maxBig(minOutFor(pv.netOut, slippageBps, pv.effectiveMinOut), userMinOut);
6148
+ minOut = await settleMin(pv);
6074
6149
  if (mode === "best") {
6075
6150
  entry = "swapBestExactIn";
6076
6151
  data = encodeSwapBestExactIn(tIn.address, tOut.address, amountIn, minOut, recipient, deadline);
@@ -6117,7 +6192,8 @@ var BlazePhoenix = class {
6117
6192
  slippageBps,
6118
6193
  approval,
6119
6194
  unwrapAfter: tOut.native,
6120
- steps: approval ? [approval.tx, tx] : [tx]
6195
+ steps: approval ? [approval.tx, tx] : [tx],
6196
+ ...exactOut !== void 0 ? { exactOut } : {}
6121
6197
  };
6122
6198
  }
6123
6199
  /** eth_call the swap from `from` on your RPC: the realised amountOut if it
@@ -6134,7 +6210,7 @@ var BlazePhoenix = class {
6134
6210
  } catch (e) {
6135
6211
  if (isTransportError(e)) throw new BlazeError("rpc_error", `simulation: your RPC failed (${e?.message?.split("\n")[0]})`, { cause: e });
6136
6212
  const d = decodeBlazeError(e);
6137
- return { ok: false, error: d ?? { contract: "unknown", name: "revert", reason: e?.message?.split("\n")[0] ?? "execution reverted" } };
6213
+ return { ok: false, error: d ?? { contract: "unknown", name: "revert", reason: untrustedText(e?.message?.split("\n")[0]) || "execution reverted" } };
6138
6214
  }
6139
6215
  }
6140
6216
  /**
@@ -6358,6 +6434,17 @@ function checkDeadlineSec(n) {
6358
6434
  return n;
6359
6435
  }
6360
6436
  var maxBig = (a, b) => a > b ? a : b;
6437
+ var EXACT_MARGIN_BPS = 50;
6438
+ function samePath(a, b) {
6439
+ const eqA = (x, y) => x.toLowerCase() === y.toLowerCase();
6440
+ return a.hops.length === b.hops.length && a.hops.every((h, i) => {
6441
+ const g = b.hops[i];
6442
+ return eqA(h.tokenIn, g.tokenIn) && eqA(h.tokenOut, g.tokenOut) && h.legs.length === g.legs.length && h.legs.every((l, j) => {
6443
+ const m = g.legs[j];
6444
+ return eqA(l.pool, m.pool) && eqA(l.hooks, m.hooks) && l.kind === m.kind && l.fee === m.fee && l.tickSpacing === m.tickSpacing && l.zeroForOne === m.zeroForOne && l.stable === m.stable;
6445
+ });
6446
+ });
6447
+ }
6361
6448
  function deepLink(opts = {}) {
6362
6449
  const base = (opts.baseUrl ?? "https://blazephoenix.xyz").replace(/\/+$/, "");
6363
6450
  const sp = new URLSearchParams();