@blazephoenix/sdk 1.0.1 → 1.2.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.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;
@@ -211,6 +212,15 @@ interface SwapRequest extends QuoteRequest {
211
212
  mode?: 'route' | 'best';
212
213
  /** Allowance to request when approval is needed: 'exact' (default) or 'max'. */
213
214
  approve?: 'exact' | 'max';
215
+ /**
216
+ * true — also read previewPlanExact (every concentrated leg dry-run) in the same block and
217
+ * derive minOut from its exactOut: a fixed 50 bps safety margin first, then slippageBps off
218
+ * that, never below the default minimum. The calldata still executes the route of the default
219
+ * path; the exact read must price that same route, or buildSwap refuses. A userMinOut above
220
+ * exactOut less the margin is refused (`min_above_exact`). One more eth_call; the swap reverts
221
+ * instead of filling short when the pool moves past the tolerance.
222
+ */
223
+ exact?: boolean;
214
224
  }
215
225
  interface ApprovalStep {
216
226
  token: Address;
@@ -237,6 +247,8 @@ interface SwapPlan {
237
247
  unwrapAfter: boolean;
238
248
  /** Ordered transactions to send: [approval?, swap]. */
239
249
  steps: TxRequest[];
250
+ /** With `exact: true`: the previewPlanExact output minOut was derived from. */
251
+ exactOut?: bigint;
240
252
  }
241
253
  interface SimulationResult {
242
254
  ok: boolean;
@@ -385,6 +397,12 @@ interface RegistryOptions {
385
397
  * delay a call, and by at most this much; later refreshes run in the
386
398
  * background (stale-while-revalidate). */
387
399
  timeoutMs?: number;
400
+ /** Route to addresses that only the fetched document supplies (a version
401
+ * released after this SDK). Default false: the document can list them, but
402
+ * resolve() only returns Router/Quoter addresses pinned in this package —
403
+ * the on-chain check of a remote entry reads the entry's own contracts, so
404
+ * it cannot tell a real deployment from a look-alike. */
405
+ remoteVersions?: boolean;
388
406
  /** Called for every pin conflict or rejected document — never silently. */
389
407
  onWarning?: (msg: string) => void;
390
408
  }
@@ -394,6 +412,7 @@ declare class Registry {
394
412
  private fetchedAt;
395
413
  private lastError?;
396
414
  private readonly mode;
415
+ private readonly remoteVersions;
397
416
  private readonly url;
398
417
  private readonly ttlMs;
399
418
  private readonly fetchFn?;
@@ -422,6 +441,8 @@ declare class Registry {
422
441
  /** Every deployed version on a chain, newest first. */
423
442
  versionsOn(chainId: SupportedChainId): string[];
424
443
  resolve(chainId: SupportedChainId, selector?: VersionSelector): ResolvedDeployment;
444
+ /** Router and Quoter both pinned in this package (not filled by a fetched document). */
445
+ private isPinned;
425
446
  private latestKnownVersion;
426
447
  /** chains × versions table (for dashboards, the MCP, debugging). */
427
448
  table(): {
@@ -4240,7 +4261,7 @@ declare const CAUTION_IMPACT_BPS = 200;
4240
4261
  declare function routeIsConsistent(route: Route, tokenIn: Address, tokenOut: Address): boolean;
4241
4262
  declare function phoenixCheck(pv: Preview, tokenIn: Address, tokenOut: Address): QuoteChecks;
4242
4263
 
4243
- 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';
4264
+ 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';
4244
4265
  declare class BlazeError extends Error {
4245
4266
  readonly code: BlazeErrorCode;
4246
4267
  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;
@@ -211,6 +212,15 @@ interface SwapRequest extends QuoteRequest {
211
212
  mode?: 'route' | 'best';
212
213
  /** Allowance to request when approval is needed: 'exact' (default) or 'max'. */
213
214
  approve?: 'exact' | 'max';
215
+ /**
216
+ * true — also read previewPlanExact (every concentrated leg dry-run) in the same block and
217
+ * derive minOut from its exactOut: a fixed 50 bps safety margin first, then slippageBps off
218
+ * that, never below the default minimum. The calldata still executes the route of the default
219
+ * path; the exact read must price that same route, or buildSwap refuses. A userMinOut above
220
+ * exactOut less the margin is refused (`min_above_exact`). One more eth_call; the swap reverts
221
+ * instead of filling short when the pool moves past the tolerance.
222
+ */
223
+ exact?: boolean;
214
224
  }
215
225
  interface ApprovalStep {
216
226
  token: Address;
@@ -237,6 +247,8 @@ interface SwapPlan {
237
247
  unwrapAfter: boolean;
238
248
  /** Ordered transactions to send: [approval?, swap]. */
239
249
  steps: TxRequest[];
250
+ /** With `exact: true`: the previewPlanExact output minOut was derived from. */
251
+ exactOut?: bigint;
240
252
  }
241
253
  interface SimulationResult {
242
254
  ok: boolean;
@@ -385,6 +397,12 @@ interface RegistryOptions {
385
397
  * delay a call, and by at most this much; later refreshes run in the
386
398
  * background (stale-while-revalidate). */
387
399
  timeoutMs?: number;
400
+ /** Route to addresses that only the fetched document supplies (a version
401
+ * released after this SDK). Default false: the document can list them, but
402
+ * resolve() only returns Router/Quoter addresses pinned in this package —
403
+ * the on-chain check of a remote entry reads the entry's own contracts, so
404
+ * it cannot tell a real deployment from a look-alike. */
405
+ remoteVersions?: boolean;
388
406
  /** Called for every pin conflict or rejected document — never silently. */
389
407
  onWarning?: (msg: string) => void;
390
408
  }
@@ -394,6 +412,7 @@ declare class Registry {
394
412
  private fetchedAt;
395
413
  private lastError?;
396
414
  private readonly mode;
415
+ private readonly remoteVersions;
397
416
  private readonly url;
398
417
  private readonly ttlMs;
399
418
  private readonly fetchFn?;
@@ -422,6 +441,8 @@ declare class Registry {
422
441
  /** Every deployed version on a chain, newest first. */
423
442
  versionsOn(chainId: SupportedChainId): string[];
424
443
  resolve(chainId: SupportedChainId, selector?: VersionSelector): ResolvedDeployment;
444
+ /** Router and Quoter both pinned in this package (not filled by a fetched document). */
445
+ private isPinned;
425
446
  private latestKnownVersion;
426
447
  /** chains × versions table (for dashboards, the MCP, debugging). */
427
448
  table(): {
@@ -4240,7 +4261,7 @@ declare const CAUTION_IMPACT_BPS = 200;
4240
4261
  declare function routeIsConsistent(route: Route, tokenIn: Address, tokenOut: Address): boolean;
4241
4262
  declare function phoenixCheck(pv: Preview, tokenIn: Address, tokenOut: Address): QuoteChecks;
4242
4263
 
4243
- 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';
4264
+ 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';
4244
4265
  declare class BlazeError extends Error {
4245
4266
  readonly code: BlazeErrorCode;
4246
4267
  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: {
@@ -5354,6 +5353,7 @@ var Registry = class {
5354
5353
  fetchedAt = 0;
5355
5354
  lastError;
5356
5355
  mode;
5356
+ remoteVersions;
5357
5357
  url;
5358
5358
  ttlMs;
5359
5359
  fetchFn;
@@ -5362,6 +5362,7 @@ var Registry = class {
5362
5362
  overrides = /* @__PURE__ */ new Map();
5363
5363
  constructor(opts = {}, overrides) {
5364
5364
  this.mode = opts.mode ?? "auto";
5365
+ this.remoteVersions = opts.remoteVersions ?? false;
5365
5366
  this.url = opts.url ?? DEPLOYMENTS_URL;
5366
5367
  this.ttlMs = opts.ttlMs ?? 10 * 6e4;
5367
5368
  this.fetchFn = opts.fetchFn ?? globalThis.fetch;
@@ -5493,7 +5494,7 @@ var Registry = class {
5493
5494
  if (!isValidSelector(selector)) throw new BlazeError("bad_request", `bad version selector "${selector}"`);
5494
5495
  const ov = this.overrides.get(chainId);
5495
5496
  const sel = ov?.version && selector === "latest" ? ov.version : selector;
5496
- const candidates = this.doc.versions.filter((v) => matchesSelector(v.version, sel) && isDeployed(v.chains[chainId])).sort((a, b) => compareVersions(b.version, a.version));
5497
+ 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));
5497
5498
  const pick = candidates[0];
5498
5499
  if (ov && (ov.router || ov.quoter)) {
5499
5500
  const base = pick ? { ...pick.chains[chainId] } : {
@@ -5513,9 +5514,10 @@ var Registry = class {
5513
5514
  }
5514
5515
  if (!pick) {
5515
5516
  const deployed = this.versionsOn(chainId);
5517
+ const remoteOnly = this.remoteVersions ? [] : deployed.filter((v) => matchesSelector(v, sel) && !this.isPinned(v, chainId));
5516
5518
  throw new BlazeError(
5517
5519
  "not_deployed",
5518
- `no BlazePhoenix deployment matching version "${selector}" on chain ${chainId}` + (deployed.length ? ` (deployed: ${deployed.join(", ")})` : ""),
5520
+ `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` : ""),
5519
5521
  { details: { chainId, selector, deployed } }
5520
5522
  );
5521
5523
  }
@@ -5524,6 +5526,11 @@ var Registry = class {
5524
5526
  const source = src("router") === "remote" || src("quoter") === "remote" ? "remote" : "embedded";
5525
5527
  return { chainId, version: pick.version, status: pick.status, contracts: { ...set }, source };
5526
5528
  }
5529
+ /** Router and Quoter both pinned in this package (not filled by a fetched document). */
5530
+ isPinned(version, chainId) {
5531
+ const src = (k) => this.fieldSource[`${version}|${chainId}|${k}`];
5532
+ return src("router") === "embedded" && src("quoter") === "embedded";
5533
+ }
5527
5534
  latestKnownVersion(selector) {
5528
5535
  const all = this.doc.versions.map((v) => v.version).sort((a, b) => compareVersions(b, a));
5529
5536
  const m = selector ? all.find((v) => matchesSelector(v, selector)) : void 0;
@@ -5771,16 +5778,19 @@ var BlazePhoenix = class {
5771
5778
  return { address: c.weth, native: true };
5772
5779
  case "WETH":
5773
5780
  return { address: c.weth, native: false };
5781
+ // `usdc` holds each chain's one dollar asset: USDG on Robinhood Chain, USDC
5782
+ // elsewhere. The other symbol names a different token there, so it is refused.
5774
5783
  case "USDC":
5775
5784
  case "USDG":
5776
- return { address: c.usdc, native: false };
5785
+ if (s.toUpperCase() === (chainId === 4663 ? "USDG" : "USDC")) return { address: c.usdc, native: false };
5786
+ break;
5777
5787
  case "BZPX":
5778
5788
  if (c.bzpx) return { address: c.bzpx, native: false };
5779
5789
  break;
5780
5790
  }
5781
5791
  throw new BlazeError(
5782
5792
  "bad_request",
5783
- `${side}: "${ref}" is not a 0x address or a built-in symbol (ETH, WETH, USDC, BZPX) on ${c.name} \u2014 pass the token address`
5793
+ `${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`
5784
5794
  );
5785
5795
  }
5786
5796
  async decimalsOf(client, chainId, t) {
@@ -6022,6 +6032,60 @@ var BlazePhoenix = class {
6022
6032
  let encodedBy = "sdk";
6023
6033
  let entry;
6024
6034
  let minOut;
6035
+ let at;
6036
+ if (req.exact) {
6037
+ try {
6038
+ at = await ctx.client.getBlockNumber();
6039
+ } catch (e) {
6040
+ this.wrapReadError(e, "block number");
6041
+ }
6042
+ }
6043
+ const pin = at !== void 0 ? { blockNumber: at } : {};
6044
+ let exactOut;
6045
+ const exactRead = async (route) => {
6046
+ let ex;
6047
+ try {
6048
+ ex = await ctx.client.readContract({
6049
+ address: ctx.dep.contracts.quoter,
6050
+ abi: QUOTER_ABI,
6051
+ functionName: "previewPlanExact",
6052
+ args: [tIn.address, tOut.address, amountIn],
6053
+ ...pin
6054
+ });
6055
+ } catch (e) {
6056
+ this.wrapReadError(e, "exact quote");
6057
+ }
6058
+ if (!samePath(ex[0], route)) {
6059
+ throw new BlazeError("calldata_mismatch", "previewPlanExact priced a different route than the one to execute");
6060
+ }
6061
+ exactOut = ex[1];
6062
+ return ex[1];
6063
+ };
6064
+ const settleMin = async (p) => {
6065
+ let floor = minOutFor(p.netOut, slippageBps, p.ironFloor);
6066
+ let ceil;
6067
+ if (req.exact) {
6068
+ const ex = await exactRead(p.route);
6069
+ ceil = minOutFor(ex, EXACT_MARGIN_BPS, 0n);
6070
+ floor = maxBig(floor, minOutFor(ceil, slippageBps, 0n));
6071
+ }
6072
+ if (userMinOut === 0n) return floor;
6073
+ if (userMinOut < floor) {
6074
+ throw new BlazeError(
6075
+ "min_implausible",
6076
+ `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`,
6077
+ { details: { userMinOut: userMinOut.toString(), floor: floor.toString() } }
6078
+ );
6079
+ }
6080
+ if (ceil !== void 0 && userMinOut > ceil && userMinOut > floor) {
6081
+ throw new BlazeError(
6082
+ "min_above_exact",
6083
+ `userMinOut is above ${ceil} (exactOut less ${EXACT_MARGIN_BPS} bps): an honest swap at this minimum may revert`,
6084
+ { details: { userMinOut: userMinOut.toString(), ceiling: ceil.toString() } }
6085
+ );
6086
+ }
6087
+ return userMinOut;
6088
+ };
6025
6089
  if (features.previewAndEncode && !tIn.native && mode === "route") {
6026
6090
  let call;
6027
6091
  try {
@@ -6029,19 +6093,21 @@ var BlazePhoenix = class {
6029
6093
  address: ctx.dep.contracts.quoter,
6030
6094
  abi: QUOTER_ABI,
6031
6095
  functionName: "previewAndEncodeWithMinOut",
6032
- args: [tIn.address, tOut.address, amountIn, userMinOut, recipient, deadline]
6096
+ args: [tIn.address, tOut.address, amountIn, userMinOut, recipient, deadline],
6097
+ ...pin
6033
6098
  }) : await ctx.client.readContract({
6034
6099
  address: ctx.dep.contracts.quoter,
6035
6100
  abi: QUOTER_ABI,
6036
6101
  functionName: "previewAndEncode",
6037
- args: [tIn.address, tOut.address, amountIn, recipient, deadline]
6102
+ args: [tIn.address, tOut.address, amountIn, recipient, deadline],
6103
+ ...pin
6038
6104
  });
6039
6105
  [pv, call] = r;
6040
6106
  } catch (e) {
6041
6107
  this.wrapReadError(e, "previewAndEncode");
6042
6108
  }
6043
6109
  assertExecutable(pv, tIn.address, tOut.address, call);
6044
- minOut = maxBig(minOutFor(pv.netOut, slippageBps, pv.effectiveMinOut), userMinOut);
6110
+ minOut = await settleMin(pv);
6045
6111
  verifySwapExactIn(call, {
6046
6112
  tokenIn: tIn.address,
6047
6113
  tokenOut: tOut.address,
@@ -6055,9 +6121,9 @@ var BlazePhoenix = class {
6055
6121
  encodedBy = "quoter";
6056
6122
  entry = "swapExactIn";
6057
6123
  } else {
6058
- ({ pv, fb } = await this.readPreview(ctx, tIn.address, tOut.address, amountIn, userMinOut));
6124
+ ({ pv, fb } = await this.readPreview(ctx, tIn.address, tOut.address, amountIn, userMinOut, at));
6059
6125
  assertExecutable(pv, tIn.address, tOut.address);
6060
- minOut = maxBig(minOutFor(pv.netOut, slippageBps, pv.effectiveMinOut), userMinOut);
6126
+ minOut = await settleMin(pv);
6061
6127
  if (mode === "best") {
6062
6128
  entry = "swapBestExactIn";
6063
6129
  data = encodeSwapBestExactIn(tIn.address, tOut.address, amountIn, minOut, recipient, deadline);
@@ -6104,7 +6170,8 @@ var BlazePhoenix = class {
6104
6170
  slippageBps,
6105
6171
  approval,
6106
6172
  unwrapAfter: tOut.native,
6107
- steps: approval ? [approval.tx, tx] : [tx]
6173
+ steps: approval ? [approval.tx, tx] : [tx],
6174
+ ...exactOut !== void 0 ? { exactOut } : {}
6108
6175
  };
6109
6176
  }
6110
6177
  /** eth_call the swap from `from` on your RPC: the realised amountOut if it
@@ -6170,6 +6237,12 @@ var BlazePhoenix = class {
6170
6237
  if (r.status !== "success") throw new BlazeError("reverted", `approval transaction reverted (${approvalHash})`);
6171
6238
  }
6172
6239
  plan = { ...plan, approval: { ...plan.approval, current: plan.approval.amount } };
6240
+ if (Date.now() - plan.quote.quotedAt > maxAge) {
6241
+ if (!req) {
6242
+ 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`);
6243
+ }
6244
+ plan = await this.buildSwap({ ...req, from: req.from ?? account.address });
6245
+ }
6173
6246
  }
6174
6247
  if (opts.simulate ?? true) {
6175
6248
  const sim = await this.simulate(plan, account.address);
@@ -6339,6 +6412,17 @@ function checkDeadlineSec(n) {
6339
6412
  return n;
6340
6413
  }
6341
6414
  var maxBig = (a, b) => a > b ? a : b;
6415
+ var EXACT_MARGIN_BPS = 50;
6416
+ function samePath(a, b) {
6417
+ const eqA = (x, y) => x.toLowerCase() === y.toLowerCase();
6418
+ return a.hops.length === b.hops.length && a.hops.every((h, i) => {
6419
+ const g = b.hops[i];
6420
+ return eqA(h.tokenIn, g.tokenIn) && eqA(h.tokenOut, g.tokenOut) && h.legs.length === g.legs.length && h.legs.every((l, j) => {
6421
+ const m = g.legs[j];
6422
+ 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;
6423
+ });
6424
+ });
6425
+ }
6342
6426
  function deepLink(opts = {}) {
6343
6427
  const base = (opts.baseUrl ?? "https://blazephoenix.xyz").replace(/\/+$/, "");
6344
6428
  const sp = new URLSearchParams();