@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/README.md CHANGED
@@ -13,6 +13,31 @@ npm i @blazephoenix/sdk viem
13
13
  # or straight from GitHub (builds on install): npm i github:blazephoenixxyz-crypto/SDK viem
14
14
  ```
15
15
 
16
+ ## BlazePhoenix in one sentence
17
+
18
+ BlazePhoenix is the fully on-chain DEX aggregator — the route is computed inside the transaction that executes it — paired with an oracle-free staking engine that proves its own solvency on every transaction. It runs on Base, Ethereum, Optimism, Arbitrum and Robinhood Chain.
19
+
20
+ **Swap** in the [app](https://blazephoenix.xyz/?tab=swap) · **integrate** with the [SDK](https://github.com/blazephoenixxyz-crypto/SDK) (`npm i @blazephoenix/sdk`) · **connect an agent** to the [MCP server](https://github.com/blazephoenixxyz-crypto/blazephoenix-mcp) (`https://blazephoenix.xyz/mcp`, no key)
21
+
22
+ ## Reading guide by level
23
+
24
+ - **New to DeFi:** [BlazePhoenix in five minutes](https://blazephoenix.xyz/learn/blazephoenix-in-five-minutes), then [What is a DEX aggregator](https://blazephoenix.xyz/learn/what-is-a-dex-aggregator) and [How to swap tokens safely](https://blazephoenix.xyz/learn/how-to-swap-safely).
25
+ - **Comfortable with swaps:** [Anatomy of a DEX aggregator](https://blazephoenix.xyz/learn/anatomy-of-an-aggregator) and [BlazePhoenix vs the established aggregators](https://blazephoenix.xyz/learn/how-we-compare).
26
+ - **Engineers and researchers:** the [Technical Whitepaper v2.3](https://blazephoenix.xyz/whitepaper.md) (DOI [10.5281/zenodo.23084091](https://doi.org/10.5281/zenodo.23084091)), the contract source in [Blaze-Phoenix-Dex](https://github.com/blazephoenixxyz-crypto/Blaze-Phoenix-Dex) and [Blaze-Phoenix-Staking](https://github.com/blazephoenixxyz-crypto/Blaze-Phoenix-Staking), and [On-chain quoting](https://blazephoenix.xyz/learn/on-chain-quoting).
27
+ - **Models and agents:** [https://blazephoenix.xyz/llms.txt](https://blazephoenix.xyz/llms.txt) and the [reading guide](https://blazephoenix.xyz/reading-guide.md).
28
+
29
+ ## Trust and verification
30
+
31
+ Each item links to the place where it can be checked. The full list is at [https://blazephoenix.xyz/trust](https://blazephoenix.xyz/trust).
32
+
33
+ - **Verified contracts:** source-verified on Sourcify and the chain explorers; every address per chain at [/api/deployments](https://blazephoenix.xyz/api/deployments).
34
+ - **Source available:** the contract source is public under the [Business Source License 1.1](https://github.com/blazephoenixxyz-crypto/Blaze-Phoenix-Dex/blob/main/LICENSE), converting to GPL-2.0-or-later on 2030-06-01.
35
+ - **Public CI:** [GitHub Actions](https://github.com/blazephoenixxyz-crypto/Blaze-Phoenix-Dex/actions) on every push, including the Certora Prover and Halmos.
36
+ - **Bug bounty:** a [public programme](https://blazephoenix.xyz/bounty) with [credited researchers](https://blazephoenix.xyz/security/researchers) and [published advisories](https://blazephoenix.xyz/security/advisories).
37
+ - **Whitepaper DOI:** [10.5281/zenodo.23084091](https://doi.org/10.5281/zenodo.23084091); papers timestamped with [OpenTimestamps](https://blazephoenix.xyz/provenance/provenance.json).
38
+ - **Contact:** contact@blazephoenix.xyz ([security.txt](https://blazephoenix.xyz/.well-known/security.txt)); official channels at [/official](https://blazephoenix.xyz/official).
39
+ - **Audit status:** an independent external audit is scheduled before launch.
40
+
16
41
  ## Quote → build → execute
17
42
 
18
43
  ```ts
@@ -33,9 +58,11 @@ const plan = await blaze.buildSwap({
33
58
  chain: 'base', tokenIn: 'WETH', tokenOut: 'USDC', amount: '1.5',
34
59
  recipient: me, from: me, // `from` enables the allowance check
35
60
  slippageBps: 50, // default 50; never below the on-chain floor
61
+ exact: true, // optional: minOut from previewPlanExact (one more eth_call)
36
62
  });
37
63
  plan.steps; // [approve?, swap] — { chainId, to, data, value }
38
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)
39
66
  plan.encodedBy; // 'quoter' on 2.x (the Quoter's own bytes, verified) · 'sdk' on 1.x
40
67
 
41
68
  // 3) optional: dry-run on your node, then execute with YOUR wallet
@@ -91,17 +118,20 @@ The SDK adapts to what each version can do (`featuresOf(version)`):
91
118
  | on-chain `batchQuote` | parallel previews | ✅ chunked |
92
119
  | `ExecutionProof` on every fill (quoted vs realised vs floor) | — | ✅ |
93
120
 
94
- ### New deployments reach you without a new SDK release
121
+ ### Only pinned addresses are routed to by default
95
122
 
96
123
  The registry has three layers:
97
124
 
98
125
  1. **Embedded** — shipped in the package. Non-zero addresses are **pins**: nothing fetched
99
- at runtime can move them.
126
+ at runtime can move them, and by default they are the only Router and Quoter addresses
127
+ a quote or a swap uses. A new deployment reaches you with a new SDK release.
100
128
  2. **Remote** — `GET https://blazephoenix.xyz/api/deployments` (static JSON, refreshed every
101
129
  10 min in the background; the first lookup waits at most 3 s). It can only fill what the
102
130
  snapshot doesn't know — e.g. the 2.0.0 addresses the day they are deployed — and only for
103
- versions that can be verified on-chain (2.x+). Every remote-sourced set is **verified on
104
- your RPC before first use**:
131
+ versions that can be verified on-chain (2.x+). Those versions are listed, but routed to
132
+ only with `registry: { remoteVersions: true }`: the on-chain check reads the entry's own
133
+ contracts, so it cannot tell a real deployment from a look-alike. With the opt-in, every
134
+ remote-sourced set is still **verified on your RPC before first use**:
105
135
  code at every address, `VERSION()` matches, and the Quoter, Router and Solver all point at
106
136
  the same Hub and Solver. Anything else fails closed (`deployment_unverified`).
107
137
  3. **Your overrides** — always win:
@@ -133,7 +163,7 @@ await blaze.quoteBatch([req1, req2]); // per-item { ok, quote } | { ok:false, er
133
163
  await blaze.getFills({ chain: 'base', lookbackBlocks: 5_000n }); // Swap (+ ExecutionProof on 2.x)
134
164
  const stop = await blaze.watchFills({ chain: 'base', onFill: console.log });
135
165
  await blaze.tokenInfo('base', '0x…'); // symbol / name / decimals
136
- 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
137
167
  toJSON(quote); // bigint → string, for HTTP / queues / LLMs
138
168
  ```
139
169
 
@@ -158,6 +188,8 @@ Every failure, `solvency()` included, is a `BlazeError` with a stable `code`; pr
158
188
  | `no_route` | the Solver found no executable path (`SolverE(5)`) or the quote reverted |
159
189
  | `not_executable` | the Quoter says the route cannot settle now (`canExecute = false`) |
160
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 |
161
193
  | `deployment_unverified` | a registry deployment failed on-chain verification |
162
194
  | `unsupported_by_version` | e.g. native ETH input on a 1.x Router |
163
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: {
@@ -5356,6 +5355,7 @@ var Registry = class {
5356
5355
  fetchedAt = 0;
5357
5356
  lastError;
5358
5357
  mode;
5358
+ remoteVersions;
5359
5359
  url;
5360
5360
  ttlMs;
5361
5361
  fetchFn;
@@ -5364,6 +5364,7 @@ var Registry = class {
5364
5364
  overrides = /* @__PURE__ */ new Map();
5365
5365
  constructor(opts = {}, overrides) {
5366
5366
  this.mode = opts.mode ?? "auto";
5367
+ this.remoteVersions = opts.remoteVersions ?? false;
5367
5368
  this.url = opts.url ?? DEPLOYMENTS_URL;
5368
5369
  this.ttlMs = opts.ttlMs ?? 10 * 6e4;
5369
5370
  this.fetchFn = opts.fetchFn ?? globalThis.fetch;
@@ -5495,7 +5496,7 @@ var Registry = class {
5495
5496
  if (!isValidSelector(selector)) throw new BlazeError("bad_request", `bad version selector "${selector}"`);
5496
5497
  const ov = this.overrides.get(chainId);
5497
5498
  const sel = ov?.version && selector === "latest" ? ov.version : selector;
5498
- const candidates = this.doc.versions.filter((v) => matchesSelector(v.version, sel) && isDeployed(v.chains[chainId])).sort((a, b) => compareVersions(b.version, a.version));
5499
+ 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));
5499
5500
  const pick = candidates[0];
5500
5501
  if (ov && (ov.router || ov.quoter)) {
5501
5502
  const base = pick ? { ...pick.chains[chainId] } : {
@@ -5515,9 +5516,10 @@ var Registry = class {
5515
5516
  }
5516
5517
  if (!pick) {
5517
5518
  const deployed = this.versionsOn(chainId);
5519
+ const remoteOnly = this.remoteVersions ? [] : deployed.filter((v) => matchesSelector(v, sel) && !this.isPinned(v, chainId));
5518
5520
  throw new BlazeError(
5519
5521
  "not_deployed",
5520
- `no BlazePhoenix deployment matching version "${selector}" on chain ${chainId}` + (deployed.length ? ` (deployed: ${deployed.join(", ")})` : ""),
5522
+ `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` : ""),
5521
5523
  { details: { chainId, selector, deployed } }
5522
5524
  );
5523
5525
  }
@@ -5526,6 +5528,11 @@ var Registry = class {
5526
5528
  const source = src("router") === "remote" || src("quoter") === "remote" ? "remote" : "embedded";
5527
5529
  return { chainId, version: pick.version, status: pick.status, contracts: { ...set }, source };
5528
5530
  }
5531
+ /** Router and Quoter both pinned in this package (not filled by a fetched document). */
5532
+ isPinned(version, chainId) {
5533
+ const src = (k) => this.fieldSource[`${version}|${chainId}|${k}`];
5534
+ return src("router") === "embedded" && src("quoter") === "embedded";
5535
+ }
5529
5536
  latestKnownVersion(selector) {
5530
5537
  const all = this.doc.versions.map((v) => v.version).sort((a, b) => compareVersions(b, a));
5531
5538
  const m = selector ? all.find((v) => matchesSelector(v, selector)) : void 0;
@@ -5773,16 +5780,19 @@ var BlazePhoenix = class {
5773
5780
  return { address: c.weth, native: true };
5774
5781
  case "WETH":
5775
5782
  return { address: c.weth, native: false };
5783
+ // `usdc` holds each chain's one dollar asset: USDG on Robinhood Chain, USDC
5784
+ // elsewhere. The other symbol names a different token there, so it is refused.
5776
5785
  case "USDC":
5777
5786
  case "USDG":
5778
- return { address: c.usdc, native: false };
5787
+ if (s.toUpperCase() === (chainId === 4663 ? "USDG" : "USDC")) return { address: c.usdc, native: false };
5788
+ break;
5779
5789
  case "BZPX":
5780
5790
  if (c.bzpx) return { address: c.bzpx, native: false };
5781
5791
  break;
5782
5792
  }
5783
5793
  throw new BlazeError(
5784
5794
  "bad_request",
5785
- `${side}: "${ref}" is not a 0x address or a built-in symbol (ETH, WETH, USDC, BZPX) on ${c.name} \u2014 pass the token address`
5795
+ `${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`
5786
5796
  );
5787
5797
  }
5788
5798
  async decimalsOf(client, chainId, t) {
@@ -6024,6 +6034,60 @@ var BlazePhoenix = class {
6024
6034
  let encodedBy = "sdk";
6025
6035
  let entry;
6026
6036
  let minOut;
6037
+ let at;
6038
+ if (req.exact) {
6039
+ try {
6040
+ at = await ctx.client.getBlockNumber();
6041
+ } catch (e) {
6042
+ this.wrapReadError(e, "block number");
6043
+ }
6044
+ }
6045
+ const pin = at !== void 0 ? { blockNumber: at } : {};
6046
+ let exactOut;
6047
+ const exactRead = async (route) => {
6048
+ let ex;
6049
+ try {
6050
+ ex = await ctx.client.readContract({
6051
+ address: ctx.dep.contracts.quoter,
6052
+ abi: QUOTER_ABI,
6053
+ functionName: "previewPlanExact",
6054
+ args: [tIn.address, tOut.address, amountIn],
6055
+ ...pin
6056
+ });
6057
+ } catch (e) {
6058
+ this.wrapReadError(e, "exact quote");
6059
+ }
6060
+ if (!samePath(ex[0], route)) {
6061
+ throw new BlazeError("calldata_mismatch", "previewPlanExact priced a different route than the one to execute");
6062
+ }
6063
+ exactOut = ex[1];
6064
+ return ex[1];
6065
+ };
6066
+ const settleMin = async (p) => {
6067
+ let floor = minOutFor(p.netOut, slippageBps, p.ironFloor);
6068
+ let ceil;
6069
+ if (req.exact) {
6070
+ const ex = await exactRead(p.route);
6071
+ ceil = minOutFor(ex, EXACT_MARGIN_BPS, 0n);
6072
+ floor = maxBig(floor, minOutFor(ceil, slippageBps, 0n));
6073
+ }
6074
+ if (userMinOut === 0n) return floor;
6075
+ if (userMinOut < floor) {
6076
+ throw new BlazeError(
6077
+ "min_implausible",
6078
+ `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`,
6079
+ { details: { userMinOut: userMinOut.toString(), floor: floor.toString() } }
6080
+ );
6081
+ }
6082
+ if (ceil !== void 0 && userMinOut > ceil && userMinOut > floor) {
6083
+ throw new BlazeError(
6084
+ "min_above_exact",
6085
+ `userMinOut is above ${ceil} (exactOut less ${EXACT_MARGIN_BPS} bps): an honest swap at this minimum may revert`,
6086
+ { details: { userMinOut: userMinOut.toString(), ceiling: ceil.toString() } }
6087
+ );
6088
+ }
6089
+ return userMinOut;
6090
+ };
6027
6091
  if (features.previewAndEncode && !tIn.native && mode === "route") {
6028
6092
  let call;
6029
6093
  try {
@@ -6031,19 +6095,21 @@ var BlazePhoenix = class {
6031
6095
  address: ctx.dep.contracts.quoter,
6032
6096
  abi: QUOTER_ABI,
6033
6097
  functionName: "previewAndEncodeWithMinOut",
6034
- args: [tIn.address, tOut.address, amountIn, userMinOut, recipient, deadline]
6098
+ args: [tIn.address, tOut.address, amountIn, userMinOut, recipient, deadline],
6099
+ ...pin
6035
6100
  }) : await ctx.client.readContract({
6036
6101
  address: ctx.dep.contracts.quoter,
6037
6102
  abi: QUOTER_ABI,
6038
6103
  functionName: "previewAndEncode",
6039
- args: [tIn.address, tOut.address, amountIn, recipient, deadline]
6104
+ args: [tIn.address, tOut.address, amountIn, recipient, deadline],
6105
+ ...pin
6040
6106
  });
6041
6107
  [pv, call] = r;
6042
6108
  } catch (e) {
6043
6109
  this.wrapReadError(e, "previewAndEncode");
6044
6110
  }
6045
6111
  assertExecutable(pv, tIn.address, tOut.address, call);
6046
- minOut = maxBig(minOutFor(pv.netOut, slippageBps, pv.effectiveMinOut), userMinOut);
6112
+ minOut = await settleMin(pv);
6047
6113
  verifySwapExactIn(call, {
6048
6114
  tokenIn: tIn.address,
6049
6115
  tokenOut: tOut.address,
@@ -6057,9 +6123,9 @@ var BlazePhoenix = class {
6057
6123
  encodedBy = "quoter";
6058
6124
  entry = "swapExactIn";
6059
6125
  } else {
6060
- ({ pv, fb } = await this.readPreview(ctx, tIn.address, tOut.address, amountIn, userMinOut));
6126
+ ({ pv, fb } = await this.readPreview(ctx, tIn.address, tOut.address, amountIn, userMinOut, at));
6061
6127
  assertExecutable(pv, tIn.address, tOut.address);
6062
- minOut = maxBig(minOutFor(pv.netOut, slippageBps, pv.effectiveMinOut), userMinOut);
6128
+ minOut = await settleMin(pv);
6063
6129
  if (mode === "best") {
6064
6130
  entry = "swapBestExactIn";
6065
6131
  data = encodeSwapBestExactIn(tIn.address, tOut.address, amountIn, minOut, recipient, deadline);
@@ -6106,7 +6172,8 @@ var BlazePhoenix = class {
6106
6172
  slippageBps,
6107
6173
  approval,
6108
6174
  unwrapAfter: tOut.native,
6109
- steps: approval ? [approval.tx, tx] : [tx]
6175
+ steps: approval ? [approval.tx, tx] : [tx],
6176
+ ...exactOut !== void 0 ? { exactOut } : {}
6110
6177
  };
6111
6178
  }
6112
6179
  /** eth_call the swap from `from` on your RPC: the realised amountOut if it
@@ -6172,6 +6239,12 @@ var BlazePhoenix = class {
6172
6239
  if (r.status !== "success") throw new BlazeError("reverted", `approval transaction reverted (${approvalHash})`);
6173
6240
  }
6174
6241
  plan = { ...plan, approval: { ...plan.approval, current: plan.approval.amount } };
6242
+ if (Date.now() - plan.quote.quotedAt > maxAge) {
6243
+ if (!req) {
6244
+ 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`);
6245
+ }
6246
+ plan = await this.buildSwap({ ...req, from: req.from ?? account.address });
6247
+ }
6175
6248
  }
6176
6249
  if (opts.simulate ?? true) {
6177
6250
  const sim = await this.simulate(plan, account.address);
@@ -6341,6 +6414,17 @@ function checkDeadlineSec(n) {
6341
6414
  return n;
6342
6415
  }
6343
6416
  var maxBig = (a, b) => a > b ? a : b;
6417
+ var EXACT_MARGIN_BPS = 50;
6418
+ function samePath(a, b) {
6419
+ const eqA = (x, y) => x.toLowerCase() === y.toLowerCase();
6420
+ return a.hops.length === b.hops.length && a.hops.every((h, i) => {
6421
+ const g = b.hops[i];
6422
+ return eqA(h.tokenIn, g.tokenIn) && eqA(h.tokenOut, g.tokenOut) && h.legs.length === g.legs.length && h.legs.every((l, j) => {
6423
+ const m = g.legs[j];
6424
+ 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;
6425
+ });
6426
+ });
6427
+ }
6344
6428
  function deepLink(opts = {}) {
6345
6429
  const base = (opts.baseUrl ?? "https://blazephoenix.xyz").replace(/\/+$/, "");
6346
6430
  const sp = new URLSearchParams();