@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/dist/index.d.cts CHANGED
@@ -145,7 +145,8 @@ interface QuoteRequest {
145
145
  amountIn?: bigint | string;
146
146
  /** Input in HUMAN units ("1.5"); decimals are read from the token on your RPC. */
147
147
  amount?: string;
148
- /** Tighten the on-chain floor with your own minimum (base units of tokenOut). */
148
+ /** Your own minimum (base units of tokenOut). In buildSwap it is the signed minimum as given,
149
+ * and it is refused (`min_implausible`) below the quote net of slippageBps or the on-chain floor. */
149
150
  userMinOut?: bigint | string;
150
151
  /** Protocol version for this call (default: the client's, itself 'latest'). */
151
152
  version?: string;
@@ -182,8 +183,10 @@ interface ExactQuote {
182
183
  tokenIn: Address;
183
184
  tokenOut: Address;
184
185
  amountIn: bigint;
185
- /** Execution-grade NET output (every concentrated leg dry-run on the pool). */
186
+ /** Execution-grade NET output (every concentrated leg dry-run on its venue), net of the protocol fee. */
186
187
  exactOut: bigint;
188
+ /** The scalar exactly as the Quoter returned it (gross of the fee on 1.x Quoters). */
189
+ quoterOut: bigint;
187
190
  route: Route;
188
191
  quotedAt: number;
189
192
  }
@@ -211,6 +214,15 @@ interface SwapRequest extends QuoteRequest {
211
214
  mode?: 'route' | 'best';
212
215
  /** Allowance to request when approval is needed: 'exact' (default) or 'max'. */
213
216
  approve?: 'exact' | 'max';
217
+ /**
218
+ * true — also read previewPlanExact (every concentrated leg dry-run) in the same block and
219
+ * derive minOut from its exactOut: a fixed 50 bps safety margin first, then slippageBps off
220
+ * that, never below the default minimum. The calldata still executes the route of the default
221
+ * path; the exact read must price that same route, or buildSwap refuses. A userMinOut above
222
+ * exactOut less the margin is refused (`min_above_exact`). One more eth_call; the swap reverts
223
+ * instead of filling short when the pool moves past the tolerance.
224
+ */
225
+ exact?: boolean;
214
226
  }
215
227
  interface ApprovalStep {
216
228
  token: Address;
@@ -237,6 +249,8 @@ interface SwapPlan {
237
249
  unwrapAfter: boolean;
238
250
  /** Ordered transactions to send: [approval?, swap]. */
239
251
  steps: TxRequest[];
252
+ /** With `exact: true`: the previewPlanExact output minOut was derived from. */
253
+ exactOut?: bigint;
240
254
  }
241
255
  interface SimulationResult {
242
256
  ok: boolean;
@@ -4249,7 +4263,7 @@ declare const CAUTION_IMPACT_BPS = 200;
4249
4263
  declare function routeIsConsistent(route: Route, tokenIn: Address, tokenOut: Address): boolean;
4250
4264
  declare function phoenixCheck(pv: Preview, tokenIn: Address, tokenOut: Address): QuoteChecks;
4251
4265
 
4252
- type BlazeErrorCode = 'rpc_required' | 'rpc_chain_mismatch' | 'rpc_error' | 'bad_request' | 'not_deployed' | 'unsupported_by_version' | 'no_route' | 'not_executable' | 'calldata_mismatch' | 'deployment_unverified' | 'reverted' | 'wallet_chain_mismatch';
4266
+ type BlazeErrorCode = 'rpc_required' | 'rpc_chain_mismatch' | 'rpc_error' | 'bad_request' | 'not_deployed' | 'unsupported_by_version' | 'no_route' | 'not_executable' | 'calldata_mismatch' | 'min_implausible' | 'min_above_exact' | 'deployment_unverified' | 'reverted' | 'wallet_chain_mismatch';
4253
4267
  declare class BlazeError extends Error {
4254
4268
  readonly code: BlazeErrorCode;
4255
4269
  readonly details?: Record<string, unknown>;
package/dist/index.d.ts CHANGED
@@ -145,7 +145,8 @@ interface QuoteRequest {
145
145
  amountIn?: bigint | string;
146
146
  /** Input in HUMAN units ("1.5"); decimals are read from the token on your RPC. */
147
147
  amount?: string;
148
- /** Tighten the on-chain floor with your own minimum (base units of tokenOut). */
148
+ /** Your own minimum (base units of tokenOut). In buildSwap it is the signed minimum as given,
149
+ * and it is refused (`min_implausible`) below the quote net of slippageBps or the on-chain floor. */
149
150
  userMinOut?: bigint | string;
150
151
  /** Protocol version for this call (default: the client's, itself 'latest'). */
151
152
  version?: string;
@@ -182,8 +183,10 @@ interface ExactQuote {
182
183
  tokenIn: Address;
183
184
  tokenOut: Address;
184
185
  amountIn: bigint;
185
- /** Execution-grade NET output (every concentrated leg dry-run on the pool). */
186
+ /** Execution-grade NET output (every concentrated leg dry-run on its venue), net of the protocol fee. */
186
187
  exactOut: bigint;
188
+ /** The scalar exactly as the Quoter returned it (gross of the fee on 1.x Quoters). */
189
+ quoterOut: bigint;
187
190
  route: Route;
188
191
  quotedAt: number;
189
192
  }
@@ -211,6 +214,15 @@ interface SwapRequest extends QuoteRequest {
211
214
  mode?: 'route' | 'best';
212
215
  /** Allowance to request when approval is needed: 'exact' (default) or 'max'. */
213
216
  approve?: 'exact' | 'max';
217
+ /**
218
+ * true — also read previewPlanExact (every concentrated leg dry-run) in the same block and
219
+ * derive minOut from its exactOut: a fixed 50 bps safety margin first, then slippageBps off
220
+ * that, never below the default minimum. The calldata still executes the route of the default
221
+ * path; the exact read must price that same route, or buildSwap refuses. A userMinOut above
222
+ * exactOut less the margin is refused (`min_above_exact`). One more eth_call; the swap reverts
223
+ * instead of filling short when the pool moves past the tolerance.
224
+ */
225
+ exact?: boolean;
214
226
  }
215
227
  interface ApprovalStep {
216
228
  token: Address;
@@ -237,6 +249,8 @@ interface SwapPlan {
237
249
  unwrapAfter: boolean;
238
250
  /** Ordered transactions to send: [approval?, swap]. */
239
251
  steps: TxRequest[];
252
+ /** With `exact: true`: the previewPlanExact output minOut was derived from. */
253
+ exactOut?: bigint;
240
254
  }
241
255
  interface SimulationResult {
242
256
  ok: boolean;
@@ -4249,7 +4263,7 @@ declare const CAUTION_IMPACT_BPS = 200;
4249
4263
  declare function routeIsConsistent(route: Route, tokenIn: Address, tokenOut: Address): boolean;
4250
4264
  declare function phoenixCheck(pv: Preview, tokenIn: Address, tokenOut: Address): QuoteChecks;
4251
4265
 
4252
- type BlazeErrorCode = 'rpc_required' | 'rpc_chain_mismatch' | 'rpc_error' | 'bad_request' | 'not_deployed' | 'unsupported_by_version' | 'no_route' | 'not_executable' | 'calldata_mismatch' | 'deployment_unverified' | 'reverted' | 'wallet_chain_mismatch';
4266
+ type BlazeErrorCode = 'rpc_required' | 'rpc_chain_mismatch' | 'rpc_error' | 'bad_request' | 'not_deployed' | 'unsupported_by_version' | 'no_route' | 'not_executable' | 'calldata_mismatch' | 'min_implausible' | 'min_above_exact' | 'deployment_unverified' | 'reverted' | 'wallet_chain_mismatch';
4253
4267
  declare class BlazeError extends Error {
4254
4268
  readonly code: BlazeErrorCode;
4255
4269
  readonly details?: Record<string, unknown>;
package/dist/index.js CHANGED
@@ -4736,8 +4736,7 @@ var CHAINS = {
4736
4736
  explorer: "https://basescan.org",
4737
4737
  weth: "0x4200000000000000000000000000000000000006",
4738
4738
  usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
4739
- bzpx: "0x23113e72165a034265Ab8Bf2277CCB7a85Cb7483",
4740
- staking: "0x3f60C7aa0c36a78D200405feBE143d2Cf3fA0c77",
4739
+ bzpx: "0x053D75c00FdD571538492e3B1686Bd963a0911A7",
4741
4740
  blockTime: 2
4742
4741
  },
4743
4742
  10: {
@@ -4767,7 +4766,8 @@ var CHAINS = {
4767
4766
  explorer: "https://robinhoodchain.blockscout.com",
4768
4767
  weth: "0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73",
4769
4768
  usdc: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168",
4770
- blockTime: 2
4769
+ blockTime: 0.104
4770
+ // measured ~0.104 s/block
4771
4771
  }
4772
4772
  };
4773
4773
  var ALIASES = {
@@ -4832,7 +4832,7 @@ function urlTransport(url, o) {
4832
4832
  throw new BlazeError("bad_request", `refusing RPC URL "${redact(url)}": use https:// or wss:// (http/ws only for localhost)`);
4833
4833
  }
4834
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 });
4835
+ return u.startsWith("ws") ? webSocket(u, { timeout: o.timeoutMs, retryCount: o.retries }) : http(u, { timeout: o.timeoutMs, retryCount: o.retries, batch: o.batch, fetchOptions: { redirect: "error" } });
4836
4836
  }
4837
4837
  function redact(url) {
4838
4838
  try {
@@ -4980,6 +4980,9 @@ function scrubCause(c, depth = 0) {
4980
4980
  out.name = c.name;
4981
4981
  return out;
4982
4982
  }
4983
+ function untrustedText(t, max = 120) {
4984
+ return (t ?? "").replace(/[^\x20-\x7e]+/g, " ").replace(/\s+/g, " ").trim().slice(0, max);
4985
+ }
4983
4986
  var BlazeError = class extends Error {
4984
4987
  code;
4985
4988
  details;
@@ -5811,6 +5814,20 @@ var BlazePhoenix = class {
5811
5814
  if (typeof req.amountIn === "bigint") v = req.amountIn;
5812
5815
  else if (INT.test(String(req.amountIn).trim())) v = BigInt(String(req.amountIn).trim());
5813
5816
  else throw new BlazeError("bad_request", 'amountIn must be an integer in base units \u2014 use `amount` for human units ("1.5")');
5817
+ if (req.amount !== void 0 && req.amount !== null && req.amount !== "") {
5818
+ const d = await this.decimalsOf(client, chainId, tIn).catch((e) => {
5819
+ 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 });
5820
+ });
5821
+ let human;
5822
+ try {
5823
+ human = toBaseUnits(req.amount, d);
5824
+ } catch (e) {
5825
+ throw new BlazeError("bad_request", e.message);
5826
+ }
5827
+ if (human !== v) {
5828
+ throw new BlazeError("bad_request", `amount and amountIn disagree (${human} vs ${v} base units): pass only one`);
5829
+ }
5830
+ }
5814
5831
  } else if (req.amount !== void 0) {
5815
5832
  const d = await this.decimalsOf(client, chainId, tIn).catch((e) => {
5816
5833
  throw new BlazeError("bad_request", `could not read decimals of ${tIn.address} on your RPC \u2014 pass amountIn in base units`, { cause: e });
@@ -5918,13 +5935,15 @@ var BlazePhoenix = class {
5918
5935
  args: [tIn.address, tOut.address, amountIn],
5919
5936
  ...req.blockNumber !== void 0 ? { blockNumber: req.blockNumber } : {}
5920
5937
  });
5938
+ const net = featuresOf(ctx.dep.version).previewAndEncode ? exactOut : exactOut - (exactOut * BigInt(FEE_BPS) + 9999n) / 10000n;
5921
5939
  return {
5922
5940
  chainId: ctx.chainId,
5923
5941
  version: ctx.dep.version,
5924
5942
  tokenIn: tIn.address,
5925
5943
  tokenOut: tOut.address,
5926
5944
  amountIn,
5927
- exactOut,
5945
+ exactOut: net,
5946
+ quoterOut: exactOut,
5928
5947
  route,
5929
5948
  quotedAt: Date.now()
5930
5949
  };
@@ -6033,6 +6052,60 @@ var BlazePhoenix = class {
6033
6052
  let encodedBy = "sdk";
6034
6053
  let entry;
6035
6054
  let minOut;
6055
+ let at;
6056
+ if (req.exact) {
6057
+ try {
6058
+ at = await ctx.client.getBlockNumber();
6059
+ } catch (e) {
6060
+ this.wrapReadError(e, "block number");
6061
+ }
6062
+ }
6063
+ const pin = at !== void 0 ? { blockNumber: at } : {};
6064
+ let exactOut;
6065
+ const exactRead = async (route) => {
6066
+ let ex;
6067
+ try {
6068
+ ex = await ctx.client.readContract({
6069
+ address: ctx.dep.contracts.quoter,
6070
+ abi: QUOTER_ABI,
6071
+ functionName: "previewPlanExact",
6072
+ args: [tIn.address, tOut.address, amountIn],
6073
+ ...pin
6074
+ });
6075
+ } catch (e) {
6076
+ this.wrapReadError(e, "exact quote");
6077
+ }
6078
+ if (!samePath(ex[0], route)) {
6079
+ throw new BlazeError("calldata_mismatch", "previewPlanExact priced a different route than the one to execute");
6080
+ }
6081
+ exactOut = ex[1];
6082
+ return ex[1];
6083
+ };
6084
+ const settleMin = async (p) => {
6085
+ let floor = minOutFor(p.netOut, slippageBps, p.ironFloor);
6086
+ let ceil;
6087
+ if (req.exact) {
6088
+ const ex = await exactRead(p.route);
6089
+ ceil = minOutFor(ex, EXACT_MARGIN_BPS, 0n);
6090
+ floor = maxBig(floor, minOutFor(ceil, slippageBps, 0n));
6091
+ }
6092
+ if (userMinOut === 0n) return floor;
6093
+ if (userMinOut < floor) {
6094
+ throw new BlazeError(
6095
+ "min_implausible",
6096
+ `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`,
6097
+ { details: { userMinOut: userMinOut.toString(), floor: floor.toString() } }
6098
+ );
6099
+ }
6100
+ if (ceil !== void 0 && userMinOut > ceil && userMinOut > floor) {
6101
+ throw new BlazeError(
6102
+ "min_above_exact",
6103
+ `userMinOut is above ${ceil} (exactOut less ${EXACT_MARGIN_BPS} bps): an honest swap at this minimum may revert`,
6104
+ { details: { userMinOut: userMinOut.toString(), ceiling: ceil.toString() } }
6105
+ );
6106
+ }
6107
+ return userMinOut;
6108
+ };
6036
6109
  if (features.previewAndEncode && !tIn.native && mode === "route") {
6037
6110
  let call;
6038
6111
  try {
@@ -6040,19 +6113,21 @@ var BlazePhoenix = class {
6040
6113
  address: ctx.dep.contracts.quoter,
6041
6114
  abi: QUOTER_ABI,
6042
6115
  functionName: "previewAndEncodeWithMinOut",
6043
- args: [tIn.address, tOut.address, amountIn, userMinOut, recipient, deadline]
6116
+ args: [tIn.address, tOut.address, amountIn, userMinOut, recipient, deadline],
6117
+ ...pin
6044
6118
  }) : await ctx.client.readContract({
6045
6119
  address: ctx.dep.contracts.quoter,
6046
6120
  abi: QUOTER_ABI,
6047
6121
  functionName: "previewAndEncode",
6048
- args: [tIn.address, tOut.address, amountIn, recipient, deadline]
6122
+ args: [tIn.address, tOut.address, amountIn, recipient, deadline],
6123
+ ...pin
6049
6124
  });
6050
6125
  [pv, call] = r;
6051
6126
  } catch (e) {
6052
6127
  this.wrapReadError(e, "previewAndEncode");
6053
6128
  }
6054
6129
  assertExecutable(pv, tIn.address, tOut.address, call);
6055
- minOut = maxBig(minOutFor(pv.netOut, slippageBps, pv.effectiveMinOut), userMinOut);
6130
+ minOut = await settleMin(pv);
6056
6131
  verifySwapExactIn(call, {
6057
6132
  tokenIn: tIn.address,
6058
6133
  tokenOut: tOut.address,
@@ -6066,9 +6141,9 @@ var BlazePhoenix = class {
6066
6141
  encodedBy = "quoter";
6067
6142
  entry = "swapExactIn";
6068
6143
  } else {
6069
- ({ pv, fb } = await this.readPreview(ctx, tIn.address, tOut.address, amountIn, userMinOut));
6144
+ ({ pv, fb } = await this.readPreview(ctx, tIn.address, tOut.address, amountIn, userMinOut, at));
6070
6145
  assertExecutable(pv, tIn.address, tOut.address);
6071
- minOut = maxBig(minOutFor(pv.netOut, slippageBps, pv.effectiveMinOut), userMinOut);
6146
+ minOut = await settleMin(pv);
6072
6147
  if (mode === "best") {
6073
6148
  entry = "swapBestExactIn";
6074
6149
  data = encodeSwapBestExactIn(tIn.address, tOut.address, amountIn, minOut, recipient, deadline);
@@ -6115,7 +6190,8 @@ var BlazePhoenix = class {
6115
6190
  slippageBps,
6116
6191
  approval,
6117
6192
  unwrapAfter: tOut.native,
6118
- steps: approval ? [approval.tx, tx] : [tx]
6193
+ steps: approval ? [approval.tx, tx] : [tx],
6194
+ ...exactOut !== void 0 ? { exactOut } : {}
6119
6195
  };
6120
6196
  }
6121
6197
  /** eth_call the swap from `from` on your RPC: the realised amountOut if it
@@ -6132,7 +6208,7 @@ var BlazePhoenix = class {
6132
6208
  } catch (e) {
6133
6209
  if (isTransportError(e)) throw new BlazeError("rpc_error", `simulation: your RPC failed (${e?.message?.split("\n")[0]})`, { cause: e });
6134
6210
  const d = decodeBlazeError(e);
6135
- return { ok: false, error: d ?? { contract: "unknown", name: "revert", reason: e?.message?.split("\n")[0] ?? "execution reverted" } };
6211
+ return { ok: false, error: d ?? { contract: "unknown", name: "revert", reason: untrustedText(e?.message?.split("\n")[0]) || "execution reverted" } };
6136
6212
  }
6137
6213
  }
6138
6214
  /**
@@ -6356,6 +6432,17 @@ function checkDeadlineSec(n) {
6356
6432
  return n;
6357
6433
  }
6358
6434
  var maxBig = (a, b) => a > b ? a : b;
6435
+ var EXACT_MARGIN_BPS = 50;
6436
+ function samePath(a, b) {
6437
+ const eqA = (x, y) => x.toLowerCase() === y.toLowerCase();
6438
+ return a.hops.length === b.hops.length && a.hops.every((h, i) => {
6439
+ const g = b.hops[i];
6440
+ return eqA(h.tokenIn, g.tokenIn) && eqA(h.tokenOut, g.tokenOut) && h.legs.length === g.legs.length && h.legs.every((l, j) => {
6441
+ const m = g.legs[j];
6442
+ 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;
6443
+ });
6444
+ });
6445
+ }
6359
6446
  function deepLink(opts = {}) {
6360
6447
  const base = (opts.baseUrl ?? "https://blazephoenix.xyz").replace(/\/+$/, "");
6361
6448
  const sp = new URLSearchParams();