@zypheragent/mcp 0.1.0 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +63 -20
  2. package/dist/index.js +934 -2921
  3. package/package.json +27 -7
package/dist/index.js CHANGED
@@ -8,22 +8,46 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
8
8
  import { z } from "zod";
9
9
 
10
10
  // src/shared/hl.ts
11
- var HL_API = { testnet: "https://api.hyperliquid-testnet.xyz", mainnet: "https://api.hyperliquid.xyz" };
11
+ var HL_API = { mainnet: "https://api.hyperliquid.xyz" };
12
12
  async function hlInfo(net, body) {
13
13
  const r = await fetch(`${HL_API[net]}/info`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
14
14
  if (!r.ok) throw new Error(`Hyperliquid info ${r.status}`);
15
15
  return r.json();
16
16
  }
17
17
  var n = (x) => x == null || x === "" ? NaN : Number(x);
18
- async function getPerpMarkets(net) {
19
- const [meta, ctxs] = await hlInfo(net, { type: "metaAndAssetCtxs" });
18
+ var assetId = (index, dexOrdinal = 0) => dexOrdinal > 0 ? 11e4 + (dexOrdinal - 1) * 1e4 + index : index;
19
+ var HL_BUILDER_DEXES = ["xyz"];
20
+ function normCoin(t) {
21
+ const s = String(t ?? "").trim();
22
+ const i = s.indexOf(":");
23
+ return i > 0 ? `${s.slice(0, i).toLowerCase()}:${s.slice(i + 1).toUpperCase()}` : s.toUpperCase();
24
+ }
25
+ var coinSymbol = (coin) => String(coin).replace(/^[a-z0-9]+:/i, "");
26
+ async function getPerpDexOrdinals(net) {
27
+ const out = {};
28
+ try {
29
+ const list = await hlInfo(net, { type: "perpDexs" });
30
+ if (Array.isArray(list)) list.forEach((d, i) => {
31
+ if (d && typeof d.name === "string" && i > 0) out[d.name] = i;
32
+ });
33
+ } catch {
34
+ }
35
+ for (const d of HL_BUILDER_DEXES) if (!out[d]) out[d] = 1;
36
+ return out;
37
+ }
38
+ function mapMarkets(meta, ctxs, dex, dexOrdinal) {
20
39
  return meta.universe.flatMap((u, index) => {
21
40
  if (u.isDelisted) return [];
22
41
  const c = ctxs[index] ?? {};
23
42
  const mark = n(c.markPx), prev = n(c.prevDayPx);
24
43
  return [{
25
- index,
26
44
  name: u.name,
45
+ symbol: coinSymbol(u.name),
46
+ dex,
47
+ dexOrdinal,
48
+ book: dex ? "trade.xyz" : "hyperliquid",
49
+ index,
50
+ asset: assetId(index, dexOrdinal),
27
51
  szDecimals: u.szDecimals,
28
52
  maxLeverage: u.maxLeverage,
29
53
  markPx: mark,
@@ -34,10 +58,23 @@ async function getPerpMarkets(net) {
34
58
  volume24hUsd: n(c.dayNtlVlm),
35
59
  openInterestUsd: n(c.openInterest) * mark,
36
60
  fundingHourly: n(c.funding),
37
- onlyIsolated: Boolean(u.onlyIsolated)
61
+ onlyIsolated: Boolean(u.onlyIsolated) || u.marginMode === "strictIsolated" || u.marginMode === "noCross"
38
62
  }];
39
63
  });
40
64
  }
65
+ async function getPerpMarkets(net) {
66
+ const [base, ordinals] = await Promise.all([hlInfo(net, { type: "metaAndAssetCtxs" }), getPerpDexOrdinals(net)]);
67
+ const out = mapMarkets(base[0], base[1], "", 0);
68
+ const extra = await Promise.all(HL_BUILDER_DEXES.map(async (dex) => {
69
+ try {
70
+ const [meta, ctxs] = await hlInfo(net, { type: "metaAndAssetCtxs", dex });
71
+ return Array.isArray(meta?.universe) ? mapMarkets(meta, ctxs ?? [], dex, ordinals[dex]) : [];
72
+ } catch {
73
+ return [];
74
+ }
75
+ }));
76
+ return out.concat(...extra);
77
+ }
41
78
  var INTERVAL_MS = { "1m": 6e4, "5m": 3e5, "15m": 9e5, "1h": 36e5, "4h": 144e5, "1d": 864e5 };
42
79
  async function getCandles(net, coin, interval, count = 300) {
43
80
  const end = Date.now();
@@ -82,14 +119,14 @@ var env = {
82
119
  get xApiBase() {
83
120
  return DEFAULT_X_API_BASE;
84
121
  },
85
- get elysiumRpc() {
86
- return e.ELYSIUM_RPC?.trim() || "https://testnet-rpc.elysium.kinetiq.xyz";
122
+ get elysiumMainnetRpc() {
123
+ return e.ELYSIUM_RPC?.trim() || "https://rpc-live.boxa.club";
87
124
  },
88
125
  get hyperevmMainnetRpc() {
89
126
  return e.HYPEREVM_RPC?.trim() || "https://rpc.hyperliquid.xyz/evm";
90
127
  },
91
128
  get network() {
92
- return e.ZYPHER_NETWORK?.trim().toLowerCase() === "mainnet" ? "mainnet" : "testnet";
129
+ return "mainnet";
93
130
  },
94
131
  get address() {
95
132
  return e.ZYPHER_ADDRESS?.trim() || void 0;
@@ -114,9 +151,16 @@ async function info(body, url = DATA()) {
114
151
  return r.json();
115
152
  }
116
153
  var n2 = (x) => x == null ? null : Number(x);
154
+ async function findMarket(coin) {
155
+ const all = await getPerpMarkets(env.network), q = normCoin(coin).toLowerCase();
156
+ const m = all.find((x) => x.name.toLowerCase() === q) ?? all.find((x) => x.symbol.toLowerCase() === q);
157
+ if (!m) throw new Error(`Ticker ${coin.slice(0, 24)} not found in Hyperliquid perps (own dex or trade.xyz)`);
158
+ return m;
159
+ }
117
160
  async function getTicker(coin) {
118
- const [meta, ctxs] = await info({ type: "metaAndAssetCtxs" });
119
- const i = meta.universe.findIndex((u) => u.name.toUpperCase() === coin.toUpperCase());
161
+ const m = await findMarket(coin);
162
+ const [meta, ctxs] = await info({ type: "metaAndAssetCtxs", ...m.dex ? { dex: m.dex } : {} });
163
+ const i = meta.universe.findIndex((u) => u.name === m.name);
120
164
  if (i < 0) throw new Error(`Ticker ${coin} not found in Hyperliquid perps (${env.network})`);
121
165
  const c = ctxs[i];
122
166
  const book = await info({ type: "l2Book", coin: meta.universe[i].name });
@@ -140,17 +184,12 @@ async function getTicker(coin) {
140
184
  };
141
185
  }
142
186
  async function topVolume(limit) {
143
- const [meta, ctxs] = await info({ type: "metaAndAssetCtxs" });
144
- return meta.universe.map((u, i) => {
145
- const c = ctxs[i];
146
- const oiUsd = Number(c.openInterest) * Number(c.markPx);
147
- return {
148
- ticker: u.name,
149
- volume24hUsd: Number(c.dayNtlVlm),
150
- volumeToOI: oiUsd > 0 ? Number(c.dayNtlVlm) / oiUsd : null,
151
- change24hPct: Number(c.prevDayPx) ? (Number(c.markPx) - Number(c.prevDayPx)) / Number(c.prevDayPx) * 100 : null
152
- };
153
- }).sort((a, b) => b.volume24hUsd - a.volume24hUsd).slice(0, limit);
187
+ return (await getPerpMarkets(env.network)).map((m) => ({
188
+ ticker: m.name,
189
+ volume24hUsd: m.volume24hUsd,
190
+ volumeToOI: m.openInterestUsd > 0 ? m.volume24hUsd / m.openInterestUsd : null,
191
+ change24hPct: m.change24hPct
192
+ })).sort((a, b) => b.volume24hUsd - a.volume24hUsd).slice(0, limit);
154
193
  }
155
194
  async function getAccount(address) {
156
195
  const [perp, spot] = await Promise.all([
@@ -171,79 +210,155 @@ async function getAccount(address) {
171
210
  };
172
211
  }
173
212
 
174
- // src/shared/util.ts
175
- var same = (a, b) => a.toLowerCase() === b.toLowerCase();
176
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
177
- var INVISIBLE = /[\u0000-\u001f\u007f-\u009f\u00ad\u034f\u061c\u115f\u1160\u17b4\u17b5\u180b-\u180f\u200b-\u200f\u2028-\u202e\u2060-\u206f\u3164\ufeff\uffa0\ufff9-\ufffb]|[\u{e0000}-\u{e007f}]/gu;
178
- function cleanLabel(v, max) {
179
- const s = String(v ?? "").replace(INVISIBLE, " ").replace(/\s+/g, " ").trim();
180
- const cp = Array.from(s);
181
- return cp.length > max ? cp.slice(0, max).join("").trim() : s;
182
- }
213
+ // src/providers/chains.ts
214
+ import { createPublicClient, defineChain as defineChain3, erc20Abi, formatUnits as formatUnits2, getAddress as getAddress3, http, isAddress, keccak256, parseAbi, toHex } from "viem";
183
215
 
184
- // src/shared/elysium/chain.ts
185
- import { defineChain, formatUnits, parseUnits } from "viem";
186
- var ELYSIUM_TESTNET_ID = 99801;
187
- var ELYSIUM_RPC = "https://testnet-rpc.elysium.kinetiq.xyz";
188
- var ELYSIUM_EXPLORER = "https://testnet.elysium.kinetiq.xyz";
189
- var MULTICALL3 = "0xcA11bde05977b3631167028862bE2a173976CA11";
190
- var elysiumTestnet = defineChain({
191
- id: ELYSIUM_TESTNET_ID,
192
- name: "Elysium Testnet",
216
+ // src/shared/hyperswap/chain.ts
217
+ import { defineChain, formatUnits, getAddress } from "viem";
218
+ var A = (s) => getAddress(s.toLowerCase());
219
+ var HYPEREVM_MAINNET_ID = 999;
220
+ var HYPEREVM_MAINNET_RPC = "https://rpc.hyperliquid.xyz/evm";
221
+ var HYPEREVM_MAINNET_EXPLORER = "https://hyperevmscan.io";
222
+ var MAINNET_MULTICALL3 = A("0xcA11bde05977b3631167028862bE2a173976CA11");
223
+ var hyperEvmMainnet = defineChain({
224
+ id: HYPEREVM_MAINNET_ID,
225
+ name: "HyperEVM",
193
226
  nativeCurrency: { name: "HYPE", symbol: "HYPE", decimals: 18 },
194
- rpcUrls: { default: { http: [ELYSIUM_RPC] } },
195
- blockExplorers: { default: { name: "Elysium Explorer", url: ELYSIUM_EXPLORER } },
196
- contracts: { multicall3: { address: MULTICALL3 } }
227
+ rpcUrls: { default: { http: [HYPEREVM_MAINNET_RPC] } },
228
+ blockExplorers: { default: { name: "HyperEVMScan", url: HYPEREVM_MAINNET_EXPLORER } },
229
+ contracts: { multicall3: { address: MAINNET_MULTICALL3 } }
197
230
  });
198
- var DEX = {
199
- name: "Dex Da Costa",
200
- note: "Independent third-party Uniswap V2-style DEX on Elysium testnet",
201
- router: "0x72D435b8030A5195Bdb7594EDEa1ff315B9994c7",
202
- factory: "0xFa456ad175F0bdB9285960720B4b6e8f25dEb3f9",
203
- wrappedNative: "0xfF5681eB95bBaCfbB334F80fCDBc31fB59F22332"
204
- // router.WETH(); NOT the canonical WHYPE
231
+ var HYPERSWAP = {
232
+ name: "HyperSwap V3",
233
+ factory: A("0xB1c0fa0B789320044A6F623cFe5eBda9562602E3"),
234
+ quoter: A("0x03A918028f22D9E1473B7959C927AD7425A45C7C"),
235
+ /** The ONLY contract a mainnet swap may call (SwapRouter02: exactInput* have no deadline field; multicall(deadline, data[]) carries it). */
236
+ router: A("0x6D99e7f6747AF2cDbB5164b6DD50e40D4fDe1e77"),
237
+ v2Factory: A("0x724412c00059bf7d6ee7d4a1d0d5cd4de3ea1c48"),
238
+ /** Listed by HyperSwap but NOT used by this app (old router / liquidity NFT manager). */
239
+ unused: { routerV1: A("0x4E2960a8cd19B467b82d26D83fAcb0fAE26b094D"), positionManager: A("0x6eDA206207c09e5428F281761DdC0D300851fBC8") }
205
240
  };
206
- var CANONICAL = {
207
- USDC: "0x7D29d8047B905000459c0E80c34a26CeedcB47b2",
208
- WHYPE: "0xCD57F65c2B0e5881CFC2E609F7CD53b746E1F234"
209
- };
210
- var NATIVE = { address: "native", symbol: "HYPE", decimals: 18 };
211
- var parseAmount = (s, decimals) => parseUnits(s, decimals);
212
- var fmtAmount = (raw, decimals, maxFrac = 6) => {
241
+ var FEE_TIERS = [100, 500, 3e3, 1e4];
242
+ var WHYPE = A("0x5555555555555555555555555555555555555555");
243
+ var MAINNET_TOKENS = [
244
+ { address: "native", symbol: "HYPE", name: "HYPE (native gas)", decimals: 18, verified: true },
245
+ { address: WHYPE, symbol: "WHYPE", name: "Wrapped HYPE", decimals: 18, verified: true },
246
+ { address: A("0xb88339CB7199b77E23DB6E890353E22632Ba630f"), symbol: "USDC", name: "USD Coin", decimals: 6, verified: true, stable: true },
247
+ { address: A("0xb8ce59fc3717ada4c02eadf9682a9e934f625ebb"), symbol: "USD\u20AE0", name: "USDT0", decimals: 6, verified: true, stable: true },
248
+ { address: A("0xbe6727b535545c67d5caa73dea54865b92cf7907"), symbol: "UETH", name: "Unit Ethereum", decimals: 18, verified: true },
249
+ { address: A("0x5d3a1ff2b6bab83b63cd9ad0787074081a52ef34"), symbol: "USDe", name: "USDe", decimals: 18, verified: true, stable: true },
250
+ { address: A("0x111111a1a0667d36bd57c0a9f569b98057111111"), symbol: "USDH", name: "USDH", decimals: 6, verified: true, stable: true },
251
+ { address: A("0xbf2d3b1a37d54ce86d0e1455884da875a97c87a8"), symbol: "USDT", name: "Tether USD", decimals: 6, verified: true, stable: true },
252
+ { address: A("0xab11329560fa9c9c860bb21a9342215a1265bbb0"), symbol: "APE", name: "ApeCoin", decimals: 18, verified: true }
253
+ ];
254
+ var low = (s) => s.toLowerCase();
255
+ var USDC = MAINNET_TOKENS[2];
256
+ var tokenBy = (a) => MAINNET_TOKENS.find((t) => low(t.address) === low(a));
257
+ var isNative = (t) => t.address === "native";
258
+ var wrapped = (t) => isNative(t) ? WHYPE : t.address;
259
+ var sameAddr = (a, b) => low(a) === low(b);
260
+ var explorerToken = (a) => `${HYPEREVM_MAINNET_EXPLORER}/token/${a}`;
261
+ var fmtRaw = (raw, decimals, maxFrac = 6) => {
213
262
  const s = formatUnits(raw, decimals);
214
263
  const [i, f = ""] = s.split(".");
215
264
  return f ? `${i}.${f.slice(0, maxFrac)}`.replace(/\.?0+$/, "") || i : i;
216
265
  };
217
- var explorerToken = (a) => `${ELYSIUM_EXPLORER}/token/${a}`;
218
266
 
219
- // src/providers/elysium.ts
220
- import { createPublicClient, defineChain as defineChain2, erc20Abi, formatUnits as formatUnits2, getAddress, http, isAddress, parseAbi } from "viem";
221
- var elysiumTestnet2 = defineChain2({ ...elysiumTestnet, rpcUrls: { default: { http: [env.elysiumRpc] } } });
222
- var hyperEvm = defineChain2({
223
- id: 999,
224
- name: "HyperEVM",
267
+ // src/shared/boxa/chain.ts
268
+ import { defineChain as defineChain2, getAddress as getAddress2 } from "viem";
269
+ var A2 = (s) => getAddress2(s.toLowerCase());
270
+ var ELYSIUM_MAINNET_ID = 99901;
271
+ var ELYSIUM_MAINNET_RPC = "https://rpc-live.boxa.club";
272
+ var elysiumMainnet = defineChain2({
273
+ id: ELYSIUM_MAINNET_ID,
274
+ name: "Elysium",
225
275
  nativeCurrency: { name: "HYPE", symbol: "HYPE", decimals: 18 },
226
- rpcUrls: { default: { http: [env.hyperevmMainnetRpc] } }
276
+ rpcUrls: { default: { http: [ELYSIUM_MAINNET_RPC] } }
227
277
  });
228
- var client = (c) => createPublicClient({ chain: c === "hyperevm" ? hyperEvm : elysiumTestnet2, transport: http() });
229
- async function nativeBalance(address, chain2 = "elysium-testnet") {
278
+ var BOXA = {
279
+ name: "Boxa V3 launcher (Uniswap v3 pool, locked liquidity)",
280
+ launcher: A2("0x8ca88b12bc89b58034c583f1a5df02f7e67d62e1"),
281
+ locker: A2("0xdf7f5605007d27a73e0651a520eb4cda8d2e992b"),
282
+ feeManager: A2("0x22d6bb457eb617872068e279c4a0b5c364673539"),
283
+ factory: A2("0xd5a7c10820446c58868a204b8979b8cc9cb2a1a3"),
284
+ positionManager: A2("0xc4e2781b7be7d5c61f45774a9628fa0e66218afa"),
285
+ router: A2("0x03c820b17f17ea86449f6cf700788c97511566c4"),
286
+ quoterV2: A2("0x4f7dc08e8cc4579763bdec97dd9753086e4f1e3f"),
287
+ whype: A2("0xba876237b0f021a6c6d852d6c26e107997cc5e81"),
288
+ multicall: A2("0xc4add687c8f1e3c4581f4badd0081aada28f7f11"),
289
+ /** locker.treasury() / feeManager.treasury() on chain (the wallet that receives the treasury share of trading fees). */
290
+ treasury: A2("0xda49456ef572d0418ae8d6abb7cb412881c33191")
291
+ };
292
+ var CODE_HASH = {
293
+ launcher: "0x0c714ea8453af368716633247b2464af744af1c80a671010432b49b718bb8da7",
294
+ locker: "0x6db135007df83fc15c02bea66a07babf77f9fc7780c630dae5b7b771010cb091",
295
+ feeManager: "0x6f9ce12facc8126ad76b25302d6f82c002672094b632ed56b6a8e62872e1c9bc",
296
+ factory: "0xd4e18e2ba5c215d6eb63742c6e0ed9427c0581ce0724ad43e60969cc6f7e6f41",
297
+ positionManager: "0x684881e1555e1258792877cd57a661e2edcdd746ad44d2590c27f45bf8fe9b23",
298
+ router: "0x6f4525d1f1b1b7c2218f409c1bb717cc6a7d3f8195eab369059d73f4a75ca126",
299
+ whype: "0x5706be52f64875fee65a2cec0d80e47a23d8793cbe85d214b48445e2d05f5353",
300
+ quoterV2: "0x75637570f7c976c0a0f3e96c622c85d253a5ecaa5d994f172fec81c107ec5ddc"
301
+ };
302
+ var LAUNCH_SET = ["launcher", "locker", "feeManager", "factory", "positionManager", "router", "whype"];
303
+ var PARENT = {
304
+ inbox: A2("0xce59f8b65ad41c9dae4c07a0571a39824bd92cff"),
305
+ bridge: A2("0x503d572157113faaad4dbed2647bb10b949216b6"),
306
+ sequencerInbox: A2("0x7f3ff53aecb87261206ad3bdc27a88b6d8420fa6"),
307
+ rollup: A2("0x508c173dfe5f737b3493195bf785d71fa76fd217"),
308
+ outbox: A2("0xec6b106dfa14428fe5eec2a4065a7be599f02728")
309
+ };
310
+ var FACTS = {
311
+ /** launcher.FEE(): Uniswap v3 pool fee in 1/1_000_000: 10000 = 1% on every buy and sell. */
312
+ poolFee: 1e4,
313
+ /** launcher.INITIAL_SUPPLY(): 1,000,000,000 tokens (18 decimals), ALL placed in the pool. */
314
+ initialSupply: 1000000000n * 10n ** 18n,
315
+ /** locker.TREASURY_BPS(): the treasury share of the native HYPE trading fees. Observed on chain: every non-zero FeesCollected event has treasuryCredit = exactly 30.00% of nativeFees. */
316
+ treasuryBps: 3e3,
317
+ /** locker.MAX_POOL_SPEND_BPS() */
318
+ maxPoolSpendBps: 100,
319
+ /** launcher.MAX_LAUNCH_DUST() */
320
+ maxLaunchDust: 1000000000000n,
321
+ /** BoxaToken anti-snipe: while on, one buy is capped at 1% of supply for 60 seconds (Boxa docs). */
322
+ antiSnipeCapBps: 100,
323
+ antiSnipeSeconds: 60
324
+ };
325
+ var MAX_FEE_PER_GAS_WEI = 5n * 10n ** 9n;
326
+ var MAX_PARENT_GAS_PRICE_WEI = 20n * 10n ** 9n;
327
+
328
+ // src/shared/util.ts
329
+ var INVISIBLE = /[\u0000-\u001f\u007f-\u009f\u00ad\u034f\u061c\u115f\u1160\u17b4\u17b5\u180b-\u180f\u200b-\u200f\u2028-\u202e\u2060-\u206f\u3164\ufeff\uffa0\ufff9-\ufffb]|[\u{e0000}-\u{e007f}]/gu;
330
+ function cleanLabel(v, max) {
331
+ const s = String(v ?? "").replace(INVISIBLE, " ").replace(/\s+/g, " ").trim();
332
+ const cp = Array.from(s);
333
+ return cp.length > max ? cp.slice(0, max).join("").trim() : s;
334
+ }
335
+
336
+ // src/providers/chains.ts
337
+ var hyper = defineChain3({ ...hyperEvmMainnet, rpcUrls: { default: { http: [env.hyperevmMainnetRpc] } } });
338
+ var ely = defineChain3({ ...elysiumMainnet, rpcUrls: { default: { http: [env.elysiumMainnetRpc] } } });
339
+ var hc = null;
340
+ var ec = null;
341
+ var hyperClient = () => hc ??= createPublicClient({ chain: hyper, transport: http(void 0, { batch: true }), batch: { multicall: { batchSize: 48e3, wait: 8 } } });
342
+ var elysiumClient = () => ec ??= createPublicClient({ chain: ely, transport: http() });
343
+ var clientOf = (c) => c === "elysium" ? elysiumClient() : hyperClient();
344
+ var chainNote = (c) => c === "elysium" ? "Elysium (chain 99901), a community-run chain served by Boxa; NOT the official Kinetiq mainnet" : "HyperEVM mainnet (chain 999)";
345
+ async function nativeBalance(address, chain2) {
230
346
  if (!isAddress(address)) throw new Error("Invalid EVM address");
231
- const wei = await client(chain2).getBalance({ address: getAddress(address) });
232
- return { chain: chain2, hype: formatUnits2(wei, 18) };
347
+ const wei = await clientOf(chain2).getBalance({ address: getAddress3(address) });
348
+ return { chain: chain2, network: chainNote(chain2), hype: formatUnits2(wei, 18) };
233
349
  }
234
- var RISK_SELECTORS = {
235
- mint: { sel: "40c10f19", penalty: 25, why: "Has mint(address,uint256): supply can increase" },
236
- pause: { sel: "8456cb59", penalty: 10, why: "Has pause(): transfers can be frozen" },
237
- blacklist: { sel: "f9f92be4", penalty: 15, why: "Has blacklist(address): addresses can be blocked" }
238
- };
350
+ var RISK = [
351
+ { sel: "40c10f19", penalty: 25, why: "Has mint(address,uint256): supply can increase" },
352
+ { sel: "8456cb59", penalty: 10, why: "Has pause(): transfers can be frozen" },
353
+ { sel: "f9f92be4", penalty: 15, why: "Has blacklist(address): addresses can be blocked" }
354
+ ];
239
355
  var ownableAbi = parseAbi(["function owner() view returns (address)"]);
240
356
  var ZERO = "0x0000000000000000000000000000000000000000";
241
357
  async function tokenHealth(addr, chain2) {
242
358
  if (!isAddress(addr)) throw new Error("Invalid token address");
243
- const address = getAddress(addr);
244
- const c = client(chain2);
359
+ const address = getAddress3(addr), c = clientOf(chain2);
245
360
  const code = await c.getCode({ address });
246
- if (!code || code === "0x") return { address, chain: chain2, score: 0, verdict: "invalid", reasons: ["No contract at this address"] };
361
+ if (!code || code === "0x") return { address, chain: chain2, network: chainNote(chain2), score: 0, verdict: "invalid", reasons: ["No contract at this address"] };
247
362
  const safe = async (fn) => {
248
363
  try {
249
364
  return await fn();
@@ -261,11 +376,9 @@ async function tokenHealth(addr, chain2) {
261
376
  let score = 100;
262
377
  const reasons = [];
263
378
  const lower = code.toLowerCase();
264
- for (const r of Object.values(RISK_SELECTORS)) {
265
- if (lower.includes(r.sel)) {
266
- score -= r.penalty;
267
- reasons.push(`-${r.penalty}: ${r.why}`);
268
- }
379
+ for (const r of RISK) if (lower.includes(r.sel)) {
380
+ score -= r.penalty;
381
+ reasons.push(`-${r.penalty}: ${r.why}`);
269
382
  }
270
383
  if (owner && owner !== ZERO) {
271
384
  score -= 10;
@@ -278,13 +391,83 @@ async function tokenHealth(addr, chain2) {
278
391
  return {
279
392
  address,
280
393
  chain: chain2,
394
+ network: chainNote(chain2),
281
395
  token: { name: name == null ? null : cleanLabel(name, 48), symbol: symbol == null ? null : cleanLabel(symbol, 24), decimals, totalSupply: totalSupply?.toString() ?? null, owner },
282
396
  score: Math.max(score, 0),
283
397
  reasons,
284
- notChecked: ["holder concentration", "locked liquidity", "pair age", "wash trading", "volume vs liquidity"],
285
- notCheckedWhy: "Needs an indexer/subgraph. Without it this score is only a basic contract check, not a safety guarantee."
398
+ notChecked: ["holder concentration", "locked liquidity", "pair age", "wash trading", "volume vs liquidity", "buy/sell simulation (honeypot test)"],
399
+ notCheckedWhy: "This is only a basic contract check, not a safety guarantee. The Zypher Agent app runs a real buy/sell simulation before a swap."
400
+ };
401
+ }
402
+ var sel = (sig) => keccak256(toHex(sig)).slice(0, 10);
403
+ async function boxaLaunchInfo() {
404
+ const c = elysiumClient();
405
+ const problems = [];
406
+ let chainId = null, block = null;
407
+ try {
408
+ chainId = await c.getChainId();
409
+ block = String(await c.getBlockNumber());
410
+ } catch (e2) {
411
+ problems.push(`Could not read Elysium: ${String(e2?.message ?? e2).split("\n")[0]}`);
412
+ }
413
+ if (chainId != null && chainId !== ELYSIUM_MAINNET_ID) problems.push(`The RPC answers chain id ${chainId}, expected ${ELYSIUM_MAINNET_ID}.`);
414
+ const codeOk = {};
415
+ if (chainId === ELYSIUM_MAINNET_ID) await Promise.all(LAUNCH_SET.map(async (k) => {
416
+ try {
417
+ const code = await c.getCode({ address: BOXA[k] });
418
+ codeOk[k] = !!code && code !== "0x" && keccak256(code).toLowerCase() === CODE_HASH[k].toLowerCase();
419
+ if (!codeOk[k]) problems.push(`${k} (${BOXA[k]}) is missing or runs different code than the pinned version.`);
420
+ } catch {
421
+ codeOk[k] = null;
422
+ problems.push(`Could not read the code of ${k}.`);
423
+ }
424
+ }));
425
+ const read = async (to, sig) => {
426
+ try {
427
+ return BigInt((await c.call({ to, data: sel(sig) })).data ?? "0x0");
428
+ } catch {
429
+ return null;
430
+ }
431
+ };
432
+ const [fee, supply, tbps, tre] = chainId === ELYSIUM_MAINNET_ID ? await Promise.all([read(BOXA.launcher, "FEE()"), read(BOXA.launcher, "INITIAL_SUPPLY()"), read(BOXA.locker, "TREASURY_BPS()"), read(BOXA.locker, "treasury()")]) : [null, null, null, null];
433
+ if (fee != null && fee !== BigInt(FACTS.poolFee)) problems.push("launcher.FEE() differs from the pinned value.");
434
+ if (tbps != null && tbps !== BigInt(FACTS.treasuryBps)) problems.push("locker.TREASURY_BPS() differs from the pinned value.");
435
+ if (tre != null && getAddress3("0x" + tre.toString(16).padStart(40, "0")) !== BOXA.treasury) problems.push("locker.treasury() differs from the pinned value.");
436
+ return {
437
+ network: chainNote("elysium"),
438
+ rpc: env.elysiumMainnetRpc,
439
+ chainId,
440
+ block,
441
+ explorer: null,
442
+ notOfficial: "Not the official Kinetiq mainnet (Kinetiq's docs call it pre-launch). The RPC and the contracts are run by Boxa (boxa.club), a third party. Check before you rely on it.",
443
+ pinsOk: problems.length === 0 && chainId === ELYSIUM_MAINNET_ID,
444
+ problems,
445
+ codeHashMatches: codeOk,
446
+ launcher: BOXA.launcher,
447
+ locker: BOXA.locker,
448
+ factory: BOXA.factory,
449
+ treasury: BOXA.treasury,
450
+ economics: {
451
+ poolFeePct: fee == null ? null : Number(fee) / 1e4,
452
+ treasuryShareOfFeePct: tbps == null ? null : Number(tbps) / 100,
453
+ supply: supply == null ? null : formatUnits2(supply, 18),
454
+ creationFee: "none: the only HYPE a launch sends is the first buy",
455
+ note: "Fees go 30% to the Boxa treasury; the rest by the creator's choice (creator / holders / buyback and burn / locked liquidity)."
456
+ },
457
+ launching: "Read tools cannot launch. With ZYPHER_API_TOKEN set, the agent_elysium_launch tool launches from the user's Agent Wallet (real funds, no confirmation step); otherwise launch in the Zypher Agent app, signed in your own wallet.",
458
+ parentChain: { id: 999, inbox: PARENT.inbox }
286
459
  };
287
460
  }
461
+ async function readTokenMeta(address) {
462
+ const a = getAddress3(address), known = tokenBy(a);
463
+ if (known) return known;
464
+ const c = hyperClient();
465
+ const code = await c.getCode({ address: a });
466
+ if (!code || code === "0x") throw new Error("No contract at this address on HyperEVM.");
467
+ const [sym, name, dec] = await Promise.allSettled([c.readContract({ address: a, abi: erc20Abi, functionName: "symbol" }), c.readContract({ address: a, abi: erc20Abi, functionName: "name" }), c.readContract({ address: a, abi: erc20Abi, functionName: "decimals" })]);
468
+ if (dec.status !== "fulfilled" || !Number.isInteger(Number(dec.value)) || Number(dec.value) > 36) throw new Error("This contract does not look like an ERC-20 token.");
469
+ return { address: a, symbol: cleanLabel(sym.status === "fulfilled" ? sym.value : "?", 12) || "?", name: cleanLabel(name.status === "fulfilled" ? name.value : "Unknown token", 40), decimals: Number(dec.value), verified: false };
470
+ }
288
471
 
289
472
  // src/core/untrusted.ts
290
473
  var cleanExternal = (v, max) => cleanLabel(v, max);
@@ -334,2876 +517,453 @@ async function searchRecentPosts(bearerToken, query, timeoutMs = 15e3) {
334
517
  return { count: j.data?.length ?? 0, tweets: (j.data ?? []).map((x) => untrusted(x.text, 280)) };
335
518
  }
336
519
 
337
- // src/market/reads.ts
338
- import { setTimeout as sleep2 } from "node:timers/promises";
339
- import { formatUnits as formatUnits4, getAddress as getAddress6, isAddress as isAddress3 } from "viem";
340
-
341
- // src/shared/elysium/abis.ts
342
- import { parseAbi as parseAbi2 } from "viem";
343
- var routerAbi = parseAbi2([
344
- "function getAmountsOut(uint256 amountIn, address[] path) view returns (uint256[] amounts)",
345
- "function swapExactETHForTokens(uint256 amountOutMin, address[] path, address to, uint256 deadline) payable returns (uint256[] amounts)",
346
- "function swapExactTokensForETH(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline) returns (uint256[] amounts)",
347
- "function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline) returns (uint256[] amounts)"
348
- ]);
349
- var factoryAbi = parseAbi2([
350
- "function allPairsLength() view returns (uint256)",
351
- "function allPairs(uint256) view returns (address)"
352
- ]);
353
- var pairAbi = parseAbi2([
354
- "function token0() view returns (address)",
355
- "function token1() view returns (address)",
356
- "function getReserves() view returns (uint112 r0, uint112 r1, uint32 ts)"
357
- ]);
358
- var venueRouterAbi = parseAbi2([
359
- "function factory() view returns (address)",
360
- "function WETH() view returns (address)",
361
- "function wrappedNative() view returns (address)",
362
- "function WHYPE() view returns (address)",
363
- "function WETH9() view returns (address)"
364
- ]);
365
- var factoryPairAbi = parseAbi2(["function getPair(address,address) view returns (address)"]);
366
- var v3FactoryAbi = parseAbi2(["function getPool(address,address,uint24) view returns (address)"]);
367
- var v3PoolAbi = parseAbi2(["function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16 a, uint16 b, uint16 c, uint8 feeProtocol, bool unlocked)"]);
368
- var quoterV2Abi = parseAbi2([
369
- "function quoteExactInputSingle((address tokenIn, address tokenOut, uint256 amountIn, uint24 fee, uint160 sqrtPriceLimitX96) params) view returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)",
370
- "function quoteExactInput(bytes path, uint256 amountIn) view returns (uint256 amountOut, uint160[] sqrtPriceX96AfterList, uint32[] initializedTicksCrossedList, uint256 gasEstimate)",
371
- "function factory() view returns (address)",
372
- "function WETH9() view returns (address)"
373
- ]);
374
- var v3RouterAbi = parseAbi2([
375
- "function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) payable returns (uint256 amountOut)",
376
- "function exactInput((bytes path, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum) params) payable returns (uint256 amountOut)",
377
- "function multicall(bytes[] data) payable returns (bytes[] results)",
378
- "function unwrapWETH9(uint256 amountMinimum, address recipient) payable"
379
- ]);
380
- var v3PoolStateAbi = parseAbi2([
381
- "function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16 a, uint16 b, uint16 c, uint8 feeProtocol, bool unlocked)",
382
- "function liquidity() view returns (uint128)",
383
- "function tickSpacing() view returns (int24)",
384
- "function tickBitmap(int16 wordPosition) view returns (uint256)",
385
- "function ticks(int24 tick) view returns (uint128 liquidityGross, int128 liquidityNet, uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128, int56 tickCumulativeOutside, uint160 secondsPerLiquidityOutsideX128, uint32 secondsOutside, bool initialized)"
386
- ]);
387
-
388
- // src/shared/elysium/venues.ts
389
- var VENUES = [
390
- {
391
- id: "dexdacosta",
392
- name: DEX.name,
393
- kind: "v2",
394
- note: DEX.note,
395
- url: "https://testnet.elysium.kinetiq.xyz/ecosystem/dex-da-costa",
396
- router: DEX.router,
397
- factory: DEX.factory,
398
- wrappedNative: DEX.wrappedNative,
399
- wrappedNativeFn: "WETH",
400
- swappable: true,
401
- verification: "2026-11-02: router.factory() and router.WETH() match, getAmountsOut answers, eth_call swap simulation passes."
402
- },
403
- {
404
- id: "hexswap",
405
- name: "HexSwap",
406
- kind: "v2",
407
- note: "Independent third-party Uniswap V2-style DEX on Elysium testnet (3 small pools)",
408
- url: "https://testnet.elysium.kinetiq.xyz/ecosystem/hexswap",
409
- router: "0x9D02c1D684389D021bF2Cf73D1ba7C4652aB1F15",
410
- factory: "0xf5269ec7452b71aA79e5eA9D2dEFbCeEF76d2c92",
411
- wrappedNative: "0x8FEdA963AA318dC94403A413AA94271588B92d64",
412
- wrappedNativeFn: "wrappedNative",
413
- swappable: true,
414
- verification: "2026-11-02: router.factory() and router.wrappedNative() match (no WETH()), getAmountsOut answers, eth_call swap simulation passes. A second HexSwap router (0xc1Cc\u202620a9, factory 0x4815\u202659B6) exists but its factory has 0 pairs, so it is not listed."
415
- },
416
- {
417
- id: "huntswap",
418
- name: "Elysium Hunt Swap",
419
- kind: "v2",
420
- note: "Uniswap V2-style swap of the Elysium Huntx game (one EHUNT/wHYPE pool)",
421
- url: "https://testnet.elysium.kinetiq.xyz/ecosystem/elysium-huntx",
422
- router: "0xBD0243B0A336C4316fBD0C01699178e95883DE2c",
423
- factory: "0x5D5D83cAf9B732E9590cCcB38C7828202570d577",
424
- wrappedNative: "0xf4ee3103c2f380be0364a4B87a7C6A8B2cD3B1e4",
425
- wrappedNativeFn: "WHYPE",
426
- swappable: true,
427
- verification: "2026-11-02: router.factory() and router.WHYPE() match, factory has 1 pair (EHUNT/wHYPE), getAmountsOut answers."
428
- },
429
- {
430
- id: "kalos",
431
- name: "Kalos",
432
- kind: "v3",
433
- note: "Independent third-party Uniswap V3-style DEX on Elysium testnet; by far the most liquidity and volume on the testnet",
434
- url: "https://www.kalos.fun",
435
- router: "0x752f6816Bc514e1Eeff852b2eAdcd96f6c0F9f3f",
436
- factory: "0x520bF060F58ceE054f725877279328F5f73A7893",
437
- wrappedNative: CANONICAL.WHYPE,
438
- quoter: "0x2b555398ac8BAf5d4B541EF12E565eF312739149",
439
- quoterV1: "0x8F09c090B202990F4Fb533539FFCaAc2b6eA72a7",
440
- feeTiers: [100, 500, 3e3, 1e4, 2e4],
441
- swappable: true,
442
- verification: "2026-11-02: router/QuoterV2/Quoter/NonfungiblePositionManager all report factory() 0x520b\u2026 and WETH9() = canonical WHYPE; addresses published by Kalos' own app config. Router is the original SwapRouter (exactInputSingle/exactInput with a deadline). The second Kalos router (0xE964\u2026) wraps the older HYPE token 0x4105\u2026 which no pool uses, so it is not listed."
443
- },
444
- {
445
- id: "perpme",
446
- name: "PerpMe",
447
- kind: "v3",
448
- note: "PerpMe launchpad: every token trades in a Uniswap V3 pool quoted in PerpMe's own wrapped HYPE (hundreds of tokens, most with dust liquidity)",
449
- url: "https://testnet.elysium.kinetiq.xyz/ecosystem/perpme",
450
- router: "0xD218817cfCcA3Ae397dE3a6E66889D4Fb4b9e2D1",
451
- factory: "0x1186DEFCaf121Da6E773425Fec0814608798e792",
452
- wrappedNative: "0x757a34dd7b411546C6dd8f510B52e4381102f9B0",
453
- feeTiers: [500, 3e3, 1e4, 2e4],
454
- swappable: true,
455
- verification: "2026-11-02: router (original SwapRouter bytecode) reports factory() 0x1186\u2026 and WETH9() = PerpMe's wrapped HYPE. No QuoterV2 is published for this factory, so quotes are simulated locally from pool state (src/shared/v3math.ts, a BigInt port of the pool's swap math that matched Kalos' on-chain QuoterV2 exactly on 64 test swaps incl. tick crossings) and every swap is eth_call-simulated through the router before it is sent."
456
- }
457
- ];
458
- var SWAP_VENUES = VENUES.filter((v) => v.swappable);
459
- var V2_VENUES = VENUES.filter((v) => v.kind === "v2");
460
- var V3_VENUES = VENUES.filter((v) => v.kind === "v3");
461
- var EXTERNAL_VENUES = [
462
- {
463
- id: "marketlab",
464
- name: "MarketLab",
465
- url: "https://cloud.marketlab.sh",
466
- kind: "custom AMM + bonding markets",
467
- reason: "Not a V2/V3 router: every pool is its own contract with swapExactInput/quoteSwap and an owner/operator-set fee, there is no factory to list pools, and the whole venue holds about 2.8k USD of TVL."
468
- },
469
- { id: "bunnypad", name: "BunnyPad", url: "https://testnet.elysium.kinetiq.xyz/ecosystem/bunnypad", kind: "launchpad", reason: "Fair-launch launchpad; tokens trade through BunnyPad's own contracts, which Zypher does not integrate." },
470
- { id: "ascend", name: "Ascend", url: "https://testnet.elysium.kinetiq.xyz/ecosystem/ascend", kind: "launchpad", reason: "Launchpad with its own router and a Universal-Router-style contract; not integrated." },
471
- { id: "corepad", name: "CorePad", url: "https://testnet.elysium.kinetiq.xyz/ecosystem/corepad", kind: "launchpad", reason: "Price-curve launchpad that moves to an order book on HyperCore; not integrated." },
472
- { id: "kiln", name: "Kiln", url: "https://testnet.elysium.kinetiq.xyz/ecosystem/kiln", kind: "bonding curve", reason: "Bonding-curve launchpad (one curve contract per token); not integrated." },
473
- { id: "oxley", name: "Oxley", url: "https://testnet.elysium.kinetiq.xyz/ecosystem/oxley", kind: "reserve curve", reason: "Reserve-curve launchpad; not integrated." },
474
- { id: "neuriio", name: "Neuriio", url: "https://testnet.elysium.kinetiq.xyz/ecosystem/neuriio", kind: "custom AMM", reason: "Pairs have V2-like pools but each comes from a different factory and the router lacks the standard V2 interface; not integrated." },
475
- { id: "mosaic", name: "Mosaic", url: "https://testnet.elysium.kinetiq.xyz/ecosystem/mosaic", kind: "order book", reason: "Order book, not an AMM; not integrated." }
476
- ];
477
- var KALOS_WRAPPED_HYPE = "0x41051f27c609dB70af05b7128f15155Dc8E49Ac7";
478
- var HYPE_LIKE = [.../* @__PURE__ */ new Set([...VENUES.map((v) => v.wrappedNative), CANONICAL.WHYPE, KALOS_WRAPPED_HYPE])];
479
- var isHypeLike = (a) => HYPE_LIKE.some((x) => same(x, a));
480
-
481
- // src/shared/elysium/rpc.ts
482
- import { createPublicClient as createPublicClient2, erc20Abi as erc20Abi2, http as http2 } from "viem";
483
- function getBatchClient(rpcUrl = ELYSIUM_RPC) {
484
- return createPublicClient2({
485
- chain: elysiumTestnet,
486
- transport: http2(rpcUrl, { batch: true }),
487
- batch: { multicall: { batchSize: 48e3, wait: 8 } }
488
- });
489
- }
490
- var revertLike = (e2) => /revert|returned no data|zero data/i.test(String(e2?.message ?? e2));
491
- async function multicallChecked(c, contracts) {
492
- for (let attempt = 0; ; attempt++) {
493
- const res = await c.multicall({ contracts, allowFailure: true });
494
- const rpcDown = res.length > 0 && res.every((r) => r.status === "failure" && !revertLike(r.error));
495
- if (!rpcDown || attempt >= 1) return { res, rpcDown };
496
- await sleep(400);
497
- }
498
- }
499
-
500
- // src/shared/elysium/tokens.ts
501
- import { erc20Abi as erc20Abi3, formatUnits as formatUnits3, getAddress as getAddress2, isAddress as isAddress2 } from "viem";
502
- async function readTokenInfo(c, addrs) {
503
- const calls = addrs.flatMap((address) => [
504
- { address, abi: erc20Abi3, functionName: "symbol" },
505
- { address, abi: erc20Abi3, functionName: "name" },
506
- { address, abi: erc20Abi3, functionName: "decimals" },
507
- { address, abi: erc20Abi3, functionName: "totalSupply" }
508
- ]);
509
- const res = await c.multicall({ contracts: calls, allowFailure: true });
510
- const out = /* @__PURE__ */ new Map();
511
- addrs.forEach((address, i) => {
512
- const [s, n3, d, t] = res.slice(i * 4, i * 4 + 4);
513
- out.set(address.toLowerCase(), {
514
- address,
515
- symbol: s.status === "success" ? cleanLabel(s.result, 24) || "???" : "???",
516
- name: n3.status === "success" ? cleanLabel(n3.result, 48) || "Unknown token" : "Unknown token",
517
- decimals: d.status === "success" ? Number(d.result) : 18,
518
- totalSupply: t.status === "success" ? String(t.result) : null
519
- });
520
- });
521
- return out;
522
- }
523
- async function importToken(c, address) {
524
- if (!isAddress2(address)) throw new Error("Invalid token address");
525
- const a = getAddress2(address);
526
- const code = await c.getCode({ address: a });
527
- if (!code || code === "0x") throw new Error("No contract at this address on Elysium testnet");
528
- return (await readTokenInfo(c, [a])).get(a.toLowerCase());
529
- }
530
- async function listAllVenuePairs(c, venues = V2_VENUES) {
531
- venues = venues.filter((v) => v.kind === "v2");
532
- const lens = await c.multicall({ contracts: venues.map((v) => ({ address: v.factory, abi: factoryAbi, functionName: "allPairsLength" })), allowFailure: true });
533
- const idx = [];
534
- venues.forEach((v, k) => {
535
- const r = lens[k];
536
- if (r.status !== "success") throw new Error(`could not read the ${v.name} factory`);
537
- for (let i = 0; i < Number(r.result); i++) idx.push({ venue: v, i });
538
- });
539
- if (!idx.length) return [];
540
- const addrs = await c.multicall({ contracts: idx.map(({ venue, i }) => ({ address: venue.factory, abi: factoryAbi, functionName: "allPairs", args: [BigInt(i)] })), allowFailure: true });
541
- const live = idx.map((x, k) => {
542
- const a = addrs[k];
543
- if (a.status !== "success") throw new Error(`could not read pair #${x.i} of ${x.venue.name}`);
544
- return { ...x, pair: a.result };
545
- });
546
- const data = await c.multicall({
547
- contracts: live.flatMap(({ pair: address }) => [
548
- { address, abi: pairAbi, functionName: "token0" },
549
- { address, abi: pairAbi, functionName: "token1" },
550
- { address, abi: pairAbi, functionName: "getReserves" }
551
- ]),
552
- allowFailure: true
553
- });
554
- const out = [];
555
- live.forEach(({ venue, pair }, k) => {
556
- const [t0, t1, r] = data.slice(k * 3, k * 3 + 3);
557
- if (t0.status !== "success" || t1.status !== "success" || r.status !== "success") throw new Error(`could not read pair ${pair} of ${venue.name}`);
558
- const [r0, r1] = r.result;
559
- out.push({ venue: venue.id, pair, token0: t0.result, token1: t1.result, reserve0: r0, reserve1: r1 });
560
- });
561
- return out;
562
- }
563
-
564
- // src/shared/elysium/quote.ts
565
- import { encodePacked } from "viem";
566
-
567
- // src/shared/v3math.ts
568
- var Q96 = 1n << 96n;
569
- var MIN_TICK = -887272;
570
- var MAX_TICK = 887272;
571
- var MIN_SQRT_RATIO = 4295128739n;
572
- var MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342n;
573
- var MAX_UINT256 = (1n << 256n) - 1n;
574
- var MAX_UINT160 = (1n << 160n) - 1n;
575
- var MAX_UINT128 = (1n << 128n) - 1n;
576
- var mulDiv = (a, b, d) => a * b / d;
577
- var mulDivRoundingUp = (a, b, d) => {
578
- const p = a * b;
579
- const q = p / d;
580
- return p % d === 0n ? q : q + 1n;
581
- };
582
- var divRoundingUp = (a, b) => a % b === 0n ? a / b : a / b + 1n;
583
- var TICK_RATIOS = [
584
- [0x2n, 0xfff97272373d413259a46990580e213an],
585
- [0x4n, 0xfff2e50f5f656932ef12357cf3c7fdccn],
586
- [0x8n, 0xffe5caca7e10e4e61c3624eaa0941cd0n],
587
- [0x10n, 0xffcb9843d60f6159c9db58835c926644n],
588
- [0x20n, 0xff973b41fa98c081472e6896dfb254c0n],
589
- [0x40n, 0xff2ea16466c96a3843ec78b326b52861n],
590
- [0x80n, 0xfe5dee046a99a2a811c461f1969c3053n],
591
- [0x100n, 0xfcbe86c7900a88aedcffc83b479aa3a4n],
592
- [0x200n, 0xf987a7253ac413176f2b074cf7815e54n],
593
- [0x400n, 0xf3392b0822b70005940c7a398e4b70f3n],
594
- [0x800n, 0xe7159475a2c29b7443b29c7fa6e889d9n],
595
- [0x1000n, 0xd097f3bdfd2022b8845ad8f792aa5825n],
596
- [0x2000n, 0xa9f746462d870fdf8a65dc1f90e061e5n],
597
- [0x4000n, 0x70d869a156d2a1b890bb3df62baf32f7n],
598
- [0x8000n, 0x31be135f97d08fd981231505542fcfa6n],
599
- [0x10000n, 0x9aa508b5b7a84e1c677de54f3e99bc9n],
600
- [0x20000n, 0x5d6af8dedb81196699c329225ee604n],
601
- [0x40000n, 0x2216e584f5fa1ea926041bedfe98n],
602
- [0x80000n, 0x48a170391f7dc42444e8fa2n]
603
- ];
604
- function getSqrtRatioAtTick(tick) {
605
- if (!Number.isInteger(tick) || tick < MIN_TICK || tick > MAX_TICK) throw new Error(`tick ${tick} out of range`);
606
- const abs = BigInt(tick < 0 ? -tick : tick);
607
- let ratio = (abs & 0x1n) !== 0n ? 0xfffcb933bd6fad37aa2d162d1a594001n : 0x100000000000000000000000000000000n;
608
- for (const [bit, mul] of TICK_RATIOS) if ((abs & bit) !== 0n) ratio = ratio * mul >> 128n;
609
- if (tick > 0) ratio = MAX_UINT256 / ratio;
610
- return (ratio >> 32n) + ((ratio & 0xffffffffn) === 0n ? 0n : 1n);
611
- }
612
- function getAmount0Delta(sqrtA, sqrtB, liquidity, roundUp) {
613
- if (sqrtA > sqrtB) [sqrtA, sqrtB] = [sqrtB, sqrtA];
614
- const num1 = liquidity << 96n, num2 = sqrtB - sqrtA;
615
- if (sqrtA <= 0n) throw new Error("sqrtA must be positive");
616
- return roundUp ? divRoundingUp(mulDivRoundingUp(num1, num2, sqrtB), sqrtA) : mulDiv(num1, num2, sqrtB) / sqrtA;
617
- }
618
- function getAmount1Delta(sqrtA, sqrtB, liquidity, roundUp) {
619
- if (sqrtA > sqrtB) [sqrtA, sqrtB] = [sqrtB, sqrtA];
620
- return roundUp ? mulDivRoundingUp(liquidity, sqrtB - sqrtA, Q96) : mulDiv(liquidity, sqrtB - sqrtA, Q96);
621
- }
622
- function nextSqrtFromAmount0RoundingUp(sqrtPX96, liquidity, amount, add) {
623
- if (amount === 0n) return sqrtPX96;
624
- const numerator1 = liquidity << 96n;
625
- const product = amount * sqrtPX96 & MAX_UINT256;
626
- if (add) {
627
- if (product / amount === sqrtPX96) {
628
- const denominator = numerator1 + product & MAX_UINT256;
629
- if (denominator >= numerator1) return mulDivRoundingUp(numerator1, sqrtPX96, denominator);
630
- }
631
- return divRoundingUp(numerator1, numerator1 / sqrtPX96 + amount);
632
- }
633
- if (!(product / amount === sqrtPX96 && numerator1 > product)) throw new Error("price underflow");
634
- return mulDivRoundingUp(numerator1, sqrtPX96, numerator1 - product);
635
- }
636
- function nextSqrtFromAmount1RoundingDown(sqrtPX96, liquidity, amount, add) {
637
- if (add) {
638
- const quotient2 = amount <= MAX_UINT160 ? (amount << 96n) / liquidity : mulDiv(amount, Q96, liquidity);
639
- return sqrtPX96 + quotient2;
640
- }
641
- const quotient = amount <= MAX_UINT160 ? divRoundingUp(amount << 96n, liquidity) : mulDivRoundingUp(amount, Q96, liquidity);
642
- if (sqrtPX96 <= quotient) throw new Error("price underflow");
643
- return sqrtPX96 - quotient;
644
- }
645
- function getNextSqrtPriceFromInput(sqrtPX96, liquidity, amountIn, zeroForOne) {
646
- if (sqrtPX96 <= 0n || liquidity <= 0n) throw new Error("bad pool state");
647
- return zeroForOne ? nextSqrtFromAmount0RoundingUp(sqrtPX96, liquidity, amountIn, true) : nextSqrtFromAmount1RoundingDown(sqrtPX96, liquidity, amountIn, true);
648
- }
649
- function computeSwapStepExactIn(sqrtCurrent, sqrtTarget, liquidity, amountRemaining, feePips) {
650
- const zeroForOne = sqrtCurrent >= sqrtTarget;
651
- const fee = BigInt(feePips);
652
- const amountRemainingLessFee = mulDiv(amountRemaining, 1000000n - fee, 1000000n);
653
- let amountIn = zeroForOne ? getAmount0Delta(sqrtTarget, sqrtCurrent, liquidity, true) : getAmount1Delta(sqrtCurrent, sqrtTarget, liquidity, true);
654
- let sqrtNext;
655
- if (amountRemainingLessFee >= amountIn) sqrtNext = sqrtTarget;
656
- else sqrtNext = getNextSqrtPriceFromInput(sqrtCurrent, liquidity, amountRemainingLessFee, zeroForOne);
657
- const max = sqrtTarget === sqrtNext;
658
- let amountOut;
659
- if (zeroForOne) {
660
- amountIn = max ? amountIn : getAmount0Delta(sqrtNext, sqrtCurrent, liquidity, true);
661
- amountOut = getAmount1Delta(sqrtNext, sqrtCurrent, liquidity, false);
662
- } else {
663
- amountIn = max ? amountIn : getAmount1Delta(sqrtCurrent, sqrtNext, liquidity, true);
664
- amountOut = getAmount0Delta(sqrtCurrent, sqrtNext, liquidity, false);
520
+ // src/providers/zypher.ts
521
+ var BASE = () => (process.env.ZYPHER_API_URL?.trim() || "https://zypheragent.com/agent").replace(/\/+$/, "");
522
+ async function getJson(path, q) {
523
+ const u = new URL(BASE() + path);
524
+ for (const [k, v] of Object.entries(q)) if (v !== void 0 && v !== "") u.searchParams.set(k, String(v));
525
+ const r = await fetch(u, { headers: { accept: "application/json" }, redirect: "error", signal: AbortSignal.timeout(2e4) });
526
+ const text = await r.text();
527
+ if (text.length > 4e5) throw new Error("Response from the Zypher API is too large; refused.");
528
+ let j;
529
+ try {
530
+ j = JSON.parse(text);
531
+ } catch {
532
+ j = null;
665
533
  }
666
- const feeAmount = !max ? amountRemaining - amountIn : mulDivRoundingUp(amountIn, fee, 1000000n - fee);
667
- return { sqrtNextX96: sqrtNext, amountIn, amountOut, feeAmount };
534
+ if (!r.ok || !j || j.ok === false) throw new Error(`Zypher API ${r.status}: ${cleanLabel(String(j?.error ?? "request failed"), 200)}`);
535
+ return j;
668
536
  }
669
- var mostSignificantBit = (x) => x.toString(2).length - 1;
670
- var leastSignificantBit = (x) => {
671
- const s = (x & -x).toString(2);
672
- return s.length - 1;
673
- };
674
- var floorDiv = (a, b) => Math.floor(a / b);
675
- var mod256 = (a) => (a % 256 + 256) % 256;
676
- var MissingPoolData = class extends Error {
677
- /** dir = direction the swap is travelling (-1 = towards lower ticks), so a loader knows which words to fetch next */
678
- constructor(kind, at, dir = -1) {
679
- super(`missing ${kind} ${at}`);
680
- this.kind = kind;
681
- this.at = at;
682
- this.dir = dir;
683
- }
684
- kind;
685
- at;
686
- dir;
687
- };
688
- function nextInitializedTickWithinOneWord(words, tick, spacing, lte) {
689
- let compressed = floorDiv(tick, spacing);
690
- if (lte) {
691
- const wordPos2 = compressed >> 8, bitPos2 = mod256(compressed);
692
- const word2 = words.get(wordPos2);
693
- if (word2 === void 0) throw new MissingPoolData("word", wordPos2, lte ? -1 : 1);
694
- const mask2 = (1n << BigInt(bitPos2)) - 1n + (1n << BigInt(bitPos2));
695
- const masked2 = word2 & mask2;
696
- const initialized2 = masked2 !== 0n;
697
- return [(initialized2 ? compressed - (bitPos2 - mostSignificantBit(masked2)) : compressed - bitPos2) * spacing, initialized2];
698
- }
699
- compressed += 1;
700
- const wordPos = compressed >> 8, bitPos = mod256(compressed);
701
- const word = words.get(wordPos);
702
- if (word === void 0) throw new MissingPoolData("word", wordPos, lte ? -1 : 1);
703
- const mask = ~((1n << BigInt(bitPos)) - 1n) & MAX_UINT256;
704
- const masked = word & mask;
705
- const initialized = masked !== 0n;
706
- return [(initialized ? compressed + (leastSignificantBit(masked) - bitPos) : compressed + (255 - bitPos)) * spacing, initialized];
537
+ var num = (x, d = 6) => typeof x === "number" && Number.isFinite(x) ? Number(x.toPrecision(d)) : null;
538
+ var row = (t, hypeUsd2) => ({
539
+ address: t.address,
540
+ symbol: cleanLabel(String(t.symbol ?? "?"), 12),
541
+ name: cleanLabel(String(t.name ?? ""), 40),
542
+ launchpad: t.launchpad,
543
+ pool: t.poolVersion,
544
+ createdAt: t.createdAt ? new Date(t.createdAt * 1e3).toISOString() : null,
545
+ priceHype: num(t.priceHype),
546
+ priceUsd: hypeUsd2 && t.priceHype ? num(t.priceHype * hypeUsd2) : null,
547
+ fdvUsd: hypeUsd2 && t.fdvHype ? Math.round(t.fdvHype * hypeUsd2) : null,
548
+ change24hPct: num(t.change24hPct, 4),
549
+ volume24hHype: num(t.volume24hHype, 6),
550
+ trades24h: t.trades24h ?? null,
551
+ holders: t.holders ?? null,
552
+ tradable: t.tradable ?? null,
553
+ stock: t.stock ?? null
554
+ });
555
+ var ELY_SORTS = ["newest", "fdv", "volume", "change", "trades", "holders"];
556
+ async function elysiumTokens(o) {
557
+ const j = await getJson("/api/market/elysium", { sort: o.sort, launchpad: o.launchpad, q: o.q, limit: o.limit, priced: o.priced ? 1 : void 0, tradable: o.tradable ? 1 : void 0 });
558
+ const px = typeof j.hypeUsd === "number" ? j.hypeUsd : null;
559
+ return {
560
+ network: "Elysium 99901 (community-run, served by Boxa; not the official Kinetiq mainnet)",
561
+ hypeUsd: px,
562
+ total: j.total,
563
+ sort: o.sort,
564
+ launchpad: o.launchpad ?? "all",
565
+ launchpads: j.launchpads ?? null,
566
+ volume24hHype: j.volume24hHype ?? null,
567
+ stale: j.stale ?? false,
568
+ tokens: (j.tokens ?? []).map((t) => row(t, px)),
569
+ note: "Prices are pool prices from the BOXA index; thin pools can be manipulated. Token names are untrusted text. Not financial advice."
570
+ };
707
571
  }
708
- function simulateExactIn(pool, zeroForOne, amountIn, sqrtPriceLimitX96 = 0n) {
709
- if (amountIn <= 0n) throw new Error("amountIn must be positive");
710
- const limit = sqrtPriceLimitX96 === 0n ? zeroForOne ? MIN_SQRT_RATIO + 1n : MAX_SQRT_RATIO - 1n : sqrtPriceLimitX96;
711
- if (zeroForOne ? !(limit < pool.sqrtPriceX96 && limit > MIN_SQRT_RATIO) : !(limit > pool.sqrtPriceX96 && limit < MAX_SQRT_RATIO)) throw new Error("bad price limit");
712
- let remaining = amountIn, out = 0n, sqrt = pool.sqrtPriceX96, tick = pool.tick, liquidity = pool.liquidity, crossed = 0;
713
- while (remaining !== 0n && sqrt !== limit) {
714
- let [tickNext, initialized] = nextInitializedTickWithinOneWord(pool.words, tick, pool.tickSpacing, zeroForOne);
715
- if (tickNext < MIN_TICK) tickNext = MIN_TICK;
716
- else if (tickNext > MAX_TICK) tickNext = MAX_TICK;
717
- const sqrtNextTick = getSqrtRatioAtTick(tickNext);
718
- const target = (zeroForOne ? sqrtNextTick < limit : sqrtNextTick > limit) ? limit : sqrtNextTick;
719
- const step = computeSwapStepExactIn(sqrt, target, liquidity, remaining, pool.feePips);
720
- remaining -= step.amountIn + step.feeAmount;
721
- out += step.amountOut;
722
- sqrt = step.sqrtNextX96;
723
- if (sqrt === sqrtNextTick) {
724
- if (initialized) {
725
- const net = pool.nets.get(tickNext);
726
- if (net === void 0) throw new MissingPoolData("tick", tickNext);
727
- liquidity += zeroForOne ? -net : net;
728
- if (liquidity < 0n || liquidity > MAX_UINT128) throw new Error("liquidity out of range");
729
- crossed++;
730
- }
731
- tick = zeroForOne ? tickNext - 1 : tickNext;
732
- }
733
- }
734
- return { amountIn: amountIn - remaining, amountOut: out, sqrtPriceX96After: sqrt, ticksCrossed: crossed };
572
+ async function elysiumToken(address) {
573
+ const j = await getJson("/api/market/elysium/token", { address });
574
+ const px = typeof j.hypeUsd === "number" ? j.hypeUsd : null;
575
+ return { network: "Elysium 99901", hypeUsd: px, token: row(j.token ?? j, px), detail: j.info ?? null };
735
576
  }
736
577
 
737
- // src/shared/elysium/v3-local.ts
738
- async function quoteV3Local(c, cands, amountIn) {
739
- const feeOf = /* @__PURE__ */ new Map();
740
- cands.forEach((x) => x.hopPools.forEach((p, i) => feeOf.set(p.toLowerCase(), x.fees[i])));
741
- const poolAddrs = [...new Set(cands.flatMap((x) => x.hopPools))];
742
- const base = await multicallChecked(c, poolAddrs.flatMap((address) => [
743
- { address, abi: v3PoolStateAbi, functionName: "slot0" },
744
- { address, abi: v3PoolStateAbi, functionName: "liquidity" },
745
- { address, abi: v3PoolStateAbi, functionName: "tickSpacing" }
746
- ]));
747
- if (base.rpcDown) return { outs: cands.map(() => null), rpcDown: true };
748
- const data = /* @__PURE__ */ new Map();
749
- poolAddrs.forEach((p, i) => {
750
- const [s, l, t] = base.res.slice(i * 3, i * 3 + 3);
751
- if (s.status !== "success" || l.status !== "success" || t.status !== "success") return;
752
- const slot = s.result;
753
- data.set(p.toLowerCase(), { sqrtPriceX96: slot[0], tick: Number(slot[1]), liquidity: l.result, tickSpacing: Number(t.result), feePips: feeOf.get(p.toLowerCase()), words: /* @__PURE__ */ new Map(), nets: /* @__PURE__ */ new Map() });
754
- });
755
- const outs = cands.map(() => null);
756
- let pending = cands.map((_, i) => i);
757
- let rpcDown = false;
758
- const radius = /* @__PURE__ */ new Map();
759
- for (let round = 0; round < 14 && pending.length; round++) {
760
- const need = /* @__PURE__ */ new Map();
761
- const want = (pool) => need.get(pool) ?? need.set(pool, { words: /* @__PURE__ */ new Set(), ticks: /* @__PURE__ */ new Set() }).get(pool);
762
- const still = [];
763
- for (const i of pending) {
764
- const x = cands[i];
765
- let amt2 = amountIn, missing = null, bad = false;
766
- for (let h = 0; h < x.hopPools.length && !missing && !bad; h++) {
767
- const pool = data.get(x.hopPools[h].toLowerCase());
768
- if (!pool) {
769
- bad = true;
770
- break;
771
- }
772
- try {
773
- amt2 = simulateExactIn(pool, x.path[h].toLowerCase() < x.path[h + 1].toLowerCase(), amt2).amountOut;
774
- } catch (e2) {
775
- if (e2 instanceof MissingPoolData) missing = { pool: x.hopPools[h].toLowerCase(), err: e2 };
776
- else bad = true;
777
- }
778
- if (!missing && !bad && amt2 === 0n) break;
779
- }
780
- if (missing) {
781
- const w = want(missing.pool);
782
- if (missing.err.kind === "word") {
783
- const r = Math.min(64, (radius.get(missing.pool) ?? 1) * 2);
784
- radius.set(missing.pool, r);
785
- const spacing = data.get(missing.pool).tickSpacing, lo = Math.floor(-887272 / spacing) >> 8, hi = Math.floor(887272 / spacing) >> 8;
786
- for (let d = -1; d <= r; d++) {
787
- const wp = missing.err.at + missing.err.dir * d;
788
- if (wp >= lo && wp <= hi) w.words.add(wp);
789
- }
790
- } else w.ticks.add(missing.err.at);
791
- still.push(i);
792
- } else if (!bad) outs[i] = amt2;
793
- }
794
- pending = still;
795
- if (!still.length) break;
796
- const wordCalls = [];
797
- for (const [pool, w] of need) for (const word of w.words) if (!data.get(pool).words.has(word)) wordCalls.push({ pool, word });
798
- if (wordCalls.length) {
799
- const r = await multicallChecked(c, wordCalls.map(({ pool, word }) => ({ address: pool, abi: v3PoolStateAbi, functionName: "tickBitmap", args: [word] })));
800
- if (r.rpcDown) {
801
- rpcDown = true;
802
- break;
803
- }
804
- wordCalls.forEach(({ pool, word }, k) => {
805
- const res = r.res[k];
806
- if (res.status !== "success") return;
807
- const bits = res.result;
808
- data.get(pool).words.set(word, bits);
809
- const spacing = data.get(pool).tickSpacing;
810
- const t = want(pool);
811
- for (let b = 0; b < 256; b++) if (bits >> BigInt(b) & 1n) t.ticks.add((word * 256 + b) * spacing);
812
- });
813
- }
814
- const tickCalls = [];
815
- for (const [pool, w] of need) {
816
- const d = data.get(pool);
817
- const sorted = [...w.ticks].filter((t) => !d.nets.has(t)).sort((a, b) => Math.abs(a - d.tick) - Math.abs(b - d.tick)).slice(0, 48);
818
- for (const tick of sorted) tickCalls.push({ pool, tick });
819
- }
820
- if (tickCalls.length) {
821
- const r = await multicallChecked(c, tickCalls.map(({ pool, tick }) => ({ address: pool, abi: v3PoolStateAbi, functionName: "ticks", args: [tick] })));
822
- if (r.rpcDown) {
823
- rpcDown = true;
824
- break;
825
- }
826
- tickCalls.forEach(({ pool, tick }, k) => {
827
- const res = r.res[k];
828
- if (res.status === "success") data.get(pool).nets.set(tick, res.result[1]);
829
- });
830
- }
831
- if (!wordCalls.length && !tickCalls.length) break;
832
- }
833
- return { outs, rpcDown };
834
- }
578
+ // src/market/reads.ts
579
+ import { formatUnits as formatUnits3, getAddress as getAddress4, isAddress as isAddress2, parseUnits } from "viem";
835
580
 
836
- // src/shared/elysium/quote.ts
837
- var venueNative = (v, t) => t.address === "native" ? v.wrappedNative : t.address;
838
- var V3_PATH_FEES = (v) => v.feeTiers ?? [100, 500, 3e3, 1e4];
839
- var sortedKey = (a, b) => (a.toLowerCase() < b.toLowerCase() ? `${a}|${b}` : `${b}|${a}`).toLowerCase();
840
- function venuePaths(v, tokenIn, tokenOut3) {
841
- const a = venueNative(v, tokenIn), b = venueNative(v, tokenOut3);
842
- if (same(a, b)) return [];
843
- const paths = [[a, b]];
844
- for (const mid of [v.wrappedNative, CANONICAL.USDC]) if (!same(a, mid) && !same(b, mid)) paths.push([a, mid, b]);
845
- return paths;
581
+ // src/shared/hyperswap/route.ts
582
+ import { encodePacked, erc20Abi as erc20Abi2, parseAbi as parseAbi2 } from "viem";
583
+ var MIN_POOL_TVL_USD = 5e3;
584
+ var REF_SLACK_PCT = 1.5;
585
+ var REF_ABOVE_PCT = 3;
586
+ var CLUSTER_PCT = 10;
587
+ var factoryAbi = parseAbi2(["function getPool(address,address,uint24) view returns (address)"]);
588
+ var poolAbi = parseAbi2([
589
+ "function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16 a, uint16 b, uint16 c, uint8 feeProtocol, bool unlocked)",
590
+ "function liquidity() view returns (uint128)"
591
+ ]);
592
+ var quoterAbi = parseAbi2([
593
+ "function quoteExactInputSingle((address tokenIn, address tokenOut, uint256 amountIn, uint24 fee, uint160 sqrtPriceLimitX96) params) view returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)",
594
+ "function quoteExactInput(bytes path, uint256 amountIn) view returns (uint256 amountOut, uint160[] sqrtPriceX96AfterList, uint32[] initializedTicksCrossedList, uint256 gasEstimate)"
595
+ ]);
596
+ function usdPrice(address, hypeUsd2) {
597
+ if (address === "native" || sameAddr(address, WHYPE)) return hypeUsd2 != null && hypeUsd2 > 0 ? hypeUsd2 : null;
598
+ return tokenBy(address)?.stable ? 1 : null;
599
+ }
600
+ var decOf = (address, fallback) => address === "native" ? 18 : tokenBy(address)?.decimals ?? fallback;
601
+ function candidatePaths(a, b) {
602
+ const out = [[a, b]];
603
+ for (const mid of [WHYPE, USDC.address]) if (!sameAddr(mid, a) && !sameAddr(mid, b)) out.push([a, mid, b]);
604
+ return out;
846
605
  }
847
- function encodeV3Path(tokens, fees) {
848
- if (tokens.length !== fees.length + 1 || fees.length < 1) throw new Error("V3 path needs one fee per hop");
849
- const types = [], values = [];
850
- tokens.forEach((t, i) => {
606
+ var encodePath = (path, fees) => {
607
+ const types = [], vals = [];
608
+ path.forEach((p, i) => {
851
609
  types.push("address");
852
- values.push(t);
610
+ vals.push(p);
853
611
  if (i < fees.length) {
854
612
  types.push("uint24");
855
- values.push(fees[i]);
613
+ vals.push(fees[i]);
856
614
  }
857
615
  });
858
- return encodePacked(types, values);
859
- }
860
- var poolCache = /* @__PURE__ */ new Map();
861
- var POOL_MISS_TTL = 3e4;
862
- var zeroAddr = (a) => /^0x0+$/.test(a);
863
- async function quoteVenues(c, tokenIn, tokenOut3, amountInRaw, venues = SWAP_VENUES) {
616
+ return encodePacked(types, vals);
617
+ };
618
+ var feeCombos = (hops) => hops === 1 ? FEE_TIERS.map((f) => [f]) : FEE_TIERS.flatMap((f1) => FEE_TIERS.map((f2) => [f1, f2]));
619
+ var ZERO2 = "0x0000000000000000000000000000000000000000";
620
+ var key = (x, y, fee) => `${[x.toLowerCase(), y.toLowerCase()].sort().join("|")}|${fee}`;
621
+ var feeFactor = (fees) => fees.reduce((p, f) => p * (1 - f / 1e6), 1);
622
+ function poolVerdict(p, hypeUsd2) {
623
+ if (p.liquidity === 0n) return { ok: false, tvlUsd: null, reason: "no in-range liquidity" };
624
+ const val = (addr, bal) => {
625
+ const px = usdPrice(addr, hypeUsd2);
626
+ return px == null ? null : Number(bal) / 10 ** decOf(addr, 18) * px;
627
+ };
628
+ const va = val(p.tokenA, p.balA), vb = val(p.tokenB, p.balB);
629
+ const priced = [va, vb].filter((x) => x != null);
630
+ if (!priced.length) return { ok: false, tvlUsd: null, reason: "value unknown (no reference price for either token)" };
631
+ const tvl = 2 * Math.min(...priced);
632
+ return tvl >= MIN_POOL_TVL_USD ? { ok: true, tvlUsd: tvl } : { ok: false, tvlUsd: tvl, reason: `dust pool (~$${tvl < 10 ? tvl.toFixed(2) : Math.round(tvl)} TVL, minimum $${MIN_POOL_TVL_USD})` };
633
+ }
634
+ async function loadPools(c, triples, hypeUsd2) {
635
+ const uniq = [...new Map(triples.map((t) => [key(t.a, t.b, t.fee), t])).values()];
636
+ const found = await c.multicall({ contracts: uniq.map((t) => ({ address: HYPERSWAP.factory, abi: factoryAbi, functionName: "getPool", args: [t.a, t.b, t.fee] })), allowFailure: true });
637
+ if (found.length && found.every((r) => r.status === "failure")) throw new Error("rpc_error");
638
+ const live = uniq.flatMap((t, i) => {
639
+ const r = found[i];
640
+ return r.status === "success" && r.result !== ZERO2 ? [{ ...t, pool: r.result }] : [];
641
+ });
864
642
  const out = /* @__PURE__ */ new Map();
865
- const done = (venue, quote, reason, detail = null) => out.set(venue.id, { venue, quote, reason, detail });
866
- const plans = venues.map((venue) => ({ venue, paths: venuePaths(venue, tokenIn, tokenOut3) }));
867
- for (const p of plans) if (!p.paths.length) done(p.venue, null, "same_token");
868
- const active = plans.filter((p) => p.paths.length);
869
- const calls = [];
870
- const seen = /* @__PURE__ */ new Set();
871
- const now = Date.now();
872
- const pools = /* @__PURE__ */ new Map();
873
- for (const { venue, paths } of active) {
874
- if (venue.kind === "v2") {
875
- for (const path of paths) calls.push({ t: "v2amounts", venue, path });
876
- for (const path of paths) path.slice(0, -1).forEach((a, i) => {
877
- const key = `${venue.id}|${sortedKey(a, path[i + 1])}`;
878
- if (!seen.has(key)) {
879
- seen.add(key);
880
- calls.push({ t: "v2pair", venue, key, a, b: path[i + 1] });
881
- }
882
- });
883
- } else {
884
- for (const path of paths) path.slice(0, -1).forEach((a, i) => {
885
- for (const fee of V3_PATH_FEES(venue)) {
886
- const key = `${venue.id}|${sortedKey(a, path[i + 1])}|${fee}`;
887
- if (seen.has(key)) continue;
888
- seen.add(key);
889
- const hit = poolCache.get(key);
890
- if (hit && (hit.pool || now - hit.at < POOL_MISS_TTL)) pools.set(key, hit.pool);
891
- else calls.push({ t: "v3pool", venue, key, a, b: path[i + 1], fee });
892
- }
893
- });
894
- }
895
- }
896
- const pairs = /* @__PURE__ */ new Map();
897
- const v2out = /* @__PURE__ */ new Map();
898
- let rpcFailed = false;
899
- if (calls.length) {
900
- const { res, rpcDown } = await multicallChecked(c, calls.map((x) => x.t === "v2amounts" ? { address: x.venue.router, abi: routerAbi, functionName: "getAmountsOut", args: [amountInRaw, x.path] } : x.t === "v2pair" ? { address: x.venue.factory, abi: factoryPairAbi, functionName: "getPair", args: [x.a, x.b] } : { address: x.venue.factory, abi: v3FactoryAbi, functionName: "getPool", args: [x.a, x.b, x.fee] }));
901
- rpcFailed = rpcDown;
902
- calls.forEach((x, i) => {
903
- const r = res[i];
904
- if (x.t === "v2amounts") {
905
- if (r.status === "success") {
906
- const a = r.result;
907
- v2out.set(`${x.venue.id}|${x.path.join(",")}`, a[a.length - 1]);
908
- } else if (!revertLike(r.error)) rpcFailed = true;
909
- return;
910
- }
911
- if (r.status !== "success") {
912
- rpcFailed = true;
913
- return;
643
+ if (!live.length) return out;
644
+ const calls = live.flatMap((t) => [
645
+ { address: t.pool, abi: poolAbi, functionName: "liquidity" },
646
+ { address: t.pool, abi: poolAbi, functionName: "slot0" },
647
+ { address: t.a, abi: erc20Abi2, functionName: "balanceOf", args: [t.pool] },
648
+ { address: t.b, abi: erc20Abi2, functionName: "balanceOf", args: [t.pool] }
649
+ ]);
650
+ const st = await c.multicall({ contracts: calls, allowFailure: true });
651
+ live.forEach((t, i) => {
652
+ const [l, s, ba, bb] = st.slice(i * 4, i * 4 + 4);
653
+ const ok4 = l.status === "success" && s.status === "success" && ba.status === "success" && bb.status === "success";
654
+ const base = { pool: t.pool, tokenA: t.a, tokenB: t.b, fee: t.fee, liquidity: ok4 ? l.result : 0n, sqrtPriceX96: ok4 ? s.result[0] : 0n, balA: ok4 ? ba.result : 0n, balB: ok4 ? bb.result : 0n };
655
+ const v = ok4 ? poolVerdict(base, hypeUsd2) : { ok: false, tvlUsd: null, reason: "pool state unreadable" };
656
+ out.set(key(t.a, t.b, t.fee), { ...base, ...v });
657
+ });
658
+ return out;
659
+ }
660
+ function midOf(p, tokenIn) {
661
+ if (p.sqrtPriceX96 === 0n) return null;
662
+ const sq = Number(p.sqrtPriceX96) / 2 ** 96, p01 = sq * sq;
663
+ return tokenIn.toLowerCase() < (sameAddr(tokenIn, p.tokenA) ? p.tokenB : p.tokenA).toLowerCase() ? p01 : 1 / p01;
664
+ }
665
+ function referenceOut(tIn, tOut, amountIn, hypeUsd2) {
666
+ const pi = usdPrice(tIn.address, hypeUsd2), po = usdPrice(tOut.address, hypeUsd2);
667
+ if (pi == null || po == null) return null;
668
+ const outFloat = Number(amountIn) / 10 ** tIn.decimals * (pi / po) * 10 ** tOut.decimals;
669
+ return Number.isFinite(outFloat) && outFloat > 0 ? BigInt(Math.floor(outFloat)) : null;
670
+ }
671
+ function filterRoutes(routes, refOut, maxImpactPct) {
672
+ const rejected = [];
673
+ let kept = routes;
674
+ if (refOut != null) {
675
+ kept = kept.filter((r) => {
676
+ const lo = Number(refOut) * (1 - (maxImpactPct + r.feePct + REF_SLACK_PCT) / 100), hi = Number(refOut) * (1 + REF_ABOVE_PCT / 100), o = Number(r.amountOut);
677
+ if (o < lo) {
678
+ rejected.push({ path: r.path, fees: r.fees, reason: `output ${(100 * (1 - o / Number(refOut))).toFixed(1)}% below the reference price` });
679
+ return false;
914
680
  }
915
- const addr = zeroAddr(r.result) ? null : r.result;
916
- if (x.t === "v2pair") pairs.set(x.key, addr);
917
- else {
918
- pools.set(x.key, addr);
919
- poolCache.set(x.key, { pool: addr, at: now });
681
+ if (o > hi) {
682
+ rejected.push({ path: r.path, fees: r.fees, reason: `output ${(100 * (o / Number(refOut) - 1)).toFixed(1)}% above the reference price` });
683
+ return false;
920
684
  }
685
+ return true;
921
686
  });
922
687
  }
923
- for (const { venue, paths } of active.filter((p) => p.venue.kind === "v2")) {
924
- let best = null, anyComplete = false;
925
- for (const path of paths) {
926
- const hops = path.slice(0, -1).map((a, i) => pairs.get(`${venue.id}|${sortedKey(a, path[i + 1])}`) ?? null);
927
- if (hops.some((h) => !h)) continue;
928
- anyComplete = true;
929
- const o = v2out.get(`${venue.id}|${path.join(",")}`);
930
- if (o !== void 0 && o > 0n && (!best || o > best.amountOutRaw)) best = { venue, kind: "v2", path, fees: null, pools: hops, amountInRaw, amountOutRaw: o, priceImpactPct: null, gasEstimate: null };
931
- }
932
- done(venue, best, best ? null : rpcFailed && !anyComplete ? "rpc_error" : anyComplete ? "no_liquidity" : "no_pool", best ? null : anyComplete ? "A pool exists but cannot return anything for this amount (empty or drained)." : null);
933
- }
934
- const cands = [];
935
- const v3Complete = /* @__PURE__ */ new Set();
936
- for (const { venue, paths } of active.filter((p) => p.venue.kind === "v3")) {
937
- for (const path of paths) {
938
- const perHop = path.slice(0, -1).map((a, i) => V3_PATH_FEES(venue).flatMap((fee) => {
939
- const p = pools.get(`${venue.id}|${sortedKey(a, path[i + 1])}|${fee}`);
940
- return p ? [{ fee, pool: p }] : [];
941
- }));
942
- if (perHop.some((h) => !h.length)) continue;
943
- v3Complete.add(venue.id);
944
- const combos = perHop.reduce((acc, hop) => acc.flatMap((c0) => hop.map((h) => [...c0, h])), [[]]);
945
- for (const combo of combos) cands.push({ venue, path, fees: combo.map((x) => x.fee), hopPools: combo.map((x) => x.pool) });
946
- }
688
+ if (kept.length > 1) {
689
+ const anchor = [...kept].sort((x, y) => (y.minTvlUsd ?? 0) - (x.minTvlUsd ?? 0))[0];
690
+ kept = kept.filter((r) => {
691
+ const dev = Math.abs(Number(r.amountOut) / Number(anchor.amountOut) - 1) * 100;
692
+ if (dev > CLUSTER_PCT) {
693
+ rejected.push({ path: r.path, fees: r.fees, reason: `output differs ${dev.toFixed(0)}% from the deepest route` });
694
+ return false;
695
+ }
696
+ return true;
697
+ });
947
698
  }
948
- if (cands.length) {
949
- const viaQuoter = cands.filter((x) => x.venue.quoter), viaLocal = cands.filter((x) => !x.venue.quoter);
950
- const [qr, lr] = await Promise.all([
951
- viaQuoter.length ? multicallChecked(c, viaQuoter.map((x) => x.path.length === 2 ? { address: x.venue.quoter, abi: quoterV2Abi, functionName: "quoteExactInputSingle", args: [{ tokenIn: x.path[0], tokenOut: x.path[1], amountIn: amountInRaw, fee: x.fees[0], sqrtPriceLimitX96: 0n }] } : { address: x.venue.quoter, abi: quoterV2Abi, functionName: "quoteExactInput", args: [encodeV3Path(x.path, x.fees), amountInRaw] })) : null,
952
- viaLocal.length ? quoteV3Local(c, viaLocal, amountInRaw) : null
953
- ]);
954
- const down = /* @__PURE__ */ new Set();
955
- const outs = [];
956
- if (qr) {
957
- if (qr.rpcDown) for (const x of viaQuoter) down.add(x.venue.id);
958
- viaQuoter.forEach((x, i) => {
959
- const r = qr.res[i];
960
- if (r.status === "success") {
961
- const t = r.result;
962
- outs.push({ cand: x, out: t[0], gas: t[3] });
963
- }
964
- });
965
- }
966
- if (lr) {
967
- if (lr.rpcDown) for (const x of viaLocal) down.add(x.venue.id);
968
- viaLocal.forEach((x, i) => {
969
- const o = lr.outs[i];
970
- if (o !== null) outs.push({ cand: x, out: o, gas: null });
971
- });
972
- }
973
- if (down.size) rpcFailed = true;
974
- const best = /* @__PURE__ */ new Map();
975
- for (const { cand: x, out: o, gas } of outs) {
976
- const cur = best.get(x.venue.id);
977
- if (o > 0n && (!cur || o > cur.amountOutRaw)) best.set(x.venue.id, { venue: x.venue, kind: "v3", path: x.path, fees: x.fees, pools: x.hopPools, amountInRaw, amountOutRaw: o, priceImpactPct: null, gasEstimate: gas });
699
+ return { kept, rejected };
700
+ }
701
+ async function quoteMainnet(c, tIn, tOut, amountIn, o) {
702
+ const empty = (reason, extra = {}) => ({ best: null, routes: [], rejected: [], pools: [], refOut: null, reason, ...extra });
703
+ const a = wrapped(tIn), b = wrapped(tOut);
704
+ if (sameAddr(a, b) || amountIn <= 0n) return empty("same_token");
705
+ const maxImpact = o.maxImpactPct ?? 3;
706
+ const cands = candidatePaths(a, b).flatMap((path) => feeCombos(path.length - 1).map((fees) => ({ path, fees })));
707
+ let pools;
708
+ try {
709
+ pools = await loadPools(c, cands.flatMap((x) => x.path.slice(0, -1).map((p, i) => ({ a: p, b: x.path[i + 1], fee: x.fees[i] }))), o.hypeUsd);
710
+ } catch {
711
+ return empty("rpc_error");
712
+ }
713
+ const rejected = [];
714
+ const usable = [];
715
+ let anyPool = false, anyLive = false;
716
+ for (const x of cands) {
717
+ const hopInfo = x.path.slice(0, -1).map((p, i) => pools.get(key(p, x.path[i + 1], x.fees[i])));
718
+ if (hopInfo.some((h) => !h)) continue;
719
+ anyPool = true;
720
+ const bad = hopInfo.find((h) => !h.ok);
721
+ if (bad) {
722
+ rejected.push({ path: x.path, fees: x.fees, reason: `pool ${bad.pool.slice(0, 8)}\u2026 (fee ${bad.fee / 1e4}%): ${bad.reason}` });
723
+ continue;
978
724
  }
979
- for (const { venue } of active.filter((p) => p.venue.kind === "v3")) {
980
- const q = best.get(venue.id) ?? null;
981
- const isDown = down.has(venue.id);
982
- done(venue, q, q ? null : isDown ? "rpc_error" : v3Complete.has(venue.id) ? "no_liquidity" : rpcFailed ? "rpc_error" : "no_pool", q ? null : v3Complete.has(venue.id) && !isDown ? "Pools exist but none has liquidity in range for this amount." : null);
725
+ anyLive = true;
726
+ usable.push({ ...x, hopInfo });
727
+ }
728
+ const refOut = referenceOut(tIn, tOut, amountIn, o.hypeUsd);
729
+ const base = { rejected, pools: [...pools.values()], refOut };
730
+ if (!usable.length) return empty(anyPool ? rejected.some((r) => /dust/.test(r.reason)) ? "dust" : "no_liquidity" : "no_pool", base);
731
+ const q = await c.multicall({ contracts: usable.map((x) => x.path.length === 2 ? { address: HYPERSWAP.quoter, abi: quoterAbi, functionName: "quoteExactInputSingle", args: [{ tokenIn: x.path[0], tokenOut: x.path[1], amountIn, fee: x.fees[0], sqrtPriceLimitX96: 0n }] } : { address: HYPERSWAP.quoter, abi: quoterAbi, functionName: "quoteExactInput", args: [encodePath(x.path, x.fees), amountIn] }), allowFailure: true });
732
+ if (q.length && q.every((r) => r.status === "failure" && !/revert|returned no data|zero data|LOK|SPL/i.test(String(r.error?.message)))) return empty("rpc_error", base);
733
+ const routes = [];
734
+ usable.forEach((x, i) => {
735
+ const r = q[i];
736
+ if (r.status !== "success" || !(r.result[0] > 0n)) {
737
+ rejected.push({ path: x.path, fees: x.fees, reason: "the quoter reverted (not enough liquidity for this amount)" });
738
+ return;
983
739
  }
984
- } else {
985
- for (const { venue } of active.filter((p) => p.venue.kind === "v3")) done(venue, null, rpcFailed ? "rpc_error" : "no_pool");
740
+ let mid = 1, okMid = true;
741
+ x.hopInfo.forEach((h, k) => {
742
+ const m = midOf(h, x.path[k]);
743
+ if (m == null) okMid = false;
744
+ else mid *= m;
745
+ });
746
+ const midOutFee = Number(amountIn) * mid * feeFactor(x.fees);
747
+ const impactPct = okMid && midOutFee > 0 ? Math.max(0, (1 - Number(r.result[0]) / midOutFee) * 100) : null;
748
+ routes.push({
749
+ path: x.path,
750
+ fees: x.fees,
751
+ pools: x.hopInfo.map((h) => h.pool),
752
+ amountIn,
753
+ amountOut: r.result[0],
754
+ gas: r.result[3],
755
+ impactPct,
756
+ feePct: (1 - feeFactor(x.fees)) * 100,
757
+ minTvlUsd: Math.min(...x.hopInfo.map((h) => h.tvlUsd ?? 0)),
758
+ hopInfo: x.hopInfo
759
+ });
760
+ });
761
+ const f = filterRoutes(routes, refOut, maxImpact);
762
+ rejected.push(...f.rejected);
763
+ const sorted = [...f.kept].sort((p, s) => p.amountOut === s.amountOut ? 0 : p.amountOut > s.amountOut ? -1 : 1);
764
+ if (!sorted.length) return empty(routes.length ? "off_reference" : anyLive ? "no_liquidity" : "no_pool", base);
765
+ return { best: sorted[0], routes: sorted, reason: null, ...base };
766
+ }
767
+ function tradeUsd(tIn, tOut, amountIn, amountOut, hypeUsd2) {
768
+ const pi = usdPrice(tIn.address, hypeUsd2);
769
+ if (pi != null) return Number(amountIn) / 10 ** tIn.decimals * pi;
770
+ const po = usdPrice(tOut.address, hypeUsd2);
771
+ if (po != null) return Number(amountOut) / 10 ** tOut.decimals * po;
772
+ return null;
773
+ }
774
+
775
+ // src/shared/hyperswap/guard.ts
776
+ function noRouteText(r) {
777
+ switch (r.reason) {
778
+ case "same_token":
779
+ return "Pick two different tokens.";
780
+ case "rpc_error":
781
+ return "Could not read HyperEVM right now (RPC error). Try again in a moment.";
782
+ case "dust":
783
+ return "Only tiny pools exist for this pair (under $5,000 of liquidity), so it is not offered: their prices are unreliable.";
784
+ case "off_reference":
785
+ return "Every route is priced far from the Hyperliquid reference price, so none is offered.";
786
+ case "no_liquidity":
787
+ return "Pools exist but have no liquidity for this amount. Try a smaller amount.";
788
+ case "no_pool":
789
+ return "No HyperSwap V3 pool for this pair, directly or through WHYPE or USDC.";
790
+ default:
791
+ return "No route.";
986
792
  }
987
- return venues.map((v) => out.get(v.id));
988
793
  }
989
- var byOutDesc = (x, y) => x.amountOutRaw === y.amountOutRaw ? 0 : x.amountOutRaw > y.amountOutRaw ? -1 : 1;
990
- async function quoteAllRoutes(c, tokenIn, tokenOut3, amountInRaw, venues = SWAP_VENUES) {
991
- const r = await quoteVenues(c, tokenIn, tokenOut3, amountInRaw, venues);
992
- return r.flatMap((x) => x.quote ? [x.quote] : []).sort(byOutDesc);
794
+
795
+ // src/shared/hyperswap/market.ts
796
+ function poolPriceUsd(p, token, hypeUsd2) {
797
+ if (p.sqrtPriceX96 === 0n) return null;
798
+ const counter = sameAddr(p.tokenA, token.address === "native" ? WHYPE : token.address) ? p.tokenB : p.tokenA;
799
+ const cpx = usdPrice(counter, hypeUsd2);
800
+ if (cpx == null) return null;
801
+ const cdec = tokenBy(counter)?.decimals ?? 18;
802
+ const sq = Number(p.sqrtPriceX96) / 2 ** 96, p01 = sq * sq;
803
+ const tokenIs0 = sameAddr(p.tokenA, token.address) ? p.tokenA.toLowerCase() < p.tokenB.toLowerCase() : p.tokenA.toLowerCase() > p.tokenB.toLowerCase();
804
+ const dTok = token.decimals;
805
+ const inCounter = (tokenIs0 ? p01 : 1 / p01) * 10 ** (dTok - cdec);
806
+ return Number.isFinite(inCounter) ? inCounter * cpx : null;
807
+ }
808
+ async function loadMarket(c, hypeUsd2, tokens = MAINNET_TOKENS) {
809
+ const list = tokens.filter((t) => t.address !== "native");
810
+ const usdc = USDC.address;
811
+ const triples = [];
812
+ for (const t of list) for (const counter of [WHYPE, usdc]) if (!sameAddr(t.address, counter)) for (const fee of FEE_TIERS) triples.push({ a: t.address, b: counter, fee });
813
+ const pools = await loadPools(c, triples, hypeUsd2);
814
+ const rows = list.map((t) => {
815
+ const mine = [...pools.values()].filter((p) => sameAddr(p.tokenA, t.address) && (sameAddr(p.tokenB, WHYPE) || sameAddr(p.tokenB, usdc)) || sameAddr(p.tokenB, t.address) && (sameAddr(p.tokenA, WHYPE) || sameAddr(p.tokenA, usdc)));
816
+ const sane = mine.filter((p) => p.ok).sort((x, y) => (y.tvlUsd ?? 0) - (x.tvlUsd ?? 0));
817
+ const best = sane[0] ?? null;
818
+ return {
819
+ token: t,
820
+ priceUsd: best ? poolPriceUsd(best, t, hypeUsd2) : null,
821
+ tvlUsd: sane.reduce((s, p) => s + (p.tvlUsd ?? 0), 0),
822
+ poolCount: sane.length,
823
+ dustCount: mine.length - sane.length,
824
+ health: sane.length ? "ok" : mine.length ? "thin" : "none",
825
+ bestPool: best,
826
+ fee: best?.fee ?? null
827
+ };
828
+ });
829
+ return rows.sort((x, y) => y.tvlUsd - x.tvlUsd);
993
830
  }
994
- async function routeImpactPct(c, q) {
831
+
832
+ // src/market/reads.ts
833
+ var hlNet = () => env.network;
834
+ var CANDLE_INTERVALS = ["1m", "5m", "15m", "1h", "4h", "1d"];
835
+ var SORT_KEYS = ["volume", "openInterest", "change", "funding"];
836
+ var rnd = (x, d = 4) => x == null || !Number.isFinite(x) ? null : Number(x.toFixed(d));
837
+ var safeSymbol = (s) => s.replace(/[^A-Za-z0-9._$₮-]/g, "").slice(0, 12) || "?";
838
+ async function hypeUsd() {
995
839
  try {
996
- const hops = q.path.slice(0, -1).map((a, i) => ({ a, pool: q.pools[i] }));
997
- const contracts = [];
998
- for (const { pool } of hops) {
999
- if (q.kind === "v2") contracts.push({ address: pool, abi: pairAbi, functionName: "token0" }, { address: pool, abi: pairAbi, functionName: "getReserves" });
1000
- else contracts.push({ address: pool, abi: v3PoolAbi, functionName: "slot0" });
1001
- }
1002
- const data = await c.multicall({ contracts, allowFailure: true });
1003
- let mid = 1, k = 0;
1004
- for (let i = 0; i < hops.length; i++) {
1005
- if (q.kind === "v2") {
1006
- const t0 = data[k++], r = data[k++];
1007
- if (t0.status !== "success" || r.status !== "success") return null;
1008
- const [r0, r1] = r.result;
1009
- const inIs0 = same(t0.result, hops[i].a);
1010
- const rIn = inIs0 ? r0 : r1, rOut = inIs0 ? r1 : r0;
1011
- if (rIn === 0n) return null;
1012
- mid *= Number(rOut) / Number(rIn);
1013
- } else {
1014
- const s = data[k++];
1015
- if (s.status !== "success") return null;
1016
- const sqrt = Number(s.result[0]) / 2 ** 96;
1017
- if (!(sqrt > 0)) return null;
1018
- const p01 = sqrt * sqrt;
1019
- mid *= hops[i].a.toLowerCase() < q.path[i + 1].toLowerCase() ? p01 : 1 / p01;
1020
- }
1021
- }
1022
- const midOut = Number(q.amountInRaw) * mid;
1023
- return midOut > 0 ? Math.max(0, (1 - Number(q.amountOutRaw) / midOut) * 100) : null;
840
+ const r = await fetch("https://api.hyperliquid.xyz/info", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ type: "allMids" }) });
841
+ if (!r.ok) return null;
842
+ const px = Number((await r.json())?.HYPE);
843
+ return Number.isFinite(px) && px > 0.01 && px < 1e5 ? px : null;
1024
844
  } catch {
1025
845
  return null;
1026
846
  }
1027
847
  }
1028
- async function quoteBestRoute(c, tokenIn, tokenOut3, amountInRaw, venues = SWAP_VENUES) {
1029
- const all = await quoteAllRoutes(c, tokenIn, tokenOut3, amountInRaw, venues);
1030
- if (!all.length) return null;
1031
- const best = all[0];
1032
- best.priceImpactPct = await routeImpactPct(c, best);
1033
- return best;
848
+ async function marketTokens() {
849
+ const ref = await hypeUsd();
850
+ const rows = await loadMarket(hyperClient(), ref);
851
+ return {
852
+ network: "HyperEVM mainnet (chain 999), HyperSwap V3",
853
+ hypeUsdReference: ref ? { price: ref, source: "Hyperliquid mainnet HYPE mid (reference only)" } : null,
854
+ note: "Verified tokens only (HYPE/WHYPE, USDC, USDT0, UETH, USDe, USDH, USDT, APE). Prices come from the deepest sane pool (>= $5,000 TVL); no 24h change (needs an indexer). Not every token on the chain.",
855
+ tokens: rows.map((r) => ({ symbol: safeSymbol(r.token.symbol), address: r.token.address, priceUsd: rnd(r.priceUsd, 6), tvlUsd: rnd(r.tvlUsd, 0), pools: r.poolCount, dustPools: r.dustCount, health: r.health, feeTier: r.fee, explorer: explorerToken(r.token.address) }))
856
+ };
857
+ }
858
+ async function resolveToken(q) {
859
+ const s = q.trim();
860
+ if (/^(hype|native)$/i.test(s)) return MAINNET_TOKENS[0];
861
+ if (isAddress2(s)) {
862
+ const a = getAddress4(s), k = tokenBy(a);
863
+ if (k) return k;
864
+ return readTokenMeta(a);
865
+ }
866
+ const hits = MAINNET_TOKENS.filter((t) => t.symbol.toLowerCase() === s.toLowerCase());
867
+ if (hits.length === 1) return hits[0];
868
+ throw new Error(`Unknown token "${cleanLabel(s, 40)}". Use HYPE, a token address, or one of: ${MAINNET_TOKENS.map((t) => t.symbol).join(", ")}.`);
869
+ }
870
+ async function quoteSwap(tokenInQ, tokenOutQ, amount2) {
871
+ if (!/^\d+(\.\d+)?$/.test(amount2.trim())) throw new Error('amount must be a positive decimal number string, e.g. "1.5"');
872
+ const [tin, tout] = await Promise.all([resolveToken(tokenInQ), resolveToken(tokenOutQ)]);
873
+ const amountIn = parseUnits(amount2.trim(), tin.decimals);
874
+ if (amountIn <= 0n) throw new Error("amount must be greater than zero");
875
+ const ref = await hypeUsd();
876
+ const q = await quoteMainnet(hyperClient(), tin, tout, amountIn, { hypeUsd: ref, maxImpactPct: 10 });
877
+ if (!q.best) throw new Error(`No route between ${tin.symbol} and ${tout.symbol}: ${noRouteText(q)}`);
878
+ const b = q.best, usd2 = tradeUsd(tin, tout, amountIn, b.amountOut, ref);
879
+ const label2 = (a, i) => i === 0 && isNative(tin) || i === b.path.length - 1 && isNative(tout) ? "HYPE" : tokenBy(a)?.symbol ?? `${a.slice(0, 6)}\u2026`;
880
+ return {
881
+ venue: "HyperSwap V3 on HyperEVM mainnet (third-party)",
882
+ network: "HyperEVM mainnet (chain 999)",
883
+ tokenIn: { symbol: safeSymbol(tin.symbol), address: tin.address, decimals: tin.decimals, verified: tin.verified },
884
+ tokenOut: { symbol: safeSymbol(tout.symbol), address: tout.address, decimals: tout.decimals, verified: tout.verified },
885
+ amountIn: fmtRaw(amountIn, tin.decimals, 8),
886
+ amountOut: fmtRaw(b.amountOut, tout.decimals, 8),
887
+ amountOutRaw: b.amountOut.toString(),
888
+ route: b.path.map(label2).join(" -> "),
889
+ feeTiers: b.fees,
890
+ priceImpactPct: rnd(b.impactPct, 3),
891
+ tradeUsdApprox: rnd(usd2, 2),
892
+ warning: !tin.verified || !tout.verified ? "An unverified token is involved: it was read from the chain and nobody vetted it." : b.impactPct != null && b.impactPct > 5 ? `Price impact ${b.impactPct.toFixed(2)}% is above 5%: liquidity is thin.` : null,
893
+ note: "Quote only. No transaction is built or sent; swapping is done in the Zypher Agent app, signed in your own wallet."
894
+ };
895
+ }
896
+ var label = (_net) => "Hyperliquid mainnet";
897
+ var marketOut = (m) => ({ coin: m.name, markPx: rnd(m.markPx, 6), change24hPct: rnd(m.change24hPct, 2), volume24hUsd: rnd(m.volume24hUsd, 0), openInterestUsd: rnd(m.openInterestUsd, 0), fundingHourlyPct: rnd(m.fundingHourly * 100, 5), maxLeverage: m.maxLeverage, onlyIsolated: m.onlyIsolated });
898
+ async function perpMarkets(limit = 15, sortBy = "volume", net = hlNet()) {
899
+ const all = await getPerpMarkets(net);
900
+ const key2 = { volume: (m) => m.volume24hUsd, openInterest: (m) => m.openInterestUsd, change: (m) => Math.abs(m.change24hPct ?? 0), funding: (m) => Math.abs(m.fundingHourly) };
901
+ const rows = all.filter((m) => Number.isFinite(m.markPx)).sort((a, b) => key2[sortBy](b) - key2[sortBy](a));
902
+ return { network: label(net), total: rows.length, sortedBy: sortBy, markets: rows.slice(0, limit).map(marketOut) };
903
+ }
904
+ async function perpCandles(coin, interval, count, net = hlNet()) {
905
+ const all = await getPerpMarkets(net), q = coin.trim().toLowerCase();
906
+ const m = all.find((x) => x.name.toLowerCase() === q) ?? all.find((x) => x.symbol.toLowerCase() === q);
907
+ if (!m) throw new Error(`Market ${coin.slice(0, 20)} not found on ${label(net)}`);
908
+ const cs = await getCandles(net, m.name, interval, count);
909
+ if (!cs.length) return { coin: m.name, interval, candles: [], summary: null };
910
+ const closes = cs.map((c) => c.close), sma = (n3) => closes.length >= n3 ? closes.slice(-n3).reduce((a, b) => a + b, 0) / n3 : null;
911
+ const first = cs[0], last = cs[cs.length - 1];
912
+ return {
913
+ network: label(net),
914
+ coin: m.name,
915
+ interval,
916
+ count: cs.length,
917
+ summary: { from: new Date(first.time * 1e3).toISOString(), to: new Date(last.time * 1e3).toISOString(), firstClose: first.close, lastClose: last.close, changePct: rnd((last.close - first.close) / first.close * 100, 2), high: Math.max(...cs.map((c) => c.high)), low: Math.min(...cs.map((c) => c.low)), avgVolume: rnd(cs.reduce((s, c) => s + c.volume, 0) / cs.length, 2), sma20: rnd(sma(20), 6), sma50: rnd(sma(50), 6) },
918
+ last20: cs.slice(-20).map((c) => ({ t: new Date(c.time * 1e3).toISOString(), o: c.open, h: c.high, l: c.low, c: c.close, v: rnd(c.volume, 2) }))
919
+ };
1034
920
  }
1035
- function noRouteMessage(results) {
1036
- if (results.every((r) => r.reason === "same_token")) return "Pick two different tokens.";
1037
- const rpc = results.filter((r) => r.reason === "rpc_error");
1038
- if (rpc.length) return `Could not read ${rpc.map((r) => r.venue.name).join(", ")} right now (RPC error). Try again in a moment.`;
1039
- const noLiq = results.filter((r) => r.reason === "no_liquidity");
1040
- if (noLiq.length) return `A pool exists on ${noLiq.map((r) => r.venue.name).join(", ")} but it has no liquidity for this amount. Try a smaller amount.`;
1041
- return `No pool for this pair on ${results.map((r) => r.venue.name).join(", ")}. The token may trade on a venue Zypher does not read (see Market).`;
1042
- }
1043
-
1044
- // src/market/explorer-snapshot.json
1045
- var explorer_snapshot_default = {
1046
- snapshotAt: "2026-11-02",
1047
- source: "https://testnet.elysium.kinetiq.xyz/tokens",
1048
- note: "Static snapshot of the Elysium testnet explorer's token list (the explorer's own order, which is by market cap, at snapshot time). Prices, volume, market cap and holders come from the explorer, are NOT live, and use mainnet HYPE prices; the explorer values are not tradeable quotes. Symbol, name, decimals and total supply were read on-chain at snapshot time.",
1049
- skipped: [],
1050
- tokens: [
1051
- {
1052
- rank: 1,
1053
- address: "0x8a2201953AF29e8BE7700bcB53dA3ce5caD18726",
1054
- symbol: "KALOS",
1055
- name: "KALOS",
1056
- decimals: 18,
1057
- totalSupply: "1000000000000000000000000000",
1058
- explorer: {
1059
- priceUsd: 8.817,
1060
- volume24hUsd: 121270238,
1061
- marketCapUsd: 8817455758,
1062
- holders: 84614
1063
- },
1064
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x8a2201953AF29e8BE7700bcB53dA3ce5caD18726"
1065
- },
1066
- {
1067
- rank: 2,
1068
- address: "0xD66D21D3c62ef6ABfC0505894DcF29ea20D4A7f2",
1069
- symbol: "ELYZ",
1070
- name: "Elyziuma",
1071
- decimals: 18,
1072
- totalSupply: "34330000000000000000000000",
1073
- explorer: {
1074
- priceUsd: 48.1,
1075
- volume24hUsd: 114671190,
1076
- marketCapUsd: 1651344748,
1077
- holders: 5456
1078
- },
1079
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0xD66D21D3c62ef6ABfC0505894DcF29ea20D4A7f2"
1080
- },
1081
- {
1082
- rank: 3,
1083
- address: "0x7D29d8047B905000459c0E80c34a26CeedcB47b2",
1084
- symbol: "USDC",
1085
- name: "USDC",
1086
- decimals: 6,
1087
- totalSupply: "122987676992314",
1088
- explorer: {
1089
- priceUsd: 1,
1090
- volume24hUsd: 50726423,
1091
- marketCapUsd: 122976438,
1092
- holders: 36223
1093
- },
1094
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x7D29d8047B905000459c0E80c34a26CeedcB47b2"
1095
- },
1096
- {
1097
- rank: 4,
1098
- address: "0xed31816C8b553A0D3dCd8e052046b37ab8a98888",
1099
- symbol: "AGENT",
1100
- name: "Agent K",
1101
- decimals: 18,
1102
- totalSupply: "1000000000000000000000000000",
1103
- explorer: {
1104
- priceUsd: 0.01406,
1105
- volume24hUsd: 12,
1106
- marketCapUsd: 14058045,
1107
- holders: 3681
1108
- },
1109
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0xed31816C8b553A0D3dCd8e052046b37ab8a98888"
1110
- },
1111
- {
1112
- rank: 5,
1113
- address: "0xa7531D7bE19f29B991d97eaA17b034715a835e35",
1114
- symbol: "stKALOS",
1115
- name: "Staked KALOS",
1116
- decimals: 24,
1117
- totalSupply: "97304030930752182530071425191176",
1118
- explorer: {
1119
- priceUsd: 0.0911,
1120
- volume24hUsd: 5464566,
1121
- marketCapUsd: 8942513,
1122
- holders: 33022
1123
- },
1124
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0xa7531D7bE19f29B991d97eaA17b034715a835e35"
1125
- },
1126
- {
1127
- rank: 6,
1128
- address: "0x852043605E2F2F326F6Bed69E5e5Baf970366473",
1129
- symbol: "tUSDC",
1130
- name: "Test USDC",
1131
- decimals: 6,
1132
- totalSupply: "7580830000001",
1133
- explorer: {
1134
- priceUsd: 1.001,
1135
- volume24hUsd: 2018,
1136
- marketCapUsd: 7588414,
1137
- holders: 28
1138
- },
1139
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x852043605E2F2F326F6Bed69E5e5Baf970366473"
1140
- },
1141
- {
1142
- rank: 7,
1143
- address: "0xCD57F65c2B0e5881CFC2E609F7CD53b746E1F234",
1144
- symbol: "WHYPE",
1145
- name: "WHYPE",
1146
- decimals: 18,
1147
- totalSupply: "70061170905852787795951",
1148
- explorer: {
1149
- priceUsd: 85.73,
1150
- volume24hUsd: 20697630,
1151
- marketCapUsd: 6287460,
1152
- holders: 55022
1153
- },
1154
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0xCD57F65c2B0e5881CFC2E609F7CD53b746E1F234"
1155
- },
1156
- {
1157
- rank: 8,
1158
- address: "0x01c90bceD829cC4c7D66dDCDA905b8c012348888",
1159
- symbol: "CHNZY",
1160
- name: "Chainzy",
1161
- decimals: 18,
1162
- totalSupply: "1000000000000000000000000000",
1163
- explorer: {
1164
- priceUsd: 3592e-6,
1165
- volume24hUsd: 74,
1166
- marketCapUsd: 3592020,
1167
- holders: 2816
1168
- },
1169
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x01c90bceD829cC4c7D66dDCDA905b8c012348888"
1170
- },
1171
- {
1172
- rank: 9,
1173
- address: "0x17b89c6B503E4477Fc48b0Da015ff2517Efd4313",
1174
- symbol: "ZSx",
1175
- name: "Zscaler xStock",
1176
- decimals: 18,
1177
- totalSupply: "1000000000000000000000000000",
1178
- explorer: {
1179
- priceUsd: 1026e-6,
1180
- volume24hUsd: 0,
1181
- marketCapUsd: 1025521,
1182
- holders: 14
1183
- },
1184
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x17b89c6B503E4477Fc48b0Da015ff2517Efd4313"
1185
- },
1186
- {
1187
- rank: 10,
1188
- address: "0x8aa6F0835805Ca99f370634415d502ec5c603Baf",
1189
- symbol: "GCR",
1190
- name: "Gigantic Rebirth",
1191
- decimals: 18,
1192
- totalSupply: "1000000000000000000000000000",
1193
- explorer: {
1194
- priceUsd: 5351e-7,
1195
- volume24hUsd: 619,
1196
- marketCapUsd: 535075,
1197
- holders: 16216
1198
- },
1199
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x8aa6F0835805Ca99f370634415d502ec5c603Baf"
1200
- },
1201
- {
1202
- rank: 11,
1203
- address: "0xD20b24f05930f114100D108a481c4eC022F40DaA",
1204
- symbol: "WHYPE",
1205
- name: "Wrapped HYPE",
1206
- decimals: 18,
1207
- totalSupply: "5242720082547716694381",
1208
- explorer: {
1209
- priceUsd: 85.73,
1210
- volume24hUsd: 184,
1211
- marketCapUsd: 449437,
1212
- holders: 1900
1213
- },
1214
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0xD20b24f05930f114100D108a481c4eC022F40DaA"
1215
- },
1216
- {
1217
- rank: 12,
1218
- address: "0x3F764aEEFf3d5C4eE3867A34AEe3DE082CD70999",
1219
- symbol: "D/ACC",
1220
- name: "Decelerate",
1221
- decimals: 18,
1222
- totalSupply: "1000000000000000000000000000",
1223
- explorer: {
1224
- priceUsd: 4395e-7,
1225
- volume24hUsd: 0,
1226
- marketCapUsd: 439516,
1227
- holders: 6
1228
- },
1229
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x3F764aEEFf3d5C4eE3867A34AEe3DE082CD70999"
1230
- },
1231
- {
1232
- rank: 13,
1233
- address: "0xb11B0C0fDcEb7Ddc12aa41914FDb60be27BCf7E9",
1234
- symbol: "XIAOx",
1235
- name: "Xiaomi xStock",
1236
- decimals: 18,
1237
- totalSupply: "1000000000000000000000000000",
1238
- explorer: {
1239
- priceUsd: 3263e-7,
1240
- volume24hUsd: 1015,
1241
- marketCapUsd: 326314,
1242
- holders: 9
1243
- },
1244
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0xb11B0C0fDcEb7Ddc12aa41914FDb60be27BCf7E9"
1245
- },
1246
- {
1247
- rank: 14,
1248
- address: "0x420bf11ef5E5dE04001cfe0a55243587D0690999",
1249
- symbol: "grok.ely",
1250
- name: "grok elysium",
1251
- decimals: 18,
1252
- totalSupply: "1000000000000000000000000000",
1253
- explorer: {
1254
- priceUsd: 2587e-7,
1255
- volume24hUsd: 0,
1256
- marketCapUsd: 258723,
1257
- holders: 3
1258
- },
1259
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x420bf11ef5E5dE04001cfe0a55243587D0690999"
1260
- },
1261
- {
1262
- rank: 15,
1263
- address: "0xDb18514962929998D900968Cf0AC1d4681c80b20",
1264
- symbol: "JELLYJELLY",
1265
- name: "geleia",
1266
- decimals: 18,
1267
- totalSupply: "1000000000000000000000000000",
1268
- explorer: {
1269
- priceUsd: 2448e-7,
1270
- volume24hUsd: 0,
1271
- marketCapUsd: 244848,
1272
- holders: 2637
1273
- },
1274
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0xDb18514962929998D900968Cf0AC1d4681c80b20"
1275
- },
1276
- {
1277
- rank: 16,
1278
- address: "0x57bff2a7f1D3f9716C71E3c12927a7Ae141DCA98",
1279
- symbol: "HPM",
1280
- name: "HyperMount",
1281
- decimals: 18,
1282
- totalSupply: "1000000000000000000000000000",
1283
- explorer: {
1284
- priceUsd: 2082e-7,
1285
- volume24hUsd: 69,
1286
- marketCapUsd: 208232,
1287
- holders: 2570
1288
- },
1289
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x57bff2a7f1D3f9716C71E3c12927a7Ae141DCA98"
1290
- },
1291
- {
1292
- rank: 17,
1293
- address: "0xD27Fe7B07437603CA5032737dD5Ac7727247039E",
1294
- symbol: "MNC",
1295
- name: "MNC",
1296
- decimals: 18,
1297
- totalSupply: "1000000000000000000000000000",
1298
- explorer: {
1299
- priceUsd: 1977e-7,
1300
- volume24hUsd: 199,
1301
- marketCapUsd: 197721,
1302
- holders: 5507
1303
- },
1304
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0xD27Fe7B07437603CA5032737dD5Ac7727247039E"
1305
- },
1306
- {
1307
- rank: 18,
1308
- address: "0xCF81F8c46cA5fa52b91c65BfE36CbB6F172c8888",
1309
- symbol: "BLCOKY",
1310
- name: "blocky",
1311
- decimals: 18,
1312
- totalSupply: "1000000000000000000000000000",
1313
- explorer: {
1314
- priceUsd: 1721e-7,
1315
- volume24hUsd: 10,
1316
- marketCapUsd: 172063,
1317
- holders: 2043
1318
- },
1319
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0xCF81F8c46cA5fa52b91c65BfE36CbB6F172c8888"
1320
- },
1321
- {
1322
- rank: 19,
1323
- address: "0x686F0f544ab4F2B1029345C28B0fe70Af2ef3f18",
1324
- symbol: "ELYSIUM",
1325
- name: "ELYSIUM",
1326
- decimals: 18,
1327
- totalSupply: "1000000000000000000000000000",
1328
- explorer: {
1329
- priceUsd: 144e-6,
1330
- volume24hUsd: 50,
1331
- marketCapUsd: 143959,
1332
- holders: 2037
1333
- },
1334
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x686F0f544ab4F2B1029345C28B0fe70Af2ef3f18"
1335
- },
1336
- {
1337
- rank: 20,
1338
- address: "0x4edBAE1A9F21Dab3a1d402Ee1C9B402Cc44a012b",
1339
- symbol: "TFLAME",
1340
- name: "Test Flame",
1341
- decimals: 18,
1342
- totalSupply: "1000000000000000000000000000",
1343
- explorer: {
1344
- priceUsd: 1428e-7,
1345
- volume24hUsd: 17,
1346
- marketCapUsd: 141200,
1347
- holders: 3462
1348
- },
1349
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x4edBAE1A9F21Dab3a1d402Ee1C9B402Cc44a012b"
1350
- },
1351
- {
1352
- rank: 21,
1353
- address: "0x109aC6299f7a24c13Cd35d3e3433E23DeF35274D",
1354
- symbol: "ELASIA",
1355
- name: "elasia",
1356
- decimals: 18,
1357
- totalSupply: "1000000000000000000000000000",
1358
- explorer: {
1359
- priceUsd: 1388e-7,
1360
- volume24hUsd: 0,
1361
- marketCapUsd: 138775,
1362
- holders: 2158
1363
- },
1364
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x109aC6299f7a24c13Cd35d3e3433E23DeF35274D"
1365
- },
1366
- {
1367
- rank: 22,
1368
- address: "0x8bf7AeEd37bba4F195f36Eba47e06917202d6eEf",
1369
- symbol: "AERIS",
1370
- name: "LaFulanaDeMidgar",
1371
- decimals: 18,
1372
- totalSupply: "1000000000000000000000000000",
1373
- explorer: {
1374
- priceUsd: 1224e-7,
1375
- volume24hUsd: 17,
1376
- marketCapUsd: 122444,
1377
- holders: 6395
1378
- },
1379
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x8bf7AeEd37bba4F195f36Eba47e06917202d6eEf"
1380
- },
1381
- {
1382
- rank: 23,
1383
- address: "0x6cD1D7248422eb75cF15Dfa6ae7Cd01084050999",
1384
- symbol: "Omnia",
1385
- name: "Omnia",
1386
- decimals: 18,
1387
- totalSupply: "1000000000000000000000000000",
1388
- explorer: {
1389
- priceUsd: 3878e-7,
1390
- volume24hUsd: 25815,
1391
- marketCapUsd: 90357,
1392
- holders: 26
1393
- },
1394
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x6cD1D7248422eb75cF15Dfa6ae7Cd01084050999"
1395
- },
1396
- {
1397
- rank: 24,
1398
- address: "0x1690E986915126803890E3260a25e883d9acC17A",
1399
- symbol: "AOSx",
1400
- name: "A.O. Smith xStock",
1401
- decimals: 18,
1402
- totalSupply: "1000000000000000000000000000",
1403
- explorer: {
1404
- priceUsd: 8469e-8,
1405
- volume24hUsd: 10,
1406
- marketCapUsd: 84692,
1407
- holders: 4
1408
- },
1409
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x1690E986915126803890E3260a25e883d9acC17A"
1410
- },
1411
- {
1412
- rank: 25,
1413
- address: "0xE6E3d18068192dB61Cc84d6DeAcBca48903a0999",
1414
- symbol: "PEPE",
1415
- name: "Pepecoin",
1416
- decimals: 18,
1417
- totalSupply: "1000000000000000000000000000",
1418
- explorer: {
1419
- priceUsd: 7669e-8,
1420
- volume24hUsd: 10,
1421
- marketCapUsd: 76690,
1422
- holders: 6
1423
- },
1424
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0xE6E3d18068192dB61Cc84d6DeAcBca48903a0999"
1425
- },
1426
- {
1427
- rank: 26,
1428
- address: "0x40B54b590f5b84B44BdcED58aB9DD1d365D6243B",
1429
- symbol: "AAONx",
1430
- name: "AAON xStock",
1431
- decimals: 18,
1432
- totalSupply: "1000000000000000000000000000",
1433
- explorer: {
1434
- priceUsd: 7495e-8,
1435
- volume24hUsd: 10,
1436
- marketCapUsd: 74945,
1437
- holders: 4
1438
- },
1439
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x40B54b590f5b84B44BdcED58aB9DD1d365D6243B"
1440
- },
1441
- {
1442
- rank: 27,
1443
- address: "0x3fea9DAd2BFF72a7e97459609601C7EfDcf90859",
1444
- symbol: "ABTx",
1445
- name: "Abbott xStock",
1446
- decimals: 18,
1447
- totalSupply: "1000000000000000000000000000",
1448
- explorer: {
1449
- priceUsd: 7445e-8,
1450
- volume24hUsd: 10,
1451
- marketCapUsd: 74449,
1452
- holders: 4
1453
- },
1454
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x3fea9DAd2BFF72a7e97459609601C7EfDcf90859"
1455
- },
1456
- {
1457
- rank: 28,
1458
- address: "0x98F33efe032a69B354999e2b1abF193A7727B9b6",
1459
- symbol: "AGCOx",
1460
- name: "AGCO xStock",
1461
- decimals: 18,
1462
- totalSupply: "1000000000000000000000000000",
1463
- explorer: {
1464
- priceUsd: 7435e-8,
1465
- volume24hUsd: 0,
1466
- marketCapUsd: 74350,
1467
- holders: 3
1468
- },
1469
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x98F33efe032a69B354999e2b1abF193A7727B9b6"
1470
- },
1471
- {
1472
- rank: 29,
1473
- address: "0x2f1d24E8729c20A24beA548aAfE85f58f98af193",
1474
- symbol: "AFLx",
1475
- name: "Aflac xStock",
1476
- decimals: 18,
1477
- totalSupply: "1000000000000000000000000000",
1478
- explorer: {
1479
- priceUsd: 7435e-8,
1480
- volume24hUsd: 0,
1481
- marketCapUsd: 74350,
1482
- holders: 4
1483
- },
1484
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x2f1d24E8729c20A24beA548aAfE85f58f98af193"
1485
- },
1486
- {
1487
- rank: 30,
1488
- address: "0x58Cc0f50c20b73932e0D2CF959DE29326e2aB70B",
1489
- symbol: "MMMx",
1490
- name: "3M xStock",
1491
- decimals: 18,
1492
- totalSupply: "1000000000000000000000000000",
1493
- explorer: {
1494
- priceUsd: 7435e-8,
1495
- volume24hUsd: 0,
1496
- marketCapUsd: 74350,
1497
- holders: 3
1498
- },
1499
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x58Cc0f50c20b73932e0D2CF959DE29326e2aB70B"
1500
- },
1501
- {
1502
- rank: 31,
1503
- address: "0x213e31d9e5722Ee29567A3fB526123ffde78D457",
1504
- symbol: "PPLTx",
1505
- name: "abrdn Phys Platinum Shs xStock",
1506
- decimals: 18,
1507
- totalSupply: "1000000000000000000000000000",
1508
- explorer: {
1509
- priceUsd: 7435e-8,
1510
- volume24hUsd: 0,
1511
- marketCapUsd: 74350,
1512
- holders: 3
1513
- },
1514
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x213e31d9e5722Ee29567A3fB526123ffde78D457"
1515
- },
1516
- {
1517
- rank: 32,
1518
- address: "0x4A95DcF5c1f650D1E8b6EFb484DFD2C0f5Dd0EFc",
1519
- symbol: "ADM.GBx",
1520
- name: "Admiral xStock",
1521
- decimals: 18,
1522
- totalSupply: "1000000000000000000000000000",
1523
- explorer: {
1524
- priceUsd: 7435e-8,
1525
- volume24hUsd: 0,
1526
- marketCapUsd: 74350,
1527
- holders: 3
1528
- },
1529
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x4A95DcF5c1f650D1E8b6EFb484DFD2C0f5Dd0EFc"
1530
- },
1531
- {
1532
- rank: 33,
1533
- address: "0x73AfD2C81e49086E29b42BF7f82Ee53b68706Fc1",
1534
- symbol: "ACMx",
1535
- name: "Aecom xStock",
1536
- decimals: 18,
1537
- totalSupply: "1000000000000000000000000000",
1538
- explorer: {
1539
- priceUsd: 7435e-8,
1540
- volume24hUsd: 0,
1541
- marketCapUsd: 74350,
1542
- holders: 3
1543
- },
1544
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x73AfD2C81e49086E29b42BF7f82Ee53b68706Fc1"
1545
- },
1546
- {
1547
- rank: 34,
1548
- address: "0x919d89A68c60E8bba011943B560efCfB230BD5E7",
1549
- symbol: "ABBVx",
1550
- name: "AbbVie xStock",
1551
- decimals: 18,
1552
- totalSupply: "1000000000000000000000000000",
1553
- explorer: {
1554
- priceUsd: 7435e-8,
1555
- volume24hUsd: 0,
1556
- marketCapUsd: 74350,
1557
- holders: 3
1558
- },
1559
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x919d89A68c60E8bba011943B560efCfB230BD5E7"
1560
- },
1561
- {
1562
- rank: 35,
1563
- address: "0xC04A5582219C7d56Ebd3a681f6Fc9ccaf008e10F",
1564
- symbol: "ACNx",
1565
- name: "Accenture xStock",
1566
- decimals: 18,
1567
- totalSupply: "1000000000000000000000000000",
1568
- explorer: {
1569
- priceUsd: 7435e-8,
1570
- volume24hUsd: 0,
1571
- marketCapUsd: 74350,
1572
- holders: 3
1573
- },
1574
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0xC04A5582219C7d56Ebd3a681f6Fc9ccaf008e10F"
1575
- },
1576
- {
1577
- rank: 36,
1578
- address: "0xb6D76A2BD53852db914662279775e1C47D6FA0e9",
1579
- symbol: "AYIx",
1580
- name: "Acuity xStock",
1581
- decimals: 18,
1582
- totalSupply: "1000000000000000000000000000",
1583
- explorer: {
1584
- priceUsd: 7435e-8,
1585
- volume24hUsd: 0,
1586
- marketCapUsd: 74350,
1587
- holders: 3
1588
- },
1589
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0xb6D76A2BD53852db914662279775e1C47D6FA0e9"
1590
- },
1591
- {
1592
- rank: 37,
1593
- address: "0xbdE593b674cdC4C0843E1346E2702e7943a0dE52",
1594
- symbol: "ADSx",
1595
- name: "adidas xStock",
1596
- decimals: 18,
1597
- totalSupply: "1000000000000000000000000000",
1598
- explorer: {
1599
- priceUsd: 7435e-8,
1600
- volume24hUsd: 0,
1601
- marketCapUsd: 74350,
1602
- holders: 3
1603
- },
1604
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0xbdE593b674cdC4C0843E1346E2702e7943a0dE52"
1605
- },
1606
- {
1607
- rank: 38,
1608
- address: "0x983654892fF006A14987F6Bd1A5d5303D160Edde",
1609
- symbol: "ADBEx",
1610
- name: "Adobe xStock",
1611
- decimals: 18,
1612
- totalSupply: "1000000000000000000000000000",
1613
- explorer: {
1614
- priceUsd: 7435e-8,
1615
- volume24hUsd: 0,
1616
- marketCapUsd: 74350,
1617
- holders: 3
1618
- },
1619
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x983654892fF006A14987F6Bd1A5d5303D160Edde"
1620
- },
1621
- {
1622
- rank: 39,
1623
- address: "0xd7F9527890001E3c0BdCa6Cc1B84Fc3C21e97Ea3",
1624
- symbol: "ADTx",
1625
- name: "ADT xStock",
1626
- decimals: 18,
1627
- totalSupply: "1000000000000000000000000000",
1628
- explorer: {
1629
- priceUsd: 7435e-8,
1630
- volume24hUsd: 0,
1631
- marketCapUsd: 74350,
1632
- holders: 3
1633
- },
1634
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0xd7F9527890001E3c0BdCa6Cc1B84Fc3C21e97Ea3"
1635
- },
1636
- {
1637
- rank: 40,
1638
- address: "0xD5fcD31e2781C07c81f4675C94391958B2Da4197",
1639
- symbol: "AEHRx",
1640
- name: "Aehr Test Systems xStock",
1641
- decimals: 18,
1642
- totalSupply: "1000000000000000000000000000",
1643
- explorer: {
1644
- priceUsd: 7435e-8,
1645
- volume24hUsd: 0,
1646
- marketCapUsd: 74350,
1647
- holders: 3
1648
- },
1649
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0xD5fcD31e2781C07c81f4675C94391958B2Da4197"
1650
- },
1651
- {
1652
- rank: 41,
1653
- address: "0xC017d6AD451699A92bd7c37250c0e59a26e2EB06",
1654
- symbol: "AVAVx",
1655
- name: "AeroVironment xStock",
1656
- decimals: 18,
1657
- totalSupply: "1000000000000000000000000000",
1658
- explorer: {
1659
- priceUsd: 7435e-8,
1660
- volume24hUsd: 0,
1661
- marketCapUsd: 74350,
1662
- holders: 3
1663
- },
1664
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0xC017d6AD451699A92bd7c37250c0e59a26e2EB06"
1665
- },
1666
- {
1667
- rank: 42,
1668
- address: "0x83DBbcFf5f870b0DEBaa77a8C7Dd912DA9F44972",
1669
- symbol: "AMGx",
1670
- name: "Affiliated Managers xStock",
1671
- decimals: 18,
1672
- totalSupply: "1000000000000000000000000000",
1673
- explorer: {
1674
- priceUsd: 7435e-8,
1675
- volume24hUsd: 0,
1676
- marketCapUsd: 74350,
1677
- holders: 3
1678
- },
1679
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x83DBbcFf5f870b0DEBaa77a8C7Dd912DA9F44972"
1680
- },
1681
- {
1682
- rank: 43,
1683
- address: "0xafD1C1217a409F0A3b645f25Edbd342cA4aD1277",
1684
- symbol: "AFRMx",
1685
- name: "Affirm xStock",
1686
- decimals: 18,
1687
- totalSupply: "1000000000000000000000000000",
1688
- explorer: {
1689
- priceUsd: 7435e-8,
1690
- volume24hUsd: 0,
1691
- marketCapUsd: 74350,
1692
- holders: 3
1693
- },
1694
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0xafD1C1217a409F0A3b645f25Edbd342cA4aD1277"
1695
- },
1696
- {
1697
- rank: 44,
1698
- address: "0x605AcAF5ECf14a6595b1688e99E687241Ccc0999",
1699
- symbol: "GEGE",
1700
- name: "GEGE",
1701
- decimals: 18,
1702
- totalSupply: "1000000000000000000000000000",
1703
- explorer: {
1704
- priceUsd: 7425e-8,
1705
- volume24hUsd: 9e3,
1706
- marketCapUsd: 74251,
1707
- holders: 5
1708
- },
1709
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x605AcAF5ECf14a6595b1688e99E687241Ccc0999"
1710
- },
1711
- {
1712
- rank: 45,
1713
- address: "0xf77b097287eBC76fb73062B545874F5092420999",
1714
- symbol: "THR",
1715
- name: "The Rhosaks",
1716
- decimals: 18,
1717
- totalSupply: "1000000000000000000000000000",
1718
- explorer: {
1719
- priceUsd: 7194e-8,
1720
- volume24hUsd: 0,
1721
- marketCapUsd: 71944,
1722
- holders: 5
1723
- },
1724
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0xf77b097287eBC76fb73062B545874F5092420999"
1725
- },
1726
- {
1727
- rank: 46,
1728
- address: "0x141A784764d69EC29d9AFa8a75B6ffD92fBA8A9f",
1729
- symbol: "ANEMOS",
1730
- name: "ANEMOS Launchpad",
1731
- decimals: 18,
1732
- totalSupply: "1000000000000000000000000000",
1733
- explorer: {
1734
- priceUsd: 6819e-8,
1735
- volume24hUsd: 229,
1736
- marketCapUsd: 68186,
1737
- holders: 14
1738
- },
1739
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x141A784764d69EC29d9AFa8a75B6ffD92fBA8A9f"
1740
- },
1741
- {
1742
- rank: 47,
1743
- address: "0x321CCD0f1c8Bb743625dF4CdB0DDA60820079265",
1744
- symbol: "BLOB",
1745
- name: "The blob",
1746
- decimals: 18,
1747
- totalSupply: "1000000000000000000000000000",
1748
- explorer: {
1749
- priceUsd: 6463e-8,
1750
- volume24hUsd: 0,
1751
- marketCapUsd: 64634,
1752
- holders: 2616
1753
- },
1754
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x321CCD0f1c8Bb743625dF4CdB0DDA60820079265"
1755
- },
1756
- {
1757
- rank: 48,
1758
- address: "0xFB47b6968cA7786a8E5ec4E2E74611EfB0B9EB88",
1759
- symbol: "EHYPE",
1760
- name: "eHYPE",
1761
- decimals: 18,
1762
- totalSupply: "1000000000000000000000000000",
1763
- explorer: {
1764
- priceUsd: 6253e-8,
1765
- volume24hUsd: 0,
1766
- marketCapUsd: 62533,
1767
- holders: 1264
1768
- },
1769
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0xFB47b6968cA7786a8E5ec4E2E74611EfB0B9EB88"
1770
- },
1771
- {
1772
- rank: 49,
1773
- address: "0x37c9257A6BAa1779d56A7BEbD93FadD1a463893A",
1774
- symbol: "ABY",
1775
- name: "Astrorabby",
1776
- decimals: 18,
1777
- totalSupply: "1000000000000000000000000000",
1778
- explorer: {
1779
- priceUsd: 617e-7,
1780
- volume24hUsd: 0,
1781
- marketCapUsd: 61692,
1782
- holders: 2539
1783
- },
1784
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x37c9257A6BAa1779d56A7BEbD93FadD1a463893A"
1785
- },
1786
- {
1787
- rank: 50,
1788
- address: "0x58A6b2459a4A820AC316880938e15379Ba2f8d9e",
1789
- symbol: "SIERRA",
1790
- name: "JAVIER",
1791
- decimals: 18,
1792
- totalSupply: "1000000000000000000000000000",
1793
- explorer: {
1794
- priceUsd: 603e-7,
1795
- volume24hUsd: 479,
1796
- marketCapUsd: 59685,
1797
- holders: 6288
1798
- },
1799
- explorerUrl: "https://testnet.elysium.kinetiq.xyz/token/0x58A6b2459a4A820AC316880938e15379Ba2f8d9e"
1800
- }
1801
- ]
1802
- };
1803
-
1804
- // src/market/market.ts
1805
- import { erc20Abi as erc20Abi5, getAddress as getAddress4 } from "viem";
1806
-
1807
- // src/core/cache.ts
1808
- function swr(ttlMs, staleMs, load) {
1809
- let cur = null;
1810
- let inflight = null;
1811
- const refresh = () => {
1812
- inflight ??= (async () => {
1813
- try {
1814
- const value = await load();
1815
- cur = { value, at: Date.now() };
1816
- return { value, at: cur.at, stale: false };
1817
- } catch (e2) {
1818
- if (cur && Date.now() - cur.at <= staleMs) return { value: cur.value, at: cur.at, stale: true };
1819
- throw e2;
1820
- } finally {
1821
- inflight = null;
1822
- }
1823
- })();
1824
- return inflight;
1825
- };
1826
- return {
1827
- get() {
1828
- if (cur && Date.now() - cur.at < ttlMs) return Promise.resolve({ value: cur.value, at: cur.at, stale: false });
1829
- return refresh();
1830
- },
1831
- /** Drop the cached value (tests). */
1832
- clear() {
1833
- cur = null;
1834
- }
1835
- };
1836
- }
1837
-
1838
- // src/market/v3pools.ts
1839
- import { erc20Abi as erc20Abi4, getAddress as getAddress3, parseAbi as parseAbi3, parseAbiItem } from "viem";
1840
- var poolAbi = parseAbi3([
1841
- "function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16 a, uint16 b, uint16 c, uint8 feeProtocol, bool unlocked)",
1842
- "function liquidity() view returns (uint128)"
1843
- ]);
1844
- var poolCreated = parseAbiItem("event PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool)");
1845
- var INDEX_MIN_HYPE = { kalos: 0, perpme: 1 };
1846
- var known = /* @__PURE__ */ new Map();
1847
- var scannedTo = /* @__PURE__ */ new Map();
1848
- async function discoverPools(c, venue) {
1849
- const list = known.get(venue.id) ?? known.set(venue.id, []).get(venue.id);
1850
- const seen = scannedTo.get(venue.id) ?? -1n;
1851
- const head = await c.getBlockNumber();
1852
- if (head > seen) {
1853
- const logs = await c.getLogs({ address: venue.factory, event: poolCreated, fromBlock: seen < 0n ? 0n : seen + 1n, toBlock: head });
1854
- const have = new Set(list.map((p) => p.pool));
1855
- for (const l of logs) {
1856
- const a = l.args;
1857
- if (!a.pool || !a.token0 || !a.token1 || a.fee == null) continue;
1858
- const pool = getAddress3(a.pool);
1859
- if (have.has(pool)) continue;
1860
- have.add(pool);
1861
- list.push({ venue: venue.id, pool, token0: getAddress3(a.token0), token1: getAddress3(a.token1), fee: Number(a.fee), createdBlock: l.blockNumber ?? 0n });
1862
- }
1863
- scannedTo.set(venue.id, head);
1864
- }
1865
- return list;
1866
- }
1867
- var holdCache = /* @__PURE__ */ new Map();
1868
- async function poolsHoldingHype(c, venue, refs, minHype) {
1869
- const hit = holdCache.get(venue.id);
1870
- if (hit && Date.now() - hit.at < 6e4) return hit.refs;
1871
- const w = venue.wrappedNative.toLowerCase();
1872
- const quoted = refs.filter((p) => p.token0.toLowerCase() === w || p.token1.toLowerCase() === w);
1873
- const res = await c.multicall({ contracts: quoted.map((p) => ({ address: venue.wrappedNative, abi: erc20Abi4, functionName: "balanceOf", args: [p.pool] })), allowFailure: true });
1874
- const min = BigInt(Math.round(minHype * 1e6)) * 10n ** 12n;
1875
- if (res.some((r) => r.status !== "success")) {
1876
- if (hit) return hit.refs;
1877
- throw new Error(`${venue.name} pool balances could not be read (RPC error)`);
1878
- }
1879
- const out = quoted.filter((_, i) => res[i].result >= min);
1880
- holdCache.set(venue.id, { at: Date.now(), refs: out });
1881
- return out;
1882
- }
1883
- async function listedPools(c, venue) {
1884
- const refs = await discoverPools(c, venue);
1885
- const min = INDEX_MIN_HYPE[venue.id] ?? 0;
1886
- return min > 0 ? poolsHoldingHype(c, venue, refs, min) : refs;
1887
- }
1888
- async function readPoolStates(c, pools) {
1889
- if (!pools.length) return [];
1890
- const res = await c.multicall({
1891
- contracts: pools.flatMap((p) => [
1892
- { address: p.pool, abi: poolAbi, functionName: "slot0" },
1893
- { address: p.pool, abi: poolAbi, functionName: "liquidity" },
1894
- { address: p.token0, abi: erc20Abi4, functionName: "balanceOf", args: [p.pool] },
1895
- { address: p.token1, abi: erc20Abi4, functionName: "balanceOf", args: [p.pool] }
1896
- ]),
1897
- allowFailure: true
1898
- });
1899
- return pools.map((p, i) => {
1900
- const [s, l, b0, b1] = res.slice(i * 4, i * 4 + 4);
1901
- if (s.status !== "success" || l.status !== "success") throw new Error("V3 pool read failed (RPC error)");
1902
- const sqrt = s.result[0];
1903
- return {
1904
- ...p,
1905
- sqrtPriceX96: sqrt && sqrt > 0n ? sqrt : null,
1906
- liquidity: l.result,
1907
- balance0: b0.status === "success" ? b0.result : 0n,
1908
- balance1: b1.status === "success" ? b1.result : 0n
1909
- };
1910
- });
1911
- }
1912
- function priceFromSqrt(sqrtPriceX96, decimals0, decimals1) {
1913
- const r = Number(sqrtPriceX96) / 2 ** 96;
1914
- return r * r * 10 ** (decimals0 - decimals1);
1915
- }
1916
- var feePct = (fee) => fee / 1e4;
1917
-
1918
- // src/market/history.ts
1919
- var recordSamples = (_) => {
1920
- };
1921
- var tokenSummary = (_pool, _baseIsToken0) => null;
1922
-
1923
- // src/core/concurrency.ts
1924
- var BusyError = class extends Error {
1925
- constructor(what, retryAfter = 5) {
1926
- super(`The server is busy (${what}). Try again in a few seconds.`);
1927
- this.retryAfter = retryAfter;
1928
- }
1929
- retryAfter;
1930
- status = 429;
1931
- };
1932
- function limiter(name, max, maxQueue, waitMs = 1e4) {
1933
- let active = 0, rejected = 0;
1934
- const queue = [];
1935
- const release = () => {
1936
- active--;
1937
- const next = queue.shift();
1938
- if (next) {
1939
- clearTimeout(next.timer);
1940
- active++;
1941
- next.go();
1942
- }
1943
- };
1944
- return {
1945
- async run(fn) {
1946
- if (active >= max) {
1947
- if (queue.length >= maxQueue) {
1948
- rejected++;
1949
- throw new BusyError(name);
1950
- }
1951
- await new Promise((resolve, reject) => {
1952
- const entry = { go: resolve, timer: setTimeout(() => {
1953
- const i = queue.indexOf(entry);
1954
- if (i >= 0) queue.splice(i, 1);
1955
- rejected++;
1956
- reject(new BusyError(name));
1957
- }, waitMs) };
1958
- queue.push(entry);
1959
- });
1960
- } else active++;
1961
- try {
1962
- return await fn();
1963
- } finally {
1964
- release();
1965
- }
1966
- },
1967
- stats: () => ({ active, queued: queue.length, rejected })
1968
- };
1969
- }
1970
-
1971
- // src/market/market.ts
1972
- var histLimit = limiter("price history", 3, 30);
1973
- var marketClient = getBatchClient();
1974
- var rnd = (x, sig = 8) => x == null || !Number.isFinite(x) ? null : Number(x.toPrecision(sig));
1975
- var human = (raw, dec2) => Number(raw) / 10 ** dec2;
1976
- var snapshot = explorer_snapshot_default;
1977
- var hypeRef = swr(3e4, 10 * 6e4, async () => {
1978
- const m = (await getPerpMarkets("testnet")).find((x) => x.name === "HYPE");
1979
- return m && Number.isFinite(m.markPx) && m.markPx > 0 ? m.markPx : null;
1980
- });
1981
- var metaCache = /* @__PURE__ */ new Map();
1982
- var META_TTL = 10 * 6e4;
1983
- async function tokenMeta(addrs) {
1984
- const now = Date.now();
1985
- const need = [...new Set(addrs.map((a) => getAddress4(a)))].filter((a) => {
1986
- const m = metaCache.get(a.toLowerCase());
1987
- return !m || now - m.at > META_TTL;
1988
- });
1989
- for (let i = 0; i < need.length; i += 100) {
1990
- const chunk = need.slice(i, i + 100);
1991
- const res = await marketClient.multicall({
1992
- contracts: chunk.flatMap((address) => [
1993
- { address, abi: erc20Abi5, functionName: "symbol" },
1994
- { address, abi: erc20Abi5, functionName: "name" },
1995
- { address, abi: erc20Abi5, functionName: "decimals" },
1996
- { address, abi: erc20Abi5, functionName: "totalSupply" }
1997
- ]),
1998
- allowFailure: true
1999
- });
2000
- chunk.forEach((address, k) => {
2001
- const [s, n3, d, t] = res.slice(k * 4, k * 4 + 4);
2002
- const info2 = {
2003
- address,
2004
- symbol: s.status === "success" ? String(s.result).slice(0, 24) : "???",
2005
- name: n3.status === "success" ? String(n3.result).slice(0, 48) : "Unknown token",
2006
- decimals: d.status === "success" ? Number(d.result) : 18,
2007
- totalSupply: t.status === "success" ? String(t.result) : null
2008
- };
2009
- metaCache.set(address.toLowerCase(), { info: info2, at: d.status === "success" ? now : now - META_TTL + 6e4 });
2010
- });
2011
- }
2012
- const out = /* @__PURE__ */ new Map();
2013
- for (const a of addrs) {
2014
- const m = metaCache.get(a.toLowerCase());
2015
- if (m) out.set(a.toLowerCase(), m.info);
2016
- }
2017
- return out;
2018
- }
2019
- var safeText = (s, max) => cleanLabel(s, max);
2020
- var DUST_HYPE = 1e-4;
2021
- var lastV3 = /* @__PURE__ */ new Map();
2022
- async function readV3Venue(c, v) {
2023
- try {
2024
- const s = await readPoolStates(c, await listedPools(c, v));
2025
- lastV3.set(v.id, s);
2026
- return s;
2027
- } catch (e2) {
2028
- console.error(`${v.id} pool read failed:`, String(e2.message).slice(0, 120));
2029
- return lastV3.get(v.id) ?? null;
2030
- }
2031
- }
2032
- async function buildMarket() {
2033
- const c = marketClient;
2034
- const [pairs, v3Reads, hype2] = await Promise.all([
2035
- listAllVenuePairs(c),
2036
- Promise.all(V3_VENUES.map(async (v) => [v, await readV3Venue(c, v)])),
2037
- hypeRef.get().then((r) => r.value).catch(() => null)
2038
- ]);
2039
- const v3 = v3Reads.flatMap(([, s]) => s ?? []);
2040
- const unreadV3 = v3Reads.filter(([, s]) => s === null).map(([v]) => v);
2041
- const addrSet = /* @__PURE__ */ new Set();
2042
- const addrs = [];
2043
- const add = (a) => {
2044
- const k = a.toLowerCase();
2045
- if (!addrSet.has(k)) {
2046
- addrSet.add(k);
2047
- addrs.push(getAddress4(a));
2048
- }
2049
- };
2050
- for (const p of pairs) {
2051
- add(p.token0);
2052
- add(p.token1);
2053
- }
2054
- for (const p of v3) {
2055
- add(p.token0);
2056
- add(p.token1);
2057
- }
2058
- const meta = await tokenMeta(addrs);
2059
- const dec2 = (a) => meta.get(a.toLowerCase())?.decimals ?? 18;
2060
- const sym = (a) => safeText(meta.get(a.toLowerCase())?.symbol ?? "???", 24) || "???";
2061
- const pools = [];
2062
- for (const p of pairs) {
2063
- const d0 = dec2(p.token0), d1 = dec2(p.token1), r0 = human(p.reserve0, d0), r1 = human(p.reserve1, d1);
2064
- pools.push({ address: p.pair, venue: p.venue, kind: "v2", token0: p.token0, token1: p.token1, d0, d1, r0, r1, price01: r0 > 0 && r1 > 0 ? r1 / r0 : null, fee: null, active: p.reserve0 > 0n && p.reserve1 > 0n, tvlHype: null });
2065
- }
2066
- for (const p of v3) {
2067
- const d0 = dec2(p.token0), d1 = dec2(p.token1);
2068
- const price01 = p.sqrtPriceX96 ? priceFromSqrt(p.sqrtPriceX96, d0, d1) : null;
2069
- pools.push({
2070
- address: p.pool,
2071
- venue: p.venue,
2072
- kind: "v3",
2073
- token0: p.token0,
2074
- token1: p.token1,
2075
- d0,
2076
- d1,
2077
- r0: human(p.balance0, d0),
2078
- r1: human(p.balance1, d1),
2079
- price01: price01 && Number.isFinite(price01) && price01 > 0 ? price01 : null,
2080
- fee: feePct(p.fee),
2081
- active: p.liquidity > 0n && price01 !== null && price01 > 0,
2082
- tvlHype: null
2083
- });
2084
- }
2085
- const U = CANONICAL.USDC;
2086
- for (const p of pools) {
2087
- if (!p.active) continue;
2088
- const h0 = isHypeLike(p.token0), h1 = isHypeLike(p.token1);
2089
- if (h0 || h1) p.tvlHype = 2 * (h0 ? p.r0 : p.r1);
2090
- }
2091
- const bestHype = /* @__PURE__ */ new Map(), bestUsdc = /* @__PURE__ */ new Map();
2092
- const consider = (map, token, price, tvl, pool) => {
2093
- if (!(price > 0) || !Number.isFinite(price) || !(tvl >= DUST_HYPE)) return;
2094
- const cur = map.get(token.toLowerCase());
2095
- if (!cur || tvl > cur.tvl) map.set(token.toLowerCase(), { price, tvl, pool });
2096
- };
2097
- const sideOf = (p, token) => same(p.token0, token) ? { is0: true, other: p.token1 } : { is0: false, other: p.token0 };
2098
- const pricePass = () => {
2099
- for (const p of pools) {
2100
- if (!p.active || p.price01 === null || p.tvlHype === null) continue;
2101
- for (const token of [p.token0, p.token1]) {
2102
- if (isHypeLike(token)) continue;
2103
- const { is0, other } = sideOf(p, token);
2104
- const price = is0 ? p.price01 : 1 / p.price01;
2105
- if (isHypeLike(other)) consider(bestHype, token, price, p.tvlHype, p);
2106
- else if (same(other, U)) consider(bestUsdc, token, price, p.tvlHype, p);
2107
- }
2108
- }
2109
- };
2110
- pricePass();
2111
- for (const p of pools) {
2112
- if (!p.active || p.tvlHype !== null || p.price01 === null) continue;
2113
- const a = bestHype.get(p.token1.toLowerCase()), b = bestHype.get(p.token0.toLowerCase());
2114
- if (a) p.tvlHype = 2 * p.r1 * a.price;
2115
- else if (b) p.tvlHype = 2 * p.r0 * b.price;
2116
- }
2117
- pricePass();
2118
- const byToken = /* @__PURE__ */ new Map();
2119
- for (const p of pools) for (const t of [p.token0, p.token1]) {
2120
- if (isHypeLike(t)) continue;
2121
- (byToken.get(t.toLowerCase()) ?? byToken.set(t.toLowerCase(), []).get(t.toLowerCase())).push(p);
2122
- }
2123
- const snapBy = new Map(snapshot.tokens.map((s) => [s.address.toLowerCase(), s]));
2124
- const snapOut = (s) => s ? { rank: s.rank, priceUsd: s.explorer.priceUsd, volume24hUsd: s.explorer.volume24hUsd, marketCapUsd: s.explorer.marketCapUsd, holders: s.explorer.holders, url: s.explorerUrl } : null;
2125
- const tokens = [];
2126
- for (const [key, mine] of byToken) {
2127
- const info2 = meta.get(key);
2128
- const address = info2.address;
2129
- const px = bestHype.get(key), pu = bestUsdc.get(key);
2130
- const live = mine.filter((p) => p.active);
2131
- const tvl = live.reduce((s, p) => s + (p.tvlHype ?? 0), 0);
2132
- const swappableIds = new Set(SWAP_VENUES.map((v) => v.id));
2133
- const swapPools = live.filter((p) => swappableIds.has(p.venue));
2134
- const anySwappableVenue = mine.some((p) => swappableIds.has(p.venue));
2135
- const tradable = swapPools.length > 0;
2136
- const venueNames = (ids) => ids.map((i) => VENUES.find((v) => v.id === i)?.name ?? i).join(", ");
2137
- const swapVenues = [...new Set(swapPools.map((p) => p.venue))];
2138
- const swapReason = tradable ? null : !mine.length ? "No pool on a venue the app can swap through" : !anySwappableVenue ? `Only has pools on ${venueNames([...new Set(mine.map((p) => p.venue))])}, which the app cannot swap through` : `Pools on ${venueNames([...new Set(mine.map((p) => p.venue))])} exist but hold no liquidity`;
2139
- const isUsdc = same(address, U);
2140
- const outPools = mine.map((p) => {
2141
- const { is0, other } = sideOf(p, address);
2142
- const priceOther = p.price01 === null ? null : is0 ? p.price01 : 1 / p.price01;
2143
- const od = is0 ? p.d1 : p.d0;
2144
- return {
2145
- pool: p.address,
2146
- venue: p.venue,
2147
- kind: p.kind,
2148
- fee: p.fee,
2149
- other: { address: getAddress4(other), symbol: sym(other), decimals: od, hypeLike: isHypeLike(other) },
2150
- reserveToken: rnd(is0 ? p.r0 : p.r1, 8),
2151
- reserveOther: rnd(is0 ? p.r1 : p.r0, 8),
2152
- priceHype: p.active && isHypeLike(other) ? rnd(priceOther, 8) : null,
2153
- priceUsdc: p.active && same(other, U) ? rnd(priceOther, 8) : null,
2154
- priceOther: p.active ? rnd(priceOther, 8) : null,
2155
- tvlHype: rnd(p.tvlHype, 6),
2156
- share: p.active && p.tvlHype !== null && tvl > 0 ? rnd(p.tvlHype / tvl, 4) : null,
2157
- active: p.active,
2158
- dust: p.active && (p.tvlHype ?? 0) < DUST_HYPE
2159
- };
2160
- }).sort((a, b) => (b.tvlHype ?? -1) - (a.tvlHype ?? -1));
2161
- const pricePoolRec = px?.pool ?? pu?.pool ?? null;
2162
- tokens.push({
2163
- address,
2164
- symbol: safeText(info2.symbol, 24) || "???",
2165
- name: safeText(info2.name, 48) || "Unknown token",
2166
- decimals: info2.decimals,
2167
- totalSupply: info2.totalSupply,
2168
- view: anySwappableVenue ? "app" : "kalos",
2169
- tradable,
2170
- swapVenues,
2171
- swapReason,
2172
- wrapped: false,
2173
- priceHype: rnd(px?.price ?? null),
2174
- priceUsdc: isUsdc ? 1 : rnd(pu?.price ?? null),
2175
- priceVenue: pricePoolRec ? pricePoolRec.venue : null,
2176
- tvlHype: tvl > 0 ? rnd(tvl, 6) : null,
2177
- liquidityHype: tvl > 0 ? rnd(tvl, 6) : null,
2178
- pairs: mine.length,
2179
- pairAddress: px ? px.pool.address : null,
2180
- venues: [...new Set(mine.map((p) => p.venue))],
2181
- pools: outPools,
2182
- priceBasis: px ? "HYPE" : pu ? "USDC" : null,
2183
- pricePool: pricePoolRec ? pricePoolRec.address : null,
2184
- change24hPct: null,
2185
- high24h: null,
2186
- low24h: null,
2187
- spark: null,
2188
- snapshot: snapOut(snapBy.get(key))
2189
- });
2190
- }
2191
- for (const s of snapshot.tokens) {
2192
- const k = s.address.toLowerCase();
2193
- if (byToken.has(k)) continue;
2194
- const wrapped = isHypeLike(s.address);
2195
- tokens.push({
2196
- address: getAddress4(s.address),
2197
- symbol: safeText(s.symbol, 24) || "???",
2198
- name: safeText(s.name, 48) || "Unknown token",
2199
- decimals: s.decimals,
2200
- totalSupply: s.totalSupply,
2201
- view: "snapshot",
2202
- tradable: false,
2203
- swapVenues: [],
2204
- swapReason: `Listed on the Elysium explorer but no pool on the venues the app swaps through (${SWAP_VENUES.map((v) => v.name).join(", ")})`,
2205
- wrapped,
2206
- priceHype: wrapped ? 1 : null,
2207
- priceUsdc: null,
2208
- priceVenue: null,
2209
- tvlHype: null,
2210
- liquidityHype: null,
2211
- pairs: 0,
2212
- pairAddress: null,
2213
- venues: [],
2214
- pools: [],
2215
- priceBasis: wrapped ? "HYPE" : null,
2216
- pricePool: null,
2217
- change24hPct: null,
2218
- high24h: null,
2219
- low24h: null,
2220
- spark: null,
2221
- snapshot: snapOut(s)
2222
- });
2223
- }
2224
- const poolMetas = pools.filter((p) => p.active).sort((a, b) => (b.tvlHype ?? 0) - (a.tvlHype ?? 0)).map((p) => ({ pool: p.address, kind: p.kind, venue: p.venue, token0: p.token0, token1: p.token1, d0: p.d0, d1: p.d1 }));
2225
- recordSamples(pools.filter((p) => p.active && p.price01 !== null).map((p) => ({ pool: p.address, price: p.price01, r0: p.r0, r1: p.r1 })));
2226
- for (const t of tokens) {
2227
- if (!t.pricePool) continue;
2228
- const rec = pools.find((p) => p.address === t.pricePool);
2229
- const s = tokenSummary(t.pricePool, same(rec.token0, t.address));
2230
- if (s) {
2231
- t.change24hPct = rnd(s.change24hPct, 4);
2232
- t.high24h = rnd(s.high24h);
2233
- t.low24h = rnd(s.low24h);
2234
- t.spark = s.spark.map((x) => rnd(x, 6));
2235
- }
2236
- }
2237
- tokens.sort((a, b) => (b.tvlHype ?? -1) - (a.tvlHype ?? -1) || (a.snapshot?.rank ?? 1e9) - (b.snapshot?.rank ?? 1e9) || a.symbol.localeCompare(b.symbol));
2238
- const venueInfo = (v) => {
2239
- const mine = pools.filter((p) => p.venue === v.id);
2240
- return { id: v.id, name: v.name, kind: v.kind, note: v.note, url: v.url, swappable: v.swappable, router: v.router, factory: v.factory, pools: mine.length, activePools: mine.filter((p) => p.active).length, tokens: tokens.filter((t) => t.venues.includes(v.id)).length };
2241
- };
2242
- const venues = VENUES.map(venueInfo);
2243
- const payload = {
2244
- hypeUsd: hype2 ? { price: hype2, source: "Hyperliquid testnet HYPE mark (reference only)" } : null,
2245
- venues,
2246
- externalVenues: EXTERNAL_VENUES,
2247
- coverage: {
2248
- tokens: tokens.length,
2249
- tradable: tokens.filter((t) => t.tradable).length,
2250
- kalosViewOnly: tokens.filter((t) => t.view === "kalos").length,
2251
- snapshotOnly: tokens.filter((t) => t.view === "snapshot").length,
2252
- pools: pools.length,
2253
- snapshotAt: snapshot.snapshotAt,
2254
- snapshotCount: snapshot.tokens.length,
2255
- snapshotSource: snapshot.source,
2256
- snapshotNote: snapshot.note
2257
- },
2258
- warnings: unreadV3.map((v) => `${v.name} pools could not be read right now (RPC error). ${v.name} tokens may be missing until the next refresh.`),
2259
- tokens
2260
- };
2261
- return { payload, pairs, pools: poolMetas };
2262
- }
2263
- var marketCache = swr(15e3, 12e4, buildMarket);
2264
-
2265
- // src/shared/launch/bunnypad.ts
2266
- import { decodeEventLog, encodeFunctionData, erc20Abi as erc20Abi6, getAddress as getAddress5, parseAbi as parseAbi4, zeroAddress } from "viem";
2267
-
2268
- // src/shared/launch/types.ts
2269
- var LaunchSimError = class extends Error {
2270
- kind;
2271
- constructor(kind, message) {
2272
- super(message);
2273
- this.kind = kind;
2274
- this.name = "LaunchSimError";
2275
- }
2276
- };
2277
-
2278
- // src/shared/launch/helpers.ts
2279
- import { decodeErrorResult, formatEther, parseEther } from "viem";
2280
- var group = (n3) => {
2281
- try {
2282
- return BigInt(n3).toLocaleString("en-US");
2283
- } catch {
2284
- return String(n3);
2285
- }
2286
- };
2287
- var hype = (wei, maxFrac = 6) => {
2288
- const s = formatEther(wei);
2289
- const [i, f = ""] = s.split(".");
2290
- const frac = f.slice(0, maxFrac).replace(/0+$/, "");
2291
- return frac ? `${i}.${frac}` : i;
2292
- };
2293
- var short = (a) => `${a.slice(0, 6)}\u2026${a.slice(-4)}`;
2294
- var utf8Len = (s) => new TextEncoder().encode(s).length;
2295
- var PRINTABLE = /^[^\u0000-\u001f\u007f]+$/;
2296
- var ADDR = /^0x[0-9a-fA-F]{40}$/;
2297
- var parseHypeAmount = (s) => {
2298
- const t = s.trim().replace(/[,_\s]/g, "");
2299
- if (t === "") return 0n;
2300
- if (!/^(\d{1,12}(\.\d{0,18})?|\.\d{1,18})$/.test(t)) return null;
2301
- try {
2302
- return parseEther(t.startsWith(".") ? `0${t}` : t);
2303
- } catch {
2304
- return null;
2305
- }
2306
- };
2307
- async function retry(fn) {
2308
- try {
2309
- return await fn();
2310
- } catch (e2) {
2311
- if (!/rate|429|-32017|too many|timeout|fetch failed|HTTP request failed/i.test(String(e2?.message ?? e2))) throw e2;
2312
- await sleep(700);
2313
- return fn();
2314
- }
2315
- }
2316
- function revertData(e2) {
2317
- const seen = /* @__PURE__ */ new Set();
2318
- const isRevertHex = (v) => typeof v === "string" && /^0x[0-9a-fA-F]{8,}$/.test(v);
2319
- const dig = (x, depth = 0) => {
2320
- if (!x || typeof x !== "object" || seen.has(x) || depth > 8) return null;
2321
- seen.add(x);
2322
- for (const k of ["data", "raw"]) {
2323
- const v = k in x ? x[k] : void 0;
2324
- if (isRevertHex(v)) return v;
2325
- const r = dig(v, depth + 1);
2326
- if (r) return r;
2327
- }
2328
- return "cause" in x ? dig(x.cause, depth + 1) : null;
2329
- };
2330
- return dig(e2);
2331
- }
2332
- var rawMessage = (e2) => String(e2?.shortMessage ?? e2?.message ?? e2).split("\n")[0];
2333
- function explainRevert(e2, abi, friendly, fallback = "the contract rejected the call") {
2334
- const data = revertData(e2);
2335
- if (data) {
2336
- try {
2337
- const d = decodeErrorResult({ abi, data });
2338
- const msg = friendly[d.errorName];
2339
- if (msg) return new LaunchSimError("revert", msg);
2340
- const a = d.args && d.args.length ? ` (${d.args.map(String).join(", ")})` : "";
2341
- return new LaunchSimError("revert", `the contract reverted with ${d.errorName}${a}`);
2342
- } catch {
2343
- }
2344
- }
2345
- const m = rawMessage(e2);
2346
- const r = m.match(/reverted with the following reason:\s*(.+)/i) ?? m.match(/execution reverted:\s*(.+)/i);
2347
- if (r) return new LaunchSimError("revert", r[1].trim());
2348
- if (/execution reverted|reverted/i.test(m)) return new LaunchSimError("revert", fallback);
2349
- if (/exceeds the balance|insufficient funds|gas required exceeds allowance/i.test(m)) return new LaunchSimError("funds", "the wallet does not hold enough HYPE for the value plus gas");
2350
- return new LaunchSimError("network", `the Elysium RPC could not be checked (${m})`);
2351
- }
2352
- async function multicallRows(client3, contracts) {
2353
- for (let attempt = 0; ; attempt++) {
2354
- const rows = await client3.multicall({ allowFailure: true, contracts });
2355
- if (rows.length === 0 || rows.some((r) => r.status === "success")) return rows;
2356
- if (attempt >= 1) throw new Error("every read in the batch failed (the RPC is probably rate limiting)");
2357
- await sleep(900);
2358
- }
2359
- }
2360
- async function gasPriceOf(client3) {
2361
- try {
2362
- return await retry(() => client3.getGasPrice());
2363
- } catch {
2364
- return null;
2365
- }
2366
- }
2367
- async function simulateTx(client3, tx, from, abi, friendly, readToken, extraOverride = []) {
2368
- const [balance, gasPrice] = await Promise.all([retry(() => client3.getBalance({ address: from })).catch(() => null), gasPriceOf(client3)]);
2369
- const buffer = parseEther("0.02");
2370
- const short_ = balance != null && balance < tx.value + buffer;
2371
- const own = short_ ? [{ address: from, balance: tx.value + parseEther("10") }] : [];
2372
- const stateOverride = own.length || extraOverride.length ? [...own, ...extraOverride] : void 0;
2373
- let token = null;
2374
- try {
2375
- const r = await retry(() => client3.call({ account: from, to: tx.to, data: tx.data, value: tx.value, stateOverride }));
2376
- token = r.data ? readToken(r.data) : null;
2377
- } catch (e2) {
2378
- const err = explainRevert(e2, abi, friendly);
2379
- if (err.kind === "funds" && short_) throw new LaunchSimError("network", "this RPC does not accept balance overrides, so the launch cannot be simulated while the wallet is short of HYPE");
2380
- throw err;
2381
- }
2382
- let gas = null;
2383
- try {
2384
- gas = await retry(() => client3.estimateGas({ account: from, to: tx.to, data: tx.data, value: tx.value, stateOverride }));
2385
- } catch {
2386
- }
2387
- const feeWei = gas != null && gasPrice != null ? gas * gasPrice : null;
2388
- return { token, gas, gasPrice, valueWei: tx.value, feeWei, balanceWei: balance, overridden: short_ };
2389
- }
2390
-
2391
- // src/shared/launch/bunnypad.ts
2392
- var BUNNYPAD = {
2393
- launchpad: "0x3c4F47959101557Eed9Bc4ae8D8D986aE8d3E5b1",
2394
- tokenFactory: "0x022534c476c6989Df3cDDd7c05b8Ebf9A17C4757",
2395
- feeSplitter: "0x56c6b96F80140FE6B4d043b32103Fa7bd93F624F",
2396
- site: "https://www.bunnypad.fun",
2397
- docs: "https://www.bunnypad.fun/docs",
2398
- ecosystem: "https://testnet.elysium.kinetiq.xyz/ecosystem/bunnypad"
2399
- };
2400
- var bunnyTokenUrl = (token) => `${BUNNYPAD.site}/token/${token}`;
2401
- var BUNNY_ERRORS = [
2402
- "error BadValue()",
2403
- "error HolderModeOn()",
2404
- "error InsufficientLiquidity()",
2405
- "error InvalidParams()",
2406
- "error NotCreator()",
2407
- "error NotFeeRecipient()",
2408
- "error NothingToClaim()",
2409
- "error PairNotAllowed()",
2410
- "error Paused()",
2411
- "error ReentrancyGuardReentrantCall()",
2412
- "error Slippage()",
2413
- "error TaxTooHigh()",
2414
- "error UnknownToken()",
2415
- "error UnsupportedToken()",
2416
- "error MaxWalletDuringLaunch()"
2417
- // BunnyToken: anti-snipe wallet cap, raised by the launchpad while it pays out the first buy
2418
- ];
2419
- var BUNNY_ABI = parseAbi4([
2420
- "function createToken((string name, string symbol, string metadata, address quote, address feeRecipient, bool holderMode, uint16 buyTaxBps, uint16 sellTaxBps, uint256 firstBuy, uint256 minTokensOut) p) payable returns (address token)",
2421
- "function launchFee() view returns (uint256)",
2422
- "function paused() view returns (bool)",
2423
- "function pairs(address quote) view returns (bool enabled, uint128 openingMarketCap)",
2424
- "function antiSnipeSeconds() view returns (uint256)",
2425
- "function maxWalletBps() view returns (uint256)",
2426
- "function MAX_TAX_BPS() view returns (uint16)",
2427
- "function BASE_FEE_BPS() view returns (uint16)",
2428
- "function MAX_METADATA_BYTES() view returns (uint256)",
2429
- "function TOTAL_SUPPLY() view returns (uint256)",
2430
- "function treasury() view returns (address)",
2431
- "function tokenFactory() view returns (address)",
2432
- "function tokensCount() view returns (uint256)",
2433
- "function tokensCreatedBy(address creator) view returns (address[])",
2434
- "event TokenLaunched(address indexed token, address indexed creator, address indexed quote, string name, string symbol, address feeRecipient, bool holderMode, uint16 buyTaxBps, uint16 sellTaxBps, uint256 virtualQuote, uint256 antiSnipeUntil)",
2435
- "event FirstBuy(address indexed token, address indexed creator, uint256 quoteIn, uint256 tokensOut)",
2436
- ...BUNNY_ERRORS
2437
- ]);
2438
- var BUNNY_FACTORY_ABI = parseAbi4(["function launchpad() view returns (address)"]);
2439
- var BPS = 10000n;
2440
- var CREATOR_SHARE_BPS = 7000n;
2441
- var isBunny = (d) => "openingMarketCap" in d;
2442
- function bunnyFirstBuyOut(s, buyTaxBps, quoteIn) {
2443
- const fee = quoteIn * (s.baseFeeBps + BigInt(buyTaxBps)) / BPS;
2444
- const net = quoteIn - fee;
2445
- return { fee, net, tokensOut: s.totalSupply * net / (s.openingMarketCap + net) };
2446
- }
2447
- function bunnyMaxFirstBuy(s, buyTaxBps) {
2448
- const cap = s.totalSupply * s.maxWalletBps / BPS;
2449
- const netMax = cap * s.openingMarketCap / (s.totalSupply - cap);
2450
- const feeBps = s.baseFeeBps + BigInt(buyTaxBps);
2451
- let q = netMax * BPS / (BPS - feeBps);
2452
- for (let i = 0; i < 8 && bunnyFirstBuyOut(s, buyTaxBps, q).tokensOut > cap; i++) q -= 1n;
2453
- return q;
2454
- }
2455
- var BUNNY_FRIENDLY = {
2456
- Paused: "BunnyPad has paused new launches",
2457
- PairNotAllowed: "BunnyPad has disabled HYPE as a pair asset",
2458
- BadValue: "the amount sent does not equal the launch fee plus the first buy (the launch fee may have changed: refresh the page)",
2459
- TaxTooHigh: "the trading fee is above BunnyPad's maximum",
2460
- InvalidParams: "BunnyPad rejected the name, symbol or metadata (length limits)",
2461
- Slippage: "the first buy would return fewer tokens than the minimum (the opening price changed: refresh the page)",
2462
- MaxWalletDuringLaunch: "the first buy is larger than the anti-snipe cap (a single wallet may only hold a small share of the supply in the first minutes): lower the first buy",
2463
- InsufficientLiquidity: "the pool has no liquidity for this first buy"
2464
- };
2465
- var FEE_OPTIONS = ["1", "1.5", "2", "2.5", "3", "3.5", "4"];
2466
- var taxFromPct = (p) => {
2467
- const n3 = Number(p);
2468
- if (!FEE_OPTIONS.includes(p) || !Number.isFinite(n3)) return null;
2469
- return Math.round(n3 * 100) - 100;
2470
- };
2471
- var IMG = /^data:image\/(png|jpeg|gif|webp);base64,[A-Za-z0-9+/]+={0,2}$/;
2472
- var HTTPS = (s) => {
2473
- try {
2474
- const u = new URL(s);
2475
- return u.protocol === "https:";
2476
- } catch {
2477
- return false;
2478
- }
2479
- };
2480
- function bunnyMetadata(i) {
2481
- const o = {};
2482
- const d = (i.description ?? "").trim();
2483
- if (d) o.description = d;
2484
- if (i.image) o.image = i.image;
2485
- for (const k of ["website", "twitter", "telegram"]) {
2486
- const v = (i[k] ?? "").trim();
2487
- if (v) o[k] = v;
2488
- }
2489
- return JSON.stringify(o);
2490
- }
2491
- function parseBunny(i, st) {
2492
- const issues = [];
2493
- const bad = (field, message) => issues.push({ field, message });
2494
- const name = (i.name ?? "").trim(), symbol = (i.symbol ?? "").trim();
2495
- if (!name) bad("name", "Give the token a name.");
2496
- else if (utf8Len(name) > 50) bad("name", "Name can be at most 50 characters.");
2497
- else if (!PRINTABLE.test(name)) bad("name", "Name cannot contain control characters.");
2498
- if (!symbol) bad("symbol", "Give the token a symbol.");
2499
- else if (!/^[A-Za-z0-9]{1,12}$/.test(symbol)) bad("symbol", "Symbol must be 1 to 12 letters or digits.");
2500
- const desc = (i.description ?? "").trim();
2501
- if (desc.length > 1e3) bad("description", "Keep the description under 1,000 characters.");
2502
- if (i.image && !IMG.test(i.image)) bad("image", "Artwork must be a PNG, JPEG, GIF or WebP image.");
2503
- for (const k of ["website", "twitter", "telegram"]) {
2504
- const v = (i[k] ?? "").trim();
2505
- if (v && (!HTTPS(v) || v.length > 500)) bad(k, "Use a full https:// link.");
2506
- }
2507
- const buyTax = taxFromPct(i.buyFeePct || "1"), sellTax = taxFromPct(i.sellFeePct || "1");
2508
- if (buyTax == null) bad("buyFeePct", "Choose a buy fee from 1% to 4%.");
2509
- if (sellTax == null) bad("sellFeePct", "Choose a sell fee from 1% to 4%.");
2510
- const fr = (i.feeRecipient ?? "").trim();
2511
- if (fr && (!ADDR.test(fr) || fr.toLowerCase() === zeroAddress)) bad("feeRecipient", "Fee wallet must be a valid, non-zero address (or leave it empty for your wallet).");
2512
- const holderMode = i.holderMode === "true";
2513
- if (holderMode && fr) bad("holderMode", "Holder mode sends your fee share to buybacks, so a separate fee wallet cannot be used with it.");
2514
- const firstBuy = parseHypeAmount(i.firstBuy ?? "");
2515
- if (firstBuy == null) bad("firstBuy", "Enter a HYPE amount such as 0.5 (or leave it empty).");
2516
- const metadata = bunnyMetadata(i);
2517
- const limit = st ? Number(st.maxMetadataBytes) : 16e3;
2518
- if (utf8Len(metadata) > limit) bad("image", `The token metadata (description, links and artwork) is ${utf8Len(metadata).toLocaleString("en-US")} bytes; the limit is ${limit.toLocaleString("en-US")}. Use a smaller image or a shorter description.`);
2519
- if (st && firstBuy != null && buyTax != null && firstBuy > 0n) {
2520
- const max = bunnyMaxFirstBuy(st, buyTax);
2521
- if (firstBuy > max) bad("firstBuy", `The anti-snipe cap (${Number(st.maxWalletBps) / 100}% of supply per wallet in the first ${st.antiSnipeSeconds} seconds) allows a first buy of at most ${hype(max, 4)} HYPE.`);
2522
- }
2523
- if (issues.length) return { issues };
2524
- return { issues, value: { name, symbol, metadata, feeRecipient: fr ? getAddress5(fr) : zeroAddress, holderMode, buyTaxBps: buyTax, sellTaxBps: sellTax, firstBuy, feeRecipientInput: fr } };
2525
- }
2526
- async function checkBunny(client3) {
2527
- const problems = [];
2528
- const A = BUNNYPAD.launchpad;
2529
- let code;
2530
- try {
2531
- code = await retry(() => client3.getCode({ address: A }));
2532
- } catch (e2) {
2533
- throw new LaunchSimError("network", `Could not reach the Elysium RPC (${rawMessage(e2)}).`);
2534
- }
2535
- if (!code || code === "0x") {
2536
- return { ok: false, problems: [`No contract exists at the BunnyPad launchpad address ${A} on Elysium testnet.`], facts: [], readAt: Date.now(), data: emptyBunny() };
2537
- }
2538
- const c = (functionName, args = []) => ({ address: A, abi: BUNNY_ABI, functionName, args });
2539
- const rows = await multicallRows(client3, [
2540
- c("launchFee"),
2541
- c("paused"),
2542
- c("pairs", [zeroAddress]),
2543
- c("antiSnipeSeconds"),
2544
- c("maxWalletBps"),
2545
- c("MAX_TAX_BPS"),
2546
- c("BASE_FEE_BPS"),
2547
- c("MAX_METADATA_BYTES"),
2548
- c("TOTAL_SUPPLY"),
2549
- c("treasury"),
2550
- c("tokensCount"),
2551
- c("tokenFactory"),
2552
- { address: BUNNYPAD.tokenFactory, abi: BUNNY_FACTORY_ABI, functionName: "launchpad" }
2553
- ]).catch((e2) => {
2554
- throw new LaunchSimError("network", `Could not read BunnyPad on chain (${rawMessage(e2)}).`);
2555
- });
2556
- const ok = (k) => rows[k]?.status === "success";
2557
- const v = (k) => rows[k].result;
2558
- if (!ok(0)) problems.push("The launch fee could not be read from the contract.");
2559
- if (!ok(1)) problems.push("The paused flag could not be read from the contract.");
2560
- if (!ok(2)) problems.push("The HYPE pair configuration could not be read from the contract.");
2561
- if (!ok(8)) problems.push("The token supply could not be read from the contract.");
2562
- const pair = ok(2) ? v(2) : [false, 0n];
2563
- const factoryOk = ok(11) && ok(12) && String(v(11)).toLowerCase() === BUNNYPAD.tokenFactory.toLowerCase() && String(v(12)).toLowerCase() === A.toLowerCase();
2564
- if (!factoryOk) problems.push("The launchpad's token factory does not point back to the launchpad: not the contracts this integration was verified against.");
2565
- const data = {
2566
- launchFee: ok(0) ? v(0) : 0n,
2567
- paused: ok(1) ? v(1) : true,
2568
- pairEnabled: pair[0],
2569
- openingMarketCap: pair[1],
2570
- antiSnipeSeconds: ok(3) ? v(3) : 120n,
2571
- maxWalletBps: ok(4) ? v(4) : 200n,
2572
- maxTaxBps: ok(5) ? BigInt(v(5)) : 300n,
2573
- baseFeeBps: ok(6) ? BigInt(v(6)) : 100n,
2574
- maxMetadataBytes: ok(7) ? v(7) : 16000n,
2575
- totalSupply: ok(8) ? v(8) : 10n ** 27n,
2576
- treasury: ok(9) ? v(9) : zeroAddress,
2577
- tokensCount: ok(10) ? v(10) : 0n,
2578
- factoryOk
2579
- };
2580
- if (data.paused) problems.push("BunnyPad has paused new launches right now.");
2581
- if (!data.pairEnabled) problems.push("BunnyPad has not enabled HYPE as a pair asset right now.");
2582
- if (data.pairEnabled && data.openingMarketCap === 0n) problems.push("BunnyPad reports a zero opening liquidity for HYPE pairs.");
2583
- if (data.totalSupply !== 10n ** 27n) problems.push("BunnyPad's token supply is not the 1,000,000,000 this integration expects.");
2584
- const facts = [
2585
- { label: "Launchpad contract", value: `${short(A)} \xB7 ${((code.length - 2) / 2).toLocaleString("en-US")} bytes of code`, tone: "good", mono: true, note: "Source verified on the Elysium explorer" },
2586
- { label: "Launch fee (live)", value: `${hype(data.launchFee)} HYPE`, tone: "good" },
2587
- { label: "HYPE pair", value: data.pairEnabled ? `enabled, opening virtual liquidity ${hype(data.openingMarketCap, 4)} HYPE` : "disabled", tone: data.pairEnabled ? "good" : "warn" },
2588
- { label: "New launches", value: data.paused ? "paused" : "open", tone: data.paused ? "warn" : "good" },
2589
- { label: "Launched so far", value: group(data.tokensCount) }
2590
- ];
2591
- return { ok: problems.length === 0, problems, facts, readAt: Date.now(), data };
2592
- }
2593
- var emptyBunny = () => ({ launchFee: 0n, paused: true, pairEnabled: false, openingMarketCap: 0n, antiSnipeSeconds: 0n, maxWalletBps: 0n, maxTaxBps: 0n, baseFeeBps: 0n, maxMetadataBytes: 0n, totalSupply: 0n, treasury: zeroAddress, tokensCount: 0n, factoryOk: false });
2594
- function bunnyArgs(p, st) {
2595
- const out = p.firstBuy > 0n ? bunnyFirstBuyOut(st, p.buyTaxBps, p.firstBuy).tokensOut : 0n;
2596
- const minTokensOut = out * 99n / 100n;
2597
- return { name: p.name, symbol: p.symbol, metadata: p.metadata, quote: zeroAddress, feeRecipient: p.feeRecipient, holderMode: p.holderMode, buyTaxBps: p.buyTaxBps, sellTaxBps: p.sellTaxBps, firstBuy: p.firstBuy, minTokensOut };
2598
- }
2599
- function bunnyBuildTx(input, status) {
2600
- if (!isBunny(status.data)) throw new Error("BunnyPad status required");
2601
- const { value, issues } = parseBunny(input, status.data);
2602
- if (!value) throw new Error(issues[0]?.message ?? "Invalid launch input");
2603
- return {
2604
- to: BUNNYPAD.launchpad,
2605
- data: encodeFunctionData({ abi: BUNNY_ABI, functionName: "createToken", args: [bunnyArgs(value, status.data)] }),
2606
- value: status.data.launchFee + value.firstBuy
2607
- };
2608
- }
2609
- function bunnyDescribe(input, status, from) {
2610
- const st = status.data;
2611
- const { value: p } = parseBunny(input, st);
2612
- const lines = [], warnings = [];
2613
- const sym = (input.symbol ?? "").trim() || "token";
2614
- lines.push({ label: "Token", value: input.name?.trim() || input.symbol?.trim() ? `${input.name?.trim() || "\u2026"} (${input.symbol?.trim() || "\u2026"})` : "enter a name and symbol", tone: input.name?.trim() ? void 0 : "muted" });
2615
- lines.push({ label: "Supply, fixed forever", value: `${group(st.totalSupply / 10n ** 18n)} ${sym}`, note: "18 decimals. Minted once; the token has no owner, no mint and no tax." });
2616
- lines.push({ label: "Where the supply goes", value: "100% into the token's pool", note: `The pool lives inside the BunnyLaunchpad contract (${short(BUNNYPAD.launchpad)}). No wallet receives the supply, you included.` });
2617
- lines.push({ label: "Pool", value: `${sym} / HYPE`, note: `Opens with ${hype(st.openingMarketCap, 4)} HYPE of virtual liquidity (sets the starting price). You seed nothing. The pool has no withdraw function, so the liquidity is locked.` });
2618
- lines.push({ label: "Launch fee", value: `${hype(st.launchFee)} HYPE`, note: `Paid to the BunnyPad treasury ${short(st.treasury)} (read from the contract).` });
2619
- const buyPct = p ? (Number(st.baseFeeBps) + p.buyTaxBps) / 100 : null, sellPct = p ? (Number(st.baseFeeBps) + p.sellTaxBps) / 100 : null;
2620
- lines.push({ label: "Trading fee", value: p ? `${buyPct}% on buys, ${sellPct}% on sells` : "choose 1% to 4%", note: `Charged in HYPE on every trade: ${Number(CREATOR_SHARE_BPS) / 100}% to the fee recipient, ${100 - Number(CREATOR_SHARE_BPS) / 100}% to BunnyPad. You can lower it later, never raise it.` });
2621
- const recipient = p?.holderMode ? "holder buybacks (bought and burned)" : p?.feeRecipient && p.feeRecipient !== zeroAddress ? short(p.feeRecipient) : from ? `${short(from)} (your wallet)` : "your wallet";
2622
- lines.push({ label: "Fee recipient", value: recipient, mono: !!p?.feeRecipientInput });
2623
- const fb = p?.firstBuy ?? 0n;
2624
- if (p && fb > 0n) {
2625
- const r = bunnyFirstBuyOut(st, p.buyTaxBps, fb);
2626
- const pct = Number(r.tokensOut * 1000000n / st.totalSupply) / 1e4;
2627
- lines.push({ label: "Your first buy", value: `${hype(fb)} HYPE \u2192 about ${group(r.tokensOut / 10n ** 18n)} ${sym}`, note: `${pct.toFixed(2)}% of supply, to your wallet, after a ${hype(r.fee, 6)} HYPE trading fee. Same pool and price as any later buyer. Capped at ${Number(st.maxWalletBps) / 100}% of supply for the first ${st.antiSnipeSeconds} seconds.` });
2628
- } else lines.push({ label: "Your first buy", value: "none", tone: "muted", note: "Optional. Without it, you hold none of the supply." });
2629
- const total = st.launchFee + fb;
2630
- lines.push({ label: "You send", value: `${hype(total)} HYPE`, tone: void 0, note: `Launch fee ${hype(st.launchFee)}${fb > 0n ? ` + first buy ${hype(fb)}` : ""}, plus network gas.` });
2631
- if (p?.feeRecipient && from && p.feeRecipient !== zeroAddress && p.feeRecipient.toLowerCase() !== from.toLowerCase()) warnings.push("The creator's share of every trading fee goes to a different address than your connected wallet.");
2632
- return { lines, warnings, valueWei: total };
2633
- }
2634
- function bunnyParse(receipt) {
2635
- const found = { launched: null, first: null };
2636
- for (const log of receipt.logs) {
2637
- if (log.address.toLowerCase() !== BUNNYPAD.launchpad.toLowerCase()) continue;
2638
- try {
2639
- const ev = decodeEventLog({ abi: BUNNY_ABI, data: log.data, topics: log.topics });
2640
- if (ev.eventName === "TokenLaunched") found.launched = ev.args;
2641
- else if (ev.eventName === "FirstBuy") found.first = { quoteIn: ev.args.quoteIn, tokensOut: ev.args.tokensOut };
2642
- } catch {
2643
- }
2644
- }
2645
- const l = found.launched, first = found.first;
2646
- if (!l) throw new Error("The transaction has no BunnyPad TokenLaunched event.");
2647
- const lines = [
2648
- { label: "Pool", value: `inside BunnyLaunchpad (${short(BUNNYPAD.launchpad)})`, note: "No separate pool contract: the launchpad holds every token's liquidity and its pool has no withdraw function." },
2649
- { label: "Pair asset", value: l.quote === zeroAddress ? "HYPE" : `${short(l.quote)} (not HYPE: launched outside this app)`, mono: l.quote !== zeroAddress },
2650
- { label: "Opening virtual liquidity", value: l.quote === zeroAddress ? `${hype(l.virtualQuote, 4)} HYPE` : `${l.virtualQuote.toString()} raw units` },
2651
- { label: "Trading fee", value: `${(100 + l.buyTaxBps) / 100}% buys, ${(100 + l.sellTaxBps) / 100}% sells` },
2652
- { label: "Fee recipient", value: short(l.feeRecipient), mono: true },
2653
- { label: "Anti-snipe window ends", value: new Date(Number(l.antiSnipeUntil) * 1e3).toISOString().replace("T", " ").slice(0, 19) + " UTC" }
2654
- ];
2655
- if (first) lines.splice(1, 0, { label: "First buy", value: `${l.quote === zeroAddress ? `${hype(first.quoteIn)} HYPE` : `${first.quoteIn.toString()} raw units`} \u2192 ${group(first.tokensOut / 10n ** 18n)} ${l.symbol}` });
2656
- return { provider: "bunnypad", token: getAddress5(l.token), pool: null, poolHolder: BUNNYPAD.launchpad, creator: getAddress5(l.creator), name: l.name, symbol: l.symbol, decimals: 18, totalSupply: 10n ** 27n, lines, tradeUrl: bunnyTokenUrl(getAddress5(l.token)) };
2657
- }
2658
- async function readTokens(client3, addrs) {
2659
- if (!addrs.length) return [];
2660
- const fields = ["name", "symbol", "decimals", "totalSupply"];
2661
- const rows = await multicallRows(client3, addrs.flatMap((a) => fields.map((f) => ({ address: a, abi: erc20Abi6, functionName: f }))));
2662
- return addrs.map((address, i) => {
2663
- const r = rows.slice(i * 4, i * 4 + 4).map((x) => x.status === "success" ? x.result : null);
2664
- return { address, name: r[0], symbol: r[1], decimals: r[2] == null ? null : Number(r[2]), totalSupply: r[3] };
2665
- });
2666
- }
2667
- var BUNNY_GUARANTEES = [
2668
- "One transaction creates the token and its pool. 100% of the 1,000,000,000 supply goes into the pool; there is no presale and no team allocation.",
2669
- "The token is a plain ERC-20: no owner, no mint, no pause, no blacklist and no transfer tax. Its only rule is a time-boxed anti-snipe wallet cap.",
2670
- "The pool has no function to withdraw liquidity, so nobody (you, BunnyPad, Zypher) can pull it. Source verified on the Elysium explorer; the owner can change fees for new launches, pause new launches and the launch fee, but cannot touch pools or mint."
2671
- ];
2672
- var BUNNY_RISKS = [
2673
- "Third-party contracts, unaudited (BunnyPad says so itself). Zypher does not run or control them.",
2674
- "The pool is internal to BunnyPad: the token trades on bunnypad.fun, not on Dex Da Costa, HexSwap or Kalos, so Zypher Swap and Market do not price it.",
2675
- "Tokens launched this way are permissionless and most go to zero. Testnet tokens have no real value.",
2676
- "Name, symbol and artwork cannot be changed after launch."
2677
- ];
2678
- var bunnypadProvider = {
2679
- id: "bunnypad",
2680
- name: "BunnyPad",
2681
- kind: "third-party",
2682
- chainId: ELYSIUM_TESTNET_ID,
2683
- tagline: "One transaction: a fixed-supply token with its own HYPE pool",
2684
- description: "BunnyPad is a fair-launch launchpad that lives on the Elysium testnet. You pay a small launch fee and gas; the contract creates a 1B-supply token and puts all of it into a HYPE pool that nobody can withdraw from. Zypher builds the transaction and your wallet signs it.",
2685
- website: BUNNYPAD.site,
2686
- sourceUrl: `https://testnet.elysium.kinetiq.xyz/address/${BUNNYPAD.launchpad}`,
2687
- contracts: [{ label: "BunnyLaunchpad", address: BUNNYPAD.launchpad }, { label: "BunnyTokenFactory", address: BUNNYPAD.tokenFactory }],
2688
- fees: ["Launch fee: read live from the contract (0.015 HYPE when this was written), paid to BunnyPad's treasury.", "Trading fee on every trade of your token: 1% to 4% (you choose, separately for buys and sells); 70% to the fee recipient, 30% to BunnyPad.", "Network gas: a few thousandths of a HYPE for a small token."],
2689
- guarantees: BUNNY_GUARANTEES,
2690
- risks: BUNNY_RISKS,
2691
- glance: (status) => [
2692
- { label: "Supply", value: "1,000,000,000, fixed" },
2693
- { label: "Liquidity", value: "HYPE pool at launch, locked" },
2694
- { label: "Cost", value: status && isBunny(status.data) && status.ok ? `${hype(status.data.launchFee)} HYPE launch fee + gas` : "small launch fee + gas" }
2695
- ],
2696
- params: [
2697
- { key: "name", label: "Name", kind: "text", placeholder: "e.g. Zypher Test Coin", maxLength: 50 },
2698
- { key: "symbol", label: "Symbol", kind: "text", placeholder: "e.g. CYT", maxLength: 12 },
2699
- { key: "image", label: "Artwork (optional)", kind: "image", optional: true, hint: "Stored on-chain inside the token." },
2700
- { key: "description", label: "Description (optional)", kind: "textarea", optional: true, maxLength: 1e3, placeholder: "What is this token?" },
2701
- { key: "firstBuy", label: "First buy (optional)", kind: "number", optional: true, inputmode: "decimal", suffix: "HYPE", placeholder: "0", hint: "Bought at the opening price in the same transaction." },
2702
- { key: "buyFeePct", label: "Fee on buys", kind: "select", advanced: true, options: FEE_OPTIONS.map((v) => ({ value: v, label: `${v}%` })), hint: "Total fee per trade. 1% is the minimum." },
2703
- { key: "sellFeePct", label: "Fee on sells", kind: "select", advanced: true, options: FEE_OPTIONS.map((v) => ({ value: v, label: `${v}%` })) },
2704
- { key: "feeRecipient", label: "Fee wallet (optional)", kind: "address", advanced: true, optional: true, mono: true, placeholder: "0x\u2026 (defaults to your wallet)", hint: "Receives your 70% share of trading fees." },
2705
- { key: "holderMode", label: "Holder mode", kind: "toggle", advanced: true, optional: true, hint: "Use your fee share to buy back and burn the token instead of paying you." },
2706
- { key: "website", label: "Website (optional)", kind: "text", advanced: true, optional: true, placeholder: "https://", maxLength: 500 },
2707
- { key: "twitter", label: "X / Twitter link (optional)", kind: "text", advanced: true, optional: true, placeholder: "https://x.com/\u2026", maxLength: 500 },
2708
- { key: "telegram", label: "Telegram link (optional)", kind: "text", advanced: true, optional: true, placeholder: "https://t.me/\u2026", maxLength: 500 }
2709
- ],
2710
- defaults: () => ({ name: "", symbol: "", image: "", description: "", firstBuy: "", buyFeePct: "1", sellFeePct: "1", feeRecipient: "", holderMode: "", website: "", twitter: "", telegram: "" }),
2711
- validate: (input, status) => parseBunny(input, status && isBunny(status.data) ? status.data : null).issues,
2712
- check: checkBunny,
2713
- describe: bunnyDescribe,
2714
- buildTx: (input, _from, status) => bunnyBuildTx(input, status),
2715
- async simulate(client3, input, from, status) {
2716
- const tx = bunnyBuildTx(input, status);
2717
- const st = status.data;
2718
- const r = await simulateTx(client3, tx, from, BUNNY_ABI, BUNNY_FRIENDLY, (ret) => {
2719
- try {
2720
- return getAddress5(`0x${ret.slice(-40)}`);
2721
- } catch {
2722
- return null;
2723
- }
2724
- });
2725
- const { value: p } = parseBunny(input, st);
2726
- const tokensOut = p && p.firstBuy > 0n ? bunnyFirstBuyOut(st, p.buyTaxBps, p.firstBuy).tokensOut : null;
2727
- const notes = [];
2728
- if (r.overridden) notes.push("Your wallet does not hold enough HYPE for this launch yet, so the check ran with a temporary balance. The contract logic passed; sending needs real HYPE.");
2729
- return { ...r, tokensOut, notes };
2730
- },
2731
- parseResult: bunnyParse,
2732
- async list(client3, creator, limit = 10) {
2733
- const all = await retry(() => client3.readContract({ address: BUNNYPAD.launchpad, abi: BUNNY_ABI, functionName: "tokensCreatedBy", args: [creator] }));
2734
- return readTokens(client3, [...all].reverse().slice(0, limit));
2735
- }
2736
- };
2737
-
2738
- // src/market/reads.ts
2739
- var hlNet = () => env.network;
2740
- var CANDLE_INTERVALS = ["1m", "5m", "15m", "1h", "4h", "1d"];
2741
- var rnd2 = (x, d = 4) => x == null || !Number.isFinite(x) ? null : Number(x.toFixed(d));
2742
- var safeSymbol = (s) => s.replace(/[^A-Za-z0-9._$-]/g, "").slice(0, 12) || "?";
2743
- var elysiumClient = getBatchClient();
2744
- var client2 = elysiumClient;
2745
- async function marketData() {
2746
- const b = (await marketCache.get()).value;
2747
- return { tokens: b.payload.tokens, pairs: b.pairs };
2748
- }
2749
- var perpCache = swr(5e3, 12e4, () => getPerpMarkets("testnet"));
2750
- var perpCacheMainnet = swr(5e3, 12e4, () => getPerpMarkets("mainnet"));
2751
- var hlMarkets = (net = hlNet()) => net === "mainnet" ? perpCacheMainnet.get() : perpCache.get();
2752
- var hlLabel = (net) => net === "mainnet" ? "Hyperliquid mainnet (read-only)" : "Hyperliquid testnet";
2753
- var mainnetOnly = (net, coin) => {
2754
- void net;
2755
- void coin;
2756
- };
2757
- var toRef = (t) => ({ address: t.address, symbol: safeSymbol(t.symbol), decimals: t.decimals });
2758
- async function resolveToken(q) {
2759
- const s = q.trim();
2760
- if (/^(hype|native)$/i.test(s)) return NATIVE;
2761
- const { tokens } = await marketData();
2762
- if (isAddress3(s)) {
2763
- const a = getAddress6(s);
2764
- const known2 = tokens.find((t) => t.address === a);
2765
- if (known2) return toRef(known2);
2766
- return toRef(await importToken(client2, a));
2767
- }
2768
- const hits = tokens.filter((t) => t.tradable && t.symbol.toLowerCase() === s.toLowerCase());
2769
- if (hits.length === 1) return toRef(hits[0]);
2770
- if (hits.length > 1) throw new Error(`Symbol "${safeSymbol(s)}" is ambiguous on Elysium testnet; use a token address: ${hits.map((h) => h.address).join(", ")}`);
2771
- throw new Error(`Unknown token "${s.slice(0, 60)}". Use HYPE, a token address, or a symbol from elysium_tokens.`);
2772
- }
2773
- var tokenOut = (t) => ({
2774
- symbol: safeSymbol(t.symbol),
2775
- name: untrusted(t.name, 60),
2776
- address: t.address,
2777
- decimals: t.decimals,
2778
- priceHype: rnd2(t.priceHype, 8),
2779
- priceUsdc: rnd2(t.priceUsdc, 8),
2780
- liquidityHype: rnd2(t.liquidityHype, 2),
2781
- pairs: t.pairs,
2782
- venues: t.venues,
2783
- swapVenues: t.swapVenues,
2784
- explorer: explorerToken(t.address)
2785
- });
2786
- async function marketTokens() {
2787
- const { tokens, pairs } = await marketData();
2788
- const swappable = tokens.filter((t) => t.tradable);
2789
- const sorted = [...swappable].sort((a, b) => (b.liquidityHype ?? 0) - (a.liquidityHype ?? 0));
2790
- return {
2791
- network: "Elysium testnet (chain 99801)",
2792
- venues: SWAP_VENUES.map((v) => `${v.name} (${v.kind === "v3" ? "Uniswap V3 style" : "Uniswap V2 style"}; ${v.note})`),
2793
- pools: pairs.length,
2794
- tokenCount: swappable.length,
2795
- note: "Only tokens with a live pool on a venue the app swaps through (Dex Da Costa, HexSwap, Elysium Hunt Swap, Kalos, PerpMe; the PerpMe list only has pools with at least 1 HYPE, other PerpMe tokens still quote by address). This is NOT every token on the chain: other tokens are analysis-only in the Market page (token_health still works) and the explorer's token list has no public API. No aggregator (0x etc.) exists on the testnet yet. Testnet tokens have no real value; liquidity is tiny.",
2796
- tokens: sorted.slice(0, 50).map(tokenOut),
2797
- truncated: sorted.length > 50
2798
- };
2799
- }
2800
- async function quoteSwapRaw(tokenInQ, tokenOutQ, amount) {
2801
- if (!/^\d+(\.\d+)?$/.test(amount.trim())) throw new Error('amount must be a positive decimal number string, e.g. "1.5"');
2802
- const [tin, tout] = await Promise.all([resolveToken(tokenInQ), resolveToken(tokenOutQ)]);
2803
- const amountInRaw = parseAmount(amount.trim(), tin.decimals);
2804
- if (amountInRaw <= 0n) throw new Error("amount must be greater than zero");
2805
- let results = await quoteVenues(client2, tin, tout, amountInRaw);
2806
- if (!results.some((r) => r.quote) && results.some((r) => r.reason === "rpc_error")) {
2807
- await sleep2(500);
2808
- results = await quoteVenues(client2, tin, tout, amountInRaw);
2809
- }
2810
- const routes = results.flatMap((r) => r.quote ? [r.quote] : []).sort((x, y) => x.amountOutRaw === y.amountOutRaw ? 0 : x.amountOutRaw > y.amountOutRaw ? -1 : 1);
2811
- if (!routes.length) throw new Error(`No route between ${tin.symbol} and ${tout.symbol}: ${noRouteMessage(results)}`);
2812
- const impacts = await Promise.all(routes.map((r) => routeImpactPct(client2, r)));
2813
- routes.forEach((r, i) => {
2814
- r.priceImpactPct = impacts[i];
2815
- });
2816
- return { tin, tout, amountInRaw, q: routes[0], routes, results };
2817
- }
2818
- async function quoteSwap(tokenInQ, tokenOutQ, amount) {
2819
- const { tin, tout, amountInRaw, q, routes, results } = await quoteSwapRaw(tokenInQ, tokenOutQ, amount);
2820
- const impact = rnd2(q.priceImpactPct, 3);
2821
- const kindName = (k) => k === "v3" ? "Uniswap V3 router" : "Uniswap V2 router";
2822
- return {
2823
- venue: `${q.venue.name} (third-party ${kindName(q.kind)} ${q.venue.router}; best quote among ${SWAP_VENUES.map((v) => v.name).join(" / ")})`,
2824
- network: "Elysium testnet",
2825
- venueId: q.venue.id,
2826
- kind: q.kind,
2827
- fees: q.fees,
2828
- tokenIn: { symbol: tin.symbol, address: tin.address, decimals: tin.decimals },
2829
- tokenOut: { symbol: tout.symbol, address: tout.address, decimals: tout.decimals },
2830
- amountIn: fmtAmount(amountInRaw, tin.decimals, 8),
2831
- amountOut: fmtAmount(q.amountOutRaw, tout.decimals, 8),
2832
- amountOutRaw: q.amountOutRaw.toString(),
2833
- path: q.path,
2834
- priceImpactPct: impact,
2835
- alternatives: routes.slice(1).map((r) => ({ venue: r.venue.name, amountOut: fmtAmount(r.amountOutRaw, tout.decimals, 8), priceImpactPct: rnd2(r.priceImpactPct, 3) })),
2836
- noRoute: results.filter((r) => !r.quote && r.reason !== "same_token").map((r) => ({ venue: r.venue.name, reason: r.reason })),
2837
- warning: impact === null ? "Price impact could not be computed." : impact > 5 ? `Price impact ${impact}% is above 5%: liquidity is very thin.` : null
2838
- };
2839
- }
2840
- var marketOut = (m) => ({
2841
- coin: m.name,
2842
- markPx: rnd2(m.markPx, 6),
2843
- change24hPct: rnd2(m.change24hPct, 2),
2844
- volume24hUsd: rnd2(m.volume24hUsd, 0),
2845
- openInterestUsd: rnd2(m.openInterestUsd, 0),
2846
- fundingHourlyPct: rnd2(m.fundingHourly * 100, 5),
2847
- maxLeverage: m.maxLeverage,
2848
- onlyIsolated: m.onlyIsolated
2849
- });
2850
- var SORT_KEYS = ["volume", "openInterest", "change", "funding"];
2851
- async function perpMarkets(limit = 15, sortBy = "volume", net = hlNet()) {
2852
- const all = (await hlMarkets(net)).value;
2853
- const key = {
2854
- volume: (m) => m.volume24hUsd,
2855
- openInterest: (m) => m.openInterestUsd,
2856
- change: (m) => Math.abs(m.change24hPct ?? 0),
2857
- funding: (m) => Math.abs(m.fundingHourly)
2858
- };
2859
- const rows = all.filter((m) => Number.isFinite(m.markPx)).sort((a, b) => key[sortBy](b) - key[sortBy](a));
2860
- return { network: hlLabel(net), total: rows.length, sortedBy: sortBy, markets: rows.slice(0, limit).map(marketOut) };
2861
- }
2862
- async function findPerp(coin, net = hlNet()) {
2863
- mainnetOnly(net, coin);
2864
- const m = (await hlMarkets(net)).value.find((x) => x.name.toLowerCase() === coin.trim().toLowerCase());
2865
- if (!m) throw new Error(`Market ${coin.slice(0, 20)} not found on ${hlLabel(net)}`);
2866
- return m;
2867
- }
2868
- async function perpCandles(coin, interval, count, net = hlNet()) {
2869
- const m = await findPerp(coin, net);
2870
- const cs = await getCandles(net, m.name, interval, count);
2871
- if (!cs.length) return { coin: m.name, interval, candles: [], summary: null };
2872
- const closes = cs.map((c) => c.close);
2873
- const sma = (n3) => closes.length >= n3 ? closes.slice(-n3).reduce((a, b) => a + b, 0) / n3 : null;
2874
- const first = cs[0], last = cs[cs.length - 1];
2875
- return {
2876
- network: hlLabel(net),
2877
- coin: m.name,
2878
- interval,
2879
- count: cs.length,
2880
- summary: {
2881
- from: new Date(first.time * 1e3).toISOString(),
2882
- to: new Date(last.time * 1e3).toISOString(),
2883
- firstClose: first.close,
2884
- lastClose: last.close,
2885
- changePct: rnd2((last.close - first.close) / first.close * 100, 2),
2886
- high: Math.max(...cs.map((c) => c.high)),
2887
- low: Math.min(...cs.map((c) => c.low)),
2888
- avgVolume: rnd2(cs.reduce((s, c) => s + c.volume, 0) / cs.length, 2),
2889
- sma20: rnd2(sma(20), 6),
2890
- sma50: rnd2(sma(50), 6)
2891
- },
2892
- last20: cs.slice(-20).map((c) => ({ t: new Date(c.time * 1e3).toISOString(), o: c.open, h: c.high, l: c.low, c: c.close, v: rnd2(c.volume, 2) }))
2893
- };
2894
- }
2895
- async function hlAccount(address, net = hlNet()) {
2896
- if (!isAddress3(address)) throw new Error("Invalid address");
2897
- const [acct, orders] = await Promise.all([getAccountState(net, address), getOpenOrders(net, address)]);
2898
- return { network: hlLabel(net), address, account: acct, openOrders: orders.slice(0, 50) };
2899
- }
2900
-
2901
- // src/market/bunny-route.ts
2902
- import { encodeAbiParameters as encodeAbiParameters2, formatUnits as formatUnits5, getAddress as getAddress7, isAddress as isAddress5, keccak256 as keccak2562, pad as pad2, toHex as toHex2, zeroAddress as zeroAddress2 } from "viem";
2903
-
2904
- // src/shared/elysium/swap.ts
2905
- import { decodeFunctionResult, encodeAbiParameters, encodeFunctionData as encodeFunctionData2, isAddress as isAddress4, keccak256, pad, toHex } from "viem";
2906
-
2907
- // src/shared/elysium/simerr.ts
2908
- import { createPublicClient as createPublicClient3, decodeErrorResult as decodeErrorResult2, http as http3, parseAbi as parseAbi5 } from "viem";
2909
- var errAbi = parseAbi5(["error Error(string message)", "error Panic(uint256 code)"]);
2910
-
2911
- // src/shared/elysium/swap.ts
2912
- function minOut(amountOutRaw, slippageBps) {
2913
- return amountOutRaw * BigInt(1e4 - slippageBps) / 10000n;
2914
- }
2915
-
2916
- // src/shared/launch/bunnypad-trade.ts
2917
- import { encodeFunctionData as encodeFunctionData3, erc20Abi as erc20Abi7, parseAbi as parseAbi6 } from "viem";
2918
- var ZERO2 = "0x0000000000000000000000000000000000000000";
2919
- var BPS2 = 10000n;
2920
- var BASE_FEE_BPS = 100n;
2921
- var TRADE_ABI = parseAbi6([
2922
- "function buy(address token, uint256 amountIn, uint256 minTokensOut, address to) payable returns (uint256 tokensOut)",
2923
- "function sell(address token, uint256 tokensIn, uint256 minQuoteOut, address to) returns (uint256 quoteOut)",
2924
- "function quoteBuy(address token, uint256 quoteIn) view returns (uint256 tokensOut, uint256 fee)",
2925
- "function quoteSell(address token, uint256 tokensIn) view returns (uint256 quoteOut, uint256 fee)",
2926
- "function getLaunch(address token) view returns ((address creator, address quote, address feeRecipient, uint64 createdAt, uint64 antiSnipeUntil, uint128 virtualQuote, uint128 realQuote, uint128 tokenReserve, uint16 buyTaxBps, uint16 sellTaxBps, bool holderMode))"
2927
- ]);
2928
- async function readPool(client3, token) {
2929
- if (!ADDR.test(token)) throw new Error("not an address");
2930
- const l = await retry(() => client3.readContract({ address: BUNNYPAD.launchpad, abi: TRADE_ABI, functionName: "getLaunch", args: [token] }));
2931
- if (l.creator === ZERO2) throw new Error("This is not a BunnyPad token.");
2932
- return { token, quote: l.quote, creator: l.creator, feeRecipient: l.feeRecipient, antiSnipeUntil: Number(l.antiSnipeUntil), virtualQuote: l.virtualQuote, realQuote: l.realQuote, tokenReserve: l.tokenReserve, buyTaxBps: l.buyTaxBps, sellTaxBps: l.sellTaxBps, holderMode: l.holderMode };
2933
- }
2934
- async function quoteTrade(client3, pool, side, amountIn, slippageBps = 100) {
2935
- if (amountIn <= 0n) throw new Error("Enter an amount.");
2936
- const P = pool.virtualQuote + pool.realQuote, R = pool.tokenReserve;
2937
- const [out, fee] = await retry(() => client3.readContract({ address: BUNNYPAD.launchpad, abi: TRADE_ABI, functionName: side === "buy" ? "quoteBuy" : "quoteSell", args: [pool.token, amountIn] }));
2938
- const feeBps = BASE_FEE_BPS + BigInt(side === "buy" ? pool.buyTaxBps : pool.sellTaxBps);
2939
- const impact = side === "buy" ? Number((amountIn - fee) * 10000n / (P + (amountIn - fee))) / 100 : Number(amountIn * 10000n / (R + amountIn)) / 100;
2940
- return { side, amountIn, out, minOut: out * (BPS2 - BigInt(slippageBps)) / BPS2, fee, feePct: Number(feeBps) / 100, impactPct: impact, slippageBps };
2941
- }
2942
-
2943
- // src/market/launchpad-index.ts
2944
- var launchpadTokens = async (_q) => ({ tokens: [] });
2945
-
2946
- // src/market/bunny-route.ts
2947
- var BUNNY_VENUE_ID = "bunnypad";
2948
- var BUNNY_VENUE_NAME = "BunnyPad (bonding curve pool)";
2949
- var defaultLookup = async (q) => (await launchpadTokens({ tab: "new", q, limit: 25 })).tokens.map((t) => ({ address: t.address, symbol: t.symbol, name: t.name }));
2950
- var defaultHypeWei = async (t, amountRaw) => (await quoteBestRoute(elysiumClient, t, NATIVE, amountRaw))?.amountOutRaw ?? null;
2951
- var bunnyHooks = () => ({});
2952
- var depsOf = (d0 = {}) => {
2953
- const d = { ...bunnyHooks().deps ?? {}, ...d0 };
2954
- return {
2955
- client: d.client ?? elysiumClient,
2956
- resolve: d.resolve ?? resolveToken,
2957
- lookup: d.lookup ?? defaultLookup,
2958
- tokenRef: d.tokenRef ?? ((a) => (d.resolve ?? resolveToken)(a)),
2959
- dex: d.dex ?? quoteSwapRaw,
2960
- hypeWei: d.hypeWei ?? defaultHypeWei
2961
- };
2962
- };
2963
- var clean = (q) => q.trim().replace(/^\$+/, "").trim();
2964
- async function poolOf(d, address) {
2965
- try {
2966
- return await readPool(d.client, getAddress7(address));
2967
- } catch {
2968
- return null;
2969
- }
2970
- }
2971
- async function findBunnyToken(q, deps = {}) {
2972
- const d = depsOf(deps), s = clean(q);
2973
- if (!s || /^(hype|native)$/i.test(s)) return null;
2974
- if (isAddress5(s)) return await poolOf(d, s) ? getAddress7(s) : null;
2975
- let hits = [];
2976
- try {
2977
- hits = await d.lookup(s);
2978
- } catch {
2979
- return null;
2980
- }
2981
- const lc = s.toLowerCase();
2982
- const bySym = hits.filter((h) => h.symbol.toLowerCase() === lc);
2983
- const byName = hits.filter((h) => h.name.trim().toLowerCase() === lc);
2984
- const pick = bySym.length ? bySym : byName.length ? byName : hits.length === 1 ? hits : [];
2985
- if (pick.length === 1) return getAddress7(pick[0].address);
2986
- if (pick.length > 1 || hits.length > 1) {
2987
- const list = (pick.length ? pick : hits).slice(0, 6).map((h) => `${safeSymbol(h.symbol)} ${h.address}`).join(", ");
2988
- throw new Error(`Several BunnyPad tokens match "${safeSymbol(s)}": ${list}. Use the token address.`);
2989
- }
2990
- return null;
2991
- }
2992
- async function resolveSide(q, d) {
2993
- const s = clean(q);
2994
- if (/^(hype|native)$/i.test(s)) return { kind: "native" };
2995
- if (isAddress5(s)) {
2996
- const pool = await poolOf(d, s);
2997
- if (pool) return { kind: "bunny", pool, ref: await d.tokenRef(pool.token) };
2998
- return { kind: "token", ref: await d.resolve(s) };
2999
- }
3000
- const dex = await d.resolve(s).catch((e2) => e2);
3001
- if (!(dex instanceof Error)) return { kind: "token", ref: dex };
3002
- if (/ambiguous/i.test(dex.message)) throw dex;
3003
- const addr = await findBunnyToken(s, d);
3004
- if (addr) {
3005
- const pool = await poolOf(d, addr);
3006
- if (pool) return { kind: "bunny", pool, ref: await d.tokenRef(pool.token) };
3007
- }
3008
- throw dex;
3009
- }
3010
- var isPair = (pool, t) => pool.quote === zeroAddress2 ? t.address === "native" : t.address !== "native" && same(t.address, pool.quote);
3011
- var qOf = (t) => t.address === "native" ? "HYPE" : t.address;
3012
- var dec = (raw, t) => formatUnits5(raw, t.decimals);
3013
- async function bunnyLeg(d, pool, side, tin, tout, amountInRaw, slippageBps, step) {
3014
- const q = await quoteTrade(d.client, pool, side, amountInRaw, slippageBps).catch((e2) => {
3015
- throw new Error(`BunnyPad could not quote this ${side}: ${String(e2?.message ?? e2).split("\n")[0].slice(0, 160)}`);
3016
- });
3017
- if (q.out <= 0n) throw new Error(`BunnyPad would pay out nothing for this ${side}: the amount is too small.`);
3018
- return { step, venue: "bunnypad", venueName: BUNNY_VENUE_NAME, action: side, tokenIn: tin, tokenOut: tout, amountInRaw, expectedOutRaw: q.out, minOutRaw: q.minOut, impactPct: q.impactPct, feePct: q.feePct, feeRaw: q.fee, feeAsset: side === "buy" ? tin : tout, bunny: q };
3019
- }
3020
- function dexLeg(r, slippageBps, step) {
3021
- return {
3022
- step,
3023
- venue: "dex",
3024
- venueName: r.q.venue.name,
3025
- action: "swap",
3026
- tokenIn: r.tin,
3027
- tokenOut: r.tout,
3028
- amountInRaw: r.amountInRaw,
3029
- expectedOutRaw: r.q.amountOutRaw,
3030
- minOutRaw: minOut(r.q.amountOutRaw, slippageBps),
3031
- impactPct: r.q.priceImpactPct,
3032
- dex: r.q
3033
- };
3034
- }
3035
- var combine = (a, b) => a === null || b === null ? null : rnd2((1 - (1 - a / 100) * (1 - b / 100)) * 100, 3);
3036
- var BunnyPlanError = class extends Error {
3037
- constructor(message, token) {
3038
- super(message);
3039
- this.token = token;
3040
- this.name = "BunnyPlanError";
3041
- }
3042
- token;
3043
- };
3044
- async function planBunnyRoute(tokenIn, tokenOut3, amount, slippageBps = 100, deps = {}) {
3045
- const d = depsOf(deps);
3046
- if (!/^\d+(\.\d+)?$/.test(amount.trim())) throw new Error('amount must be a positive decimal number string, e.g. "1.5"');
3047
- const [a, b] = await Promise.allSettled([resolveSide(tokenIn, d), resolveSide(tokenOut3, d)]);
3048
- for (const r of [a, b]) if (r.status === "rejected" && /Several BunnyPad tokens/.test(String(r.reason?.message))) throw new BunnyPlanError(String(r.reason.message), null);
3049
- const aBunny = a.status === "fulfilled" && a.value.kind === "bunny", bBunny = b.status === "fulfilled" && b.value.kind === "bunny";
3050
- if (!aBunny && !bBunny) return null;
3051
- if (a.status === "rejected") throw new BunnyPlanError(String(a.reason?.message ?? a.reason), null);
3052
- if (b.status === "rejected") throw new BunnyPlanError(String(b.reason?.message ?? b.reason), null);
3053
- const sa = a.value, sb = b.value;
3054
- const bunnyAddr = sb.kind === "bunny" ? sb.pool.token : sa.kind === "bunny" ? sa.pool.token : null;
3055
- try {
3056
- return await planResolved(d, sa, sb, amount, slippageBps);
3057
- } catch (e2) {
3058
- throw e2 instanceof BunnyPlanError ? e2 : new BunnyPlanError(String(e2?.message ?? e2), bunnyAddr);
3059
- }
3060
- }
3061
- async function planResolved(d, sa, sb, amount, slippageBps) {
3062
- if (sa.kind === "bunny" && sb.kind === "bunny") throw new Error("Both tokens are BunnyPad tokens. Swap one back to its pair asset (HYPE or USDC) first, then buy the other.");
3063
- const refOf = (s) => s.kind === "native" ? NATIVE : s.ref;
3064
- const tin = refOf(sa), tout = refOf(sb);
3065
- const amountInRaw = parseAmount(amount.trim(), tin.decimals);
3066
- if (amountInRaw <= 0n) throw new Error("amount must be greater than zero");
3067
- if (sb.kind === "bunny") {
3068
- const pool2 = sb.pool, pair2 = pool2.quote === zeroAddress2 ? NATIVE : await d.tokenRef(pool2.quote);
3069
- if (isPair(pool2, tin)) {
3070
- const leg = await bunnyLeg(d, pool2, "buy", tin, tout, amountInRaw, slippageBps, 1);
3071
- return { side: "buy", tin, tout, amountInRaw, pool: pool2, bunnyToken: sb.ref, pair: pair2, legs: [leg], twoStep: false, expectedOutRaw: leg.expectedOutRaw, minOutRaw: leg.minOutRaw, impactPct: rnd2(leg.impactPct, 3), slippageBps };
3072
- }
3073
- const r2 = await d.dex(qOf(tin), qOf(pair2), amount.trim()).catch((e2) => {
3074
- throw new Error(`${sb.ref.symbol} trades only in BunnyPad's pool against ${pair2.symbol}, and ${tin.symbol} cannot be swapped to ${pair2.symbol} on the DEX either (${String(e2?.message ?? e2).slice(0, 140)}). Fund the Agent Wallet with ${pair2.symbol} instead.`);
3075
- });
3076
- const l12 = dexLeg(r2, slippageBps, 1);
3077
- const l22 = await bunnyLeg(d, pool2, "buy", pair2, tout, l12.expectedOutRaw, slippageBps, 2);
3078
- const floor2 = await quoteTrade(d.client, pool2, "buy", l12.minOutRaw, slippageBps);
3079
- return { side: "buy", tin, tout, amountInRaw, pool: pool2, bunnyToken: sb.ref, pair: pair2, legs: [l12, l22], twoStep: true, expectedOutRaw: l22.expectedOutRaw, minOutRaw: floor2.minOut, impactPct: combine(l12.impactPct, l22.impactPct), slippageBps };
3080
- }
3081
- const pool = sa.pool, pair = pool.quote === zeroAddress2 ? NATIVE : await d.tokenRef(pool.quote);
3082
- const l1 = await bunnyLeg(d, pool, "sell", tin, pair, amountInRaw, slippageBps, 1);
3083
- if (isPair(pool, tout)) {
3084
- return { side: "sell", tin, tout, amountInRaw, pool, bunnyToken: tin, pair, legs: [l1], twoStep: false, expectedOutRaw: l1.expectedOutRaw, minOutRaw: l1.minOutRaw, impactPct: rnd2(l1.impactPct, 3), slippageBps };
3085
- }
3086
- const r = await d.dex(qOf(pair), qOf(tout), dec(l1.expectedOutRaw, pair)).catch((e2) => {
3087
- throw new Error(`${tin.symbol} sells only for ${pair.symbol} in BunnyPad's pool, and ${pair.symbol} cannot be swapped to ${tout.symbol} on the DEX (${String(e2?.message ?? e2).slice(0, 140)}). Sell it for ${pair.symbol} instead.`);
3088
- });
3089
- const l2 = dexLeg(r, slippageBps, 2);
3090
- const floor = await d.dex(qOf(pair), qOf(tout), dec(l1.minOutRaw, pair)).catch(() => null);
3091
- return {
3092
- side: "sell",
3093
- tin,
3094
- tout,
3095
- amountInRaw,
3096
- pool,
3097
- bunnyToken: tin,
3098
- pair,
3099
- legs: [l1, l2],
3100
- twoStep: true,
3101
- expectedOutRaw: l2.expectedOutRaw,
3102
- minOutRaw: floor ? minOut(floor.q.amountOutRaw, slippageBps) : l2.minOutRaw,
3103
- impactPct: combine(l1.impactPct, l2.impactPct),
3104
- slippageBps
3105
- };
3106
- }
3107
- var tokenOut2 = (t) => ({ symbol: safeSymbol(t.symbol), address: t.address, decimals: t.decimals });
3108
- var amt = (raw, t) => `${fmtAmount(raw, t.decimals, 8)} ${safeSymbol(t.symbol)}`;
3109
- function describePlan(plan) {
3110
- const bunnyLegs = plan.legs.filter((l) => l.venue === "bunnypad");
3111
- const fees = bunnyLegs.map((l) => ({ feePct: l.feePct, fee: l.feeRaw !== void 0 && l.feeAsset ? amt(l.feeRaw, l.feeAsset) : null, note: "1% launchpad fee plus the token's own buy/sell tax, taken from the quote-asset amount" }));
3112
- return {
3113
- venue: BUNNY_VENUE_NAME,
3114
- venueId: BUNNY_VENUE_ID,
3115
- network: "Elysium testnet",
3116
- kind: plan.twoStep ? "bunnypad-two-step" : "bunnypad",
3117
- twoStep: plan.twoStep,
3118
- launchpad: BUNNYPAD.launchpad,
3119
- tokenPage: bunnyTokenUrl(plan.bunnyToken.address),
3120
- tokenIn: tokenOut2(plan.tin),
3121
- tokenOut: tokenOut2(plan.tout),
3122
- pairAsset: { symbol: safeSymbol(plan.pair.symbol), address: plan.pair.address, note: plan.pair.address === "native" ? "This token's pool is paid in HYPE (buys send native HYPE)." : "This token's pool is paid in this ERC-20: a buy needs it in the wallet and an exact-amount approval to the launchpad." },
3123
- amountIn: fmtAmount(plan.amountInRaw, plan.tin.decimals, 8),
3124
- amountOut: fmtAmount(plan.expectedOutRaw, plan.tout.decimals, 8),
3125
- amountOutRaw: plan.expectedOutRaw.toString(),
3126
- minAmountOut: amt(plan.minOutRaw, plan.tout),
3127
- minAmountOutRaw: plan.minOutRaw.toString(),
3128
- slippageBps: plan.slippageBps,
3129
- priceImpactPct: plan.impactPct,
3130
- fees,
3131
- legs: plan.legs.map((l) => ({
3132
- step: l.step,
3133
- venue: l.venueName,
3134
- action: l.action,
3135
- tokenIn: safeSymbol(l.tokenIn.symbol),
3136
- tokenOut: safeSymbol(l.tokenOut.symbol),
3137
- amountIn: amt(l.amountInRaw, l.tokenIn),
3138
- expectedOut: amt(l.expectedOutRaw, l.tokenOut),
3139
- minOut: amt(l.minOutRaw, l.tokenOut),
3140
- priceImpactPct: rnd2(l.impactPct, 3),
3141
- ...l.feePct !== void 0 ? { feePct: l.feePct } : {}
3142
- })),
3143
- antiSnipeUntil: plan.pool.antiSnipeUntil * 1e3 > Date.now() ? new Date(plan.pool.antiSnipeUntil * 1e3).toISOString() : null,
3144
- warning: plan.impactPct === null ? "Price impact could not be computed." : plan.impactPct > 5 ? `Price impact ${plan.impactPct}% is above 5%: the pool is small.` : null,
3145
- note: plan.twoStep ? `${plan.side === "buy" ? plan.tout.symbol : plan.tin.symbol} trades only in BunnyPad's pool against ${plan.pair.symbol}, so this is TWO steps: ${plan.legs.map((l) => `${l.step}) ${l.venueName}: ${l.tokenIn.symbol} to ${l.tokenOut.symbol}`).join("; ")}. The second step uses the amount the first really delivered.` : "Single BunnyPad trade (BunnyLaunchpad.buy / sell). Exact figures from the contract's quoteBuy / quoteSell.",
3146
- executeWith: "agent_swap (Agent Wallet) or the trade box on the token page (main wallet)"
3147
- };
3148
- }
3149
- var isRouteProblem = (e2) => /No route between|Unknown token|No contract at this address/i.test(String(e2?.message ?? e2));
3150
- async function noRouteHint(err, tokenIn, tokenOut3, deps = {}) {
3151
- const error = String(err?.message ?? err).split("\n")[0].slice(0, 300);
3152
- let token = err instanceof BunnyPlanError ? err.token : null;
3153
- if (!token) for (const q of [tokenOut3, tokenIn]) {
3154
- token = await findBunnyToken(q, deps).catch(() => null);
3155
- if (token) break;
3156
- }
3157
- const suggestion = token ? `${token} is a BunnyPad launchpad token (it trades only in BunnyPad's own pool, not on the DEXs). ${/BunnyPad/.test(error) ? "" : "The BunnyPad route could not be built: "}${error} Call agent_swap or elysium_quote with this exact address; if the pair asset (see launchpad_token_detail) cannot be reached from the other token, ask the user to fund the wallet with the pair asset.` : "No DEX venue has a pool for this pair and no BunnyPad launchpad token matches. Check the symbol or paste the token address (launchpad_tokens searches BunnyPad launches by name or symbol, elysium_tokens lists DEX tokens). If it is a brand-new launch it may not be indexed yet.";
3158
- return { noRoute: true, error, bunnypadToken: token, suggestion };
3159
- }
3160
- async function quoteSwapAny(tokenIn, tokenOut3, amount, slippageBps = 100) {
3161
- let dexErr;
3162
- try {
3163
- return await quoteSwap(tokenIn, tokenOut3, amount);
3164
- } catch (e2) {
3165
- dexErr = e2;
3166
- }
3167
- if (!isRouteProblem(dexErr)) throw dexErr;
3168
- try {
3169
- const plan = await planBunnyRoute(tokenIn, tokenOut3, amount, slippageBps);
3170
- if (plan) {
3171
- return describePlan(plan);
3172
- }
3173
- } catch (e2) {
3174
- return noRouteHint(e2, tokenIn, tokenOut3);
3175
- }
3176
- return noRouteHint(dexErr, tokenIn, tokenOut3);
921
+ async function hlAccount(address, net = hlNet()) {
922
+ if (!isAddress2(address)) throw new Error("Invalid address");
923
+ const [acct, orders] = await Promise.all([getAccountState(net, address), getOpenOrders(net, address)]);
924
+ return { network: label(net), address, account: acct, openOrders: orders.slice(0, 50) };
3177
925
  }
3178
926
 
3179
927
  // src/tools.ts
3180
- var ticker = z.string().regex(/^[A-Za-z0-9]{1,12}$/);
928
+ var ticker = z.string().regex(/^([A-Za-z0-9]{1,12}:)?[A-Za-z0-9]{1,16}$/);
3181
929
  var evmAddr = z.string().regex(/^0x[a-fA-F0-9]{40}$/);
3182
- var chain = z.enum(["elysium-testnet", "hyperevm"]).default("elysium-testnet");
930
+ var chain = z.enum(["hyperevm", "elysium"]).default("hyperevm");
3183
931
  var addrOf = (a) => {
3184
932
  const addr = a.address ?? env.address;
3185
933
  if (!addr) throw new Error("Pass address or set ZYPHER_ADDRESS (a public 0x address; never a private key)");
3186
934
  if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) throw new Error("ZYPHER_ADDRESS / address must be a 0x + 40 hex public address");
3187
935
  return addr;
3188
936
  };
937
+ var execEnabled = false;
938
+ function setExecEnabled(v) {
939
+ execEnabled = v;
940
+ }
3189
941
  var tools = [
3190
942
  {
3191
943
  name: "get_policy",
3192
944
  description: "Show the fixed read-only policy of this server: network, what is allowed and what is not (no trading, no keys).",
3193
945
  shape: {},
3194
- handler: async () => ({
946
+ handler: async () => execEnabled ? {
947
+ mode: "read-only data tools + opt-in execution through the user's Agent Wallet API token",
948
+ network: env.network,
949
+ trading: true,
950
+ keys: false,
951
+ execution: true,
952
+ note: "The agent_* tools execute REAL trades and transfers from the user's Agent Wallet, immediately and without a confirmation step, inside the caps set by the user/operator in the Zypher Agent app. This server holds no private keys. Ask the human before every agent_* call. The token can be revoked in the app (Agent > Wallet > API tokens)."
953
+ } : {
3195
954
  mode: "read-only",
3196
955
  network: env.network,
3197
956
  trading: false,
3198
957
  keys: false,
3199
958
  execution: false,
959
+ chains: ["Hyperliquid mainnet (perp data)", "HyperEVM mainnet 999 (HyperSwap V3 quotes, balances, token checks)", "Elysium 99901 (community-run, served by Boxa; not the official Kinetiq mainnet): Boxa launch facts, balances, token checks"],
3200
960
  allowed: ["public market data", "public on-chain reads", "quotes (no transaction is built or sent)"],
3201
961
  note: "Trading, preparing orders and execution are not part of this package. Use the Zypher Agent app: https://zypheragent.com"
3202
- })
962
+ }
3203
963
  },
3204
964
  {
3205
965
  name: "analyze_ticker",
3206
- description: "Hyperliquid perp analysis: price, 24h volume, open interest, funding, orderbook depth.",
966
+ description: "Hyperliquid perp analysis for ANY listed market, crypto or trade.xyz stocks/indices/metals (e.g. BTC, xyz:NVDA): price, 24h volume, open interest, funding, orderbook depth.",
3207
967
  shape: { ticker },
3208
968
  handler: async (a) => getTicker(a.ticker)
3209
969
  },
@@ -3215,15 +975,28 @@ var tools = [
3215
975
  },
3216
976
  {
3217
977
  name: "get_new_pairs",
3218
- description: "Not available: listing new pairs needs an indexer that this package does not include. Always returns a not_available error.",
3219
- shape: { chain, limit: z.number().int().min(1).max(50).default(20) },
3220
- handler: async () => {
3221
- throw new Error("get_new_pairs is not available in @zypheragent/mcp: it needs a pair/pool event indexer.");
978
+ description: "Newest token launches on Elysium 99901 (all launchpads: Boxa, Kleos, Ascend, Montra, Volta ...), newest first, with price, FDV, holders and tradable flag. HyperEVM has no indexer here: chain=hyperevm returns an error.",
979
+ shape: { chain: z.enum(["elysium", "hyperevm"]).default("elysium"), limit: z.number().int().min(1).max(50).default(20), launchpad: z.string().regex(/^[a-z0-9]{2,20}$/).optional() },
980
+ handler: async (a) => {
981
+ if (a.chain !== "elysium") throw new Error("get_new_pairs only supports chain=elysium (HyperEVM would need a pool event indexer).");
982
+ return elysiumTokens({ sort: "newest", launchpad: a.launchpad, limit: a.limit });
3222
983
  }
3223
984
  },
985
+ {
986
+ name: "get_elysium_market",
987
+ description: "The Elysium 99901 market / launchpad feed like the app's Market and Launch tabs: sort newest | fdv (top) | volume (trending) | change (gainers) | trades | holders; filter by launchpad (boxa, kleos, ascend, montra, volta, elyspump, uniswap, stockium, obol, stocks, other); optional search q; tradable=true hides tokens the app cannot swap.",
988
+ shape: { sort: z.enum(ELY_SORTS).default("volume"), launchpad: z.string().regex(/^[a-z0-9]{2,20}$/).optional(), q: z.string().min(1).max(40).optional(), limit: z.number().int().min(1).max(50).default(20), priced: z.boolean().default(true), tradable: z.boolean().default(false) },
989
+ handler: async (a) => elysiumTokens({ sort: a.sort, launchpad: a.launchpad, q: a.q, limit: a.limit, priced: a.priced, tradable: a.tradable })
990
+ },
991
+ {
992
+ name: "get_elysium_token",
993
+ description: "One Elysium 99901 token by contract address: launchpad, pool, price, FDV, 24h change/volume, holders and whether the Zypher app can swap it.",
994
+ shape: { address: evmAddr },
995
+ handler: async (a) => elysiumToken(a.address)
996
+ },
3224
997
  {
3225
998
  name: "analyze_token_health",
3226
- description: "Check a token contract (mint/pause/blacklist/owner) and give a score with reasons. Not a safety guarantee.",
999
+ description: "Check a token contract (mint/pause/blacklist/owner) on HyperEVM mainnet (default) or Elysium 99901 and give a score with reasons. Bytecode heuristic, not a safety guarantee.",
3227
1000
  shape: { address: evmAddr, chain },
3228
1001
  handler: async (a) => tokenHealth(a.address, a.chain)
3229
1002
  },
@@ -3239,39 +1012,45 @@ var tools = [
3239
1012
  },
3240
1013
  {
3241
1014
  name: "get_portfolio",
3242
- description: "Public balances and positions for an address (argument or env ZYPHER_ADDRESS): Hyperliquid account and native HYPE on Elysium testnet.",
1015
+ description: "Public balances and positions for an address (argument or env ZYPHER_ADDRESS): Hyperliquid account, native HYPE on HyperEVM mainnet and on Elysium 99901.",
3243
1016
  shape: { address: evmAddr.optional() },
3244
1017
  handler: async (a) => {
3245
1018
  const addr = addrOf(a);
3246
- const [h, e2] = await Promise.allSettled([getAccount(addr), nativeBalance(addr)]);
1019
+ const [h, e2, y] = await Promise.allSettled([getAccount(addr), nativeBalance(addr, "hyperevm"), nativeBalance(addr, "elysium")]);
3247
1020
  const val = (r) => r.status === "fulfilled" ? r.value : { error: String(r.reason?.message ?? r.reason) };
3248
- return { address: addr, hyperliquid: val(h), elysium: val(e2) };
1021
+ return { address: addr, hyperliquid: val(h), hyperevm: val(e2), elysium: val(y) };
3249
1022
  }
3250
1023
  },
3251
1024
  {
3252
1025
  name: "get_market_tokens",
3253
- description: "Tokens with a live pool on a swappable Elysium testnet venue: prices (HYPE/USDC), liquidity, pair. Not every token on the chain.",
1026
+ description: "HyperEVM mainnet tokens (HyperSwap V3, verified list): USD price from the deepest sane pool, TVL, pool health. No 24h change. Not every token on the chain.",
3254
1027
  shape: {},
3255
1028
  handler: async () => marketTokens()
3256
1029
  },
3257
1030
  {
3258
1031
  name: "quote_swap",
3259
- description: "Quote an exact-in swap on the Elysium testnet venues (token = HYPE, an address, or a market symbol): best venue, output, route, price impact. Does not trade.",
1032
+ description: "Quote an exact-in swap on HyperSwap V3 (HyperEVM mainnet; token = HYPE, an address, or a listed symbol): output, route, price impact. Sane pools only. Quote only, builds no transaction.",
3260
1033
  shape: { tokenIn: z.string().min(1).max(60), tokenOut: z.string().min(1).max(60), amount: z.string().regex(/^\d+(\.\d+)?$/) },
3261
- handler: async (a) => quoteSwapAny(a.tokenIn, a.tokenOut, a.amount)
1034
+ handler: async (a) => quoteSwap(a.tokenIn, a.tokenOut, a.amount)
3262
1035
  },
3263
1036
  {
3264
1037
  name: "get_perp_markets",
3265
- description: "Hyperliquid perp markets (testnet by default, ZYPHER_NETWORK=mainnet) with price, 24h change, volume, open interest, funding, max leverage.",
1038
+ description: "Hyperliquid perp markets (all of them: crypto + trade.xyz stocks, indices and metals as xyz:NVDA) with price, 24h change, volume, open interest, funding, max leverage.",
3266
1039
  shape: { limit: z.number().int().min(1).max(50).default(15), sortBy: z.enum(SORT_KEYS).default("volume") },
3267
1040
  handler: async (a) => perpMarkets(a.limit, a.sortBy)
3268
1041
  },
3269
1042
  {
3270
1043
  name: "get_candles",
3271
- description: "Hyperliquid candles (testnet by default): summary statistics plus the last 20 candles.",
1044
+ description: "Hyperliquid candles (mainnet by default): summary statistics plus the last 20 candles.",
3272
1045
  shape: { ticker, interval: z.enum(CANDLE_INTERVALS).default("1h"), count: z.number().int().min(5).max(500).default(100) },
3273
1046
  handler: async (a) => perpCandles(a.ticker, a.interval, a.count)
3274
1047
  },
1048
+ {
1049
+ name: "get_launch_info",
1050
+ description: "Read-only facts about token launches on Elysium 99901 through Boxa's V3 launcher: chain check, contract code-hash pins, pool fee, treasury share, supply. Read-only (launching is the agent_elysium_launch tool, which needs ZYPHER_API_TOKEN). Elysium 99901 is community-run, not the official Kinetiq mainnet.",
1051
+ shape: {},
1052
+ handler: async () => boxaLaunchInfo()
1053
+ },
3275
1054
  {
3276
1055
  name: "get_hl_account",
3277
1056
  description: "Hyperliquid account state for a public address (argument or env ZYPHER_ADDRESS): value, margin, positions, open orders. Read-only.",
@@ -3288,11 +1067,220 @@ async function runTool(t, args) {
3288
1067
  }
3289
1068
  }
3290
1069
 
1070
+ // src/exec.ts
1071
+ import { randomUUID } from "node:crypto";
1072
+ import { z as z2 } from "zod";
1073
+ var TOKEN_RE = /^zat_[0-9a-f]{40}$/;
1074
+ var DEFAULT_API_URL = "https://zypheragent.com/agent";
1075
+ var MAX_RESPONSE_BYTES = 100 * 1024;
1076
+ var TIMEOUT_MS = 15e3;
1077
+ var SWAP_TIMEOUT_MS = 45e3;
1078
+ var DEFAULT_HOST = "zypheragent.com";
1079
+ var ALLOWED_PATHS = ["/api/agent-api/status", "/api/agent-api/swap", "/api/agent-api/elysium-swap", "/api/agent-api/elysium-launch", "/api/agent-api/bridge-boxa", "/api/agent-api/perp-order", "/api/agent-api/close", "/api/agent-api/send", "/api/agent-api/transfer-out"];
1080
+ function loadExecConfig(e2 = process.env) {
1081
+ const token = e2.ZYPHER_API_TOKEN?.trim();
1082
+ if (!token) return { enabled: false };
1083
+ if (!TOKEN_RE.test(token)) return { enabled: false, reason: "ZYPHER_API_TOKEN is set but is not a valid Zypher API token (expected zat_ followed by 40 lowercase hex characters). Execution tools stay disabled." };
1084
+ const raw = e2.ZYPHER_API_URL?.trim() || DEFAULT_API_URL;
1085
+ let u;
1086
+ try {
1087
+ u = new URL(raw);
1088
+ } catch {
1089
+ return { enabled: false, reason: "ZYPHER_API_URL is not a valid URL. Execution tools stay disabled." };
1090
+ }
1091
+ const local = u.hostname === "localhost" || u.hostname === "127.0.0.1";
1092
+ if (u.protocol !== "https:" && !(u.protocol === "http:" && local)) return { enabled: false, reason: "ZYPHER_API_URL must use https (plain http is only accepted for localhost / 127.0.0.1). Execution tools stay disabled." };
1093
+ if (u.username || u.password || u.search || u.hash) return { enabled: false, reason: "ZYPHER_API_URL must not contain credentials, a query string or a fragment. Execution tools stay disabled." };
1094
+ if (u.hostname !== DEFAULT_HOST && !local && e2.ZYPHER_ALLOW_CUSTOM_HOST !== "1") return { enabled: false, reason: `ZYPHER_API_URL points to ${u.host}, not ${DEFAULT_HOST}. Your API token would be sent there, so execution tools stay disabled. Set ZYPHER_ALLOW_CUSTOM_HOST=1 only if you run your own Zypher server.` };
1095
+ const warning = u.hostname === DEFAULT_HOST ? void 0 : `WARNING: ZYPHER_API_URL points to ${u.host}, not ${DEFAULT_HOST}. Your API token will be sent to that host.`;
1096
+ return { enabled: true, token, base: u.origin + u.pathname.replace(/\/+$/, ""), warning };
1097
+ }
1098
+ var decimalStr = z2.string().regex(/^\d{1,20}(\.\d{1,18})?$/, "must be a plain positive decimal like 12.5");
1099
+ var amount = z2.union([decimalStr, z2.number()]).transform((v, ctx) => {
1100
+ const n3 = typeof v === "number" ? v : Number(v);
1101
+ const s = typeof v === "number" ? Number.isFinite(v) && !/e/i.test(String(v)) ? String(v) : "" : v;
1102
+ if (!s || !Number.isFinite(n3) || n3 <= 0 || n3 > 1e12) {
1103
+ ctx.addIssue({ code: "custom", message: "amount must be a plain decimal number > 0 and <= 1e12" });
1104
+ return z2.NEVER;
1105
+ }
1106
+ return s;
1107
+ });
1108
+ var usd = (max) => z2.union([z2.string().regex(/^\d{1,12}(\.\d{1,8})?$/), z2.number()]).transform((v, ctx) => {
1109
+ const n3 = Number(v);
1110
+ if (!Number.isFinite(n3) || n3 <= 0 || n3 > max) {
1111
+ ctx.addIssue({ code: "custom", message: `must be a number > 0 and <= ${max}` });
1112
+ return z2.NEVER;
1113
+ }
1114
+ return n3;
1115
+ });
1116
+ var tokenRef = z2.union([ticker, evmAddr]);
1117
+ var httpsUrl = z2.string().max(400).regex(/^https:\/\/\S+$/, "must be an https:// URL");
1118
+ var slippageBps = z2.number().int().min(1).max(5e3).optional();
1119
+ var HONEST = "REAL FUNDS, MAINNET: this executes IMMEDIATELY from the user's Agent Wallet, inside the caps set by the user/operator in the Zypher Agent app. There is NO confirmation step and this MCP never holds keys. Ask the human for explicit approval of the exact parameters BEFORE calling.";
1120
+ var SPECS = [
1121
+ {
1122
+ name: "agent_status",
1123
+ method: "GET",
1124
+ path: "/api/agent-api/status",
1125
+ shape: {},
1126
+ description: "Status of the user's Agent Wallet through the Zypher Agent API: wallet address, balances, positions and the caps/limits that apply. Read only; does not move funds."
1127
+ },
1128
+ {
1129
+ name: "agent_swap",
1130
+ method: "POST",
1131
+ path: "/api/agent-api/swap",
1132
+ swap: true,
1133
+ shape: { tokenIn: tokenRef, tokenOut: tokenRef, amountIn: amount, slippageBps },
1134
+ description: `Swap tokens (exact-in) on HyperEVM from the user's Agent Wallet. ${HONEST}`
1135
+ },
1136
+ {
1137
+ name: "agent_elysium_swap",
1138
+ method: "POST",
1139
+ path: "/api/agent-api/elysium-swap",
1140
+ swap: true,
1141
+ shape: { tokenIn: tokenRef, tokenOut: tokenRef, amountIn: amount, slippageBps },
1142
+ description: `Swap on ELYSIUM 99901 (community-run chain by Boxa) from the user's Agent Wallet: native HYPE <-> a token listed in the Boxa index (use HYPE on one side; token by symbol or 0x address, prefer the 0x address from get_elysium_market). Signed server-side, delivered through the HyperEVM Inbox, so it can take a minute. ${HONEST}`
1143
+ },
1144
+ {
1145
+ name: "agent_elysium_launch",
1146
+ method: "POST",
1147
+ path: "/api/agent-api/elysium-launch",
1148
+ swap: true,
1149
+ shape: {
1150
+ name: z2.string().min(1).max(32),
1151
+ symbol: z2.string().regex(/^[A-Za-z0-9][A-Za-z0-9._$-]{0,9}$/),
1152
+ description: z2.string().max(280).optional(),
1153
+ image: httpsUrl.optional(),
1154
+ website: httpsUrl.optional(),
1155
+ twitter: httpsUrl.optional(),
1156
+ telegram: httpsUrl.optional(),
1157
+ allocation: z2.union([z2.enum(["balanced", "creator", "holders", "buyback", "liquidity"]), z2.object({ creatorBps: z2.number().int().min(0).max(1e4), holdersBps: z2.number().int().min(0).max(1e4), buybackBps: z2.number().int().min(0).max(1e4), liquidityBps: z2.number().int().min(0).max(1e4) }).strict()]).optional(),
1158
+ antiSnipe: z2.boolean().optional(),
1159
+ firstBuyHype: amount.optional()
1160
+ },
1161
+ description: `LAUNCH a new token on ELYSIUM 99901 through Boxa's V3 launcher (1B supply, all in locked liquidity, 1% pool fee, 30% of fees to the Boxa treasury; the rest by 'allocation': balanced = 25% each to creator / holders / buyback-burn / liquidity, or four basis-point shares adding up to 10000). No creation fee; the only HYPE spent is the optional firstBuyHype (anti-snipe ON caps the first buy at about 0.6 HYPE) plus gas, paid from the Agent Wallet's HYPE ON ELYSIUM (and a little on HyperEVM for delivery). image/website/twitter/telegram must be https URLs. The token is created at once and cannot be deleted. ${HONEST}`
1162
+ },
1163
+ {
1164
+ name: "agent_bridge_elysium",
1165
+ method: "POST",
1166
+ path: "/api/agent-api/bridge-boxa",
1167
+ swap: true,
1168
+ shape: { direction: z2.enum(["deposit", "withdraw"]), amount },
1169
+ description: `Move native HYPE between HyperEVM and ELYSIUM 99901 for the user's Agent Wallet (Boxa bridge): deposit = HyperEVM -> Elysium (needed before agent_elysium_swap / agent_elysium_launch), withdraw = Elysium -> HyperEVM (can take a while). ${HONEST}`
1170
+ },
1171
+ {
1172
+ name: "agent_perp_order",
1173
+ method: "POST",
1174
+ path: "/api/agent-api/perp-order",
1175
+ shape: { coin: ticker, side: z2.enum(["long", "short"]), notionalUsd: usd(1e7), leverage: z2.number().int().min(1).max(100), tpPrice: usd(1e9).optional(), slPrice: usd(1e9).optional(), reduceOnly: z2.boolean().optional() },
1176
+ description: `Open or add to a Hyperliquid perp position (coin like BTC or xyz:NVDA; side long|short; notionalUsd; leverage; optional tpPrice/slPrice) from the user's Agent Wallet. ${HONEST}`
1177
+ },
1178
+ {
1179
+ name: "agent_close",
1180
+ method: "POST",
1181
+ path: "/api/agent-api/close",
1182
+ shape: { coin: ticker, fractionPct: z2.number().gt(0).max(100).optional() },
1183
+ description: `Close a Hyperliquid perp position (all of it, or fractionPct percent of it) from the user's Agent Wallet. ${HONEST}`
1184
+ },
1185
+ {
1186
+ name: "agent_send",
1187
+ method: "POST",
1188
+ path: "/api/agent-api/send",
1189
+ shape: { asset: tokenRef, amount, to: evmAddr },
1190
+ description: `SEND funds from the user's Agent Wallet to an external 0x address (irreversible on-chain transfer). ${HONEST} Double-check the recipient address with the human.`
1191
+ },
1192
+ {
1193
+ name: "agent_transfer_out",
1194
+ method: "POST",
1195
+ path: "/api/agent-api/transfer-out",
1196
+ shape: { asset: tokenRef, amount },
1197
+ description: `Move funds out of the user's Agent Wallet back to the user's own main wallet (the owner address registered in the app). ${HONEST}`
1198
+ }
1199
+ ];
1200
+ function scrub(s, token) {
1201
+ let out = s.split(token).join("[redacted]");
1202
+ out = out.replace(/zat_[0-9a-fA-F]{8,}/g, "[redacted]").replace(/Bearer\s+\S+/gi, "Bearer [redacted]");
1203
+ return out;
1204
+ }
1205
+ async function readCapped(res) {
1206
+ const len = Number(res.headers.get("content-length"));
1207
+ if (Number.isFinite(len) && len > MAX_RESPONSE_BYTES) {
1208
+ await res.body?.cancel().catch(() => {
1209
+ });
1210
+ throw new Error(`Response from the Zypher API is larger than ${MAX_RESPONSE_BYTES / 1024} KB; refused.`);
1211
+ }
1212
+ if (!res.body) return "";
1213
+ const reader = res.body.getReader();
1214
+ const chunks = [];
1215
+ let total = 0;
1216
+ for (; ; ) {
1217
+ const { done, value } = await reader.read();
1218
+ if (done) break;
1219
+ total += value.byteLength;
1220
+ if (total > MAX_RESPONSE_BYTES) {
1221
+ await reader.cancel().catch(() => {
1222
+ });
1223
+ throw new Error(`Response from the Zypher API is larger than ${MAX_RESPONSE_BYTES / 1024} KB; refused.`);
1224
+ }
1225
+ chunks.push(value);
1226
+ }
1227
+ return Buffer.concat(chunks).toString("utf8");
1228
+ }
1229
+ var UNKNOWN_OUTCOME = " The request may or may not have been executed: call agent_status to check before retrying, and do not repeat it blindly.";
1230
+ async function call(cfg, spec, body) {
1231
+ if (!ALLOWED_PATHS.includes(spec.path)) throw new Error("internal error: path not allowed");
1232
+ const headers = { authorization: `Bearer ${cfg.token}`, accept: "application/json" };
1233
+ const init = { method: spec.method, headers, redirect: "error" };
1234
+ if (spec.method === "POST") {
1235
+ headers["content-type"] = "application/json";
1236
+ headers["idempotency-key"] = randomUUID();
1237
+ init.body = JSON.stringify(body ?? {});
1238
+ }
1239
+ const ms = spec.swap ? cfg.swapTimeoutMs ?? SWAP_TIMEOUT_MS : cfg.timeoutMs ?? TIMEOUT_MS;
1240
+ init.signal = AbortSignal.timeout(ms);
1241
+ const post = spec.method === "POST";
1242
+ let res, text;
1243
+ try {
1244
+ res = await fetch(cfg.base + spec.path, init);
1245
+ text = await readCapped(res);
1246
+ } catch (e2) {
1247
+ const name = e2?.name, msg = String(e2?.message ?? e2);
1248
+ if (name === "TimeoutError" || name === "AbortError") throw new Error(`Zypher API did not answer within ${ms / 1e3}s.` + (post ? UNKNOWN_OUTCOME : ""));
1249
+ if (/larger than/.test(msg)) throw new Error(scrub(msg, cfg.token));
1250
+ if (/redirect/i.test(msg) || /redirect/i.test(String(e2?.cause?.message ?? ""))) throw new Error("The Zypher API answered with a redirect; redirects are refused for safety. Check ZYPHER_API_URL.");
1251
+ throw new Error(scrub(`Could not reach the Zypher API (${String(e2?.cause?.code ?? e2?.cause?.message ?? msg)}).`, cfg.token) + (post ? UNKNOWN_OUTCOME : ""));
1252
+ }
1253
+ let json;
1254
+ try {
1255
+ json = text ? JSON.parse(text) : void 0;
1256
+ } catch {
1257
+ json = void 0;
1258
+ }
1259
+ if (!res.ok || !json || json.ok === false) {
1260
+ const apiMsg = typeof json?.error === "string" ? scrub(json.error, cfg.token).slice(0, 500) : "";
1261
+ const code = typeof json?.code === "string" ? scrub(json.code, cfg.token).slice(0, 60) : "";
1262
+ let hint = "";
1263
+ if (res.status === 401 || res.status === 403) hint = " The API token is invalid, revoked or not allowed for this action: create a new one in the app (Agent > Wallet > API tokens).";
1264
+ else if (res.status === 429) hint = " Rate limited: wait before retrying.";
1265
+ else if (res.status >= 500) hint = post ? UNKNOWN_OUTCOME : "";
1266
+ throw new Error(`Zypher API refused the request (HTTP ${res.status}${code ? `, code ${code}` : ""})${apiMsg ? `: ${apiMsg}` : ""}.${hint}`);
1267
+ }
1268
+ return JSON.parse(scrub(JSON.stringify(json), cfg.token));
1269
+ }
1270
+ function execTools(cfg) {
1271
+ return SPECS.map((s) => ({
1272
+ name: s.name,
1273
+ description: s.description,
1274
+ shape: s.shape,
1275
+ handler: (a) => call(cfg, s, s.method === "GET" ? void 0 : Object.fromEntries(Object.entries(a).filter(([, v]) => v !== void 0)))
1276
+ }));
1277
+ }
1278
+
3291
1279
  // package.json
3292
1280
  var package_default = {
3293
1281
  name: "@zypheragent/mcp",
3294
- version: "0.1.0",
3295
- description: "Read-only MCP server of Zypher Agent: Hyperliquid perp analytics and Elysium testnet market reads for AI assistants. No trading, no keys.",
1282
+ version: "0.4.0",
1283
+ description: "MCP server of Zypher Agent: Hyperliquid perp analytics, HyperSwap quotes on HyperEVM and Elysium market and launchpad feed for AI assistants. Read-only by default; optional execution (swaps, Elysium swap and token launch, bridge, perp orders, close, send) only through a user-created Agent Wallet API token. This package holds no keys.",
3296
1284
  license: "MIT",
3297
1285
  author: "Zypher Agent",
3298
1286
  homepage: "https://zypheragent.com",
@@ -3302,14 +1290,34 @@ var package_default = {
3302
1290
  "zypheragent-mcp": "dist/index.js"
3303
1291
  },
3304
1292
  main: "dist/index.js",
3305
- files: ["dist", "README.md", "LICENSE"],
3306
- engines: { node: ">=20" },
3307
- keywords: ["mcp", "model-context-protocol", "zypher", "zypher-agent", "hyperliquid", "elysium", "hyperevm", "read-only", "crypto", "market-data"],
3308
- publishConfig: { access: "public" },
1293
+ files: [
1294
+ "dist",
1295
+ "README.md",
1296
+ "LICENSE"
1297
+ ],
1298
+ engines: {
1299
+ node: ">=20"
1300
+ },
1301
+ keywords: [
1302
+ "mcp",
1303
+ "model-context-protocol",
1304
+ "zypher",
1305
+ "zypher-agent",
1306
+ "hyperliquid",
1307
+ "elysium",
1308
+ "hyperevm",
1309
+ "read-only",
1310
+ "agent-wallet",
1311
+ "crypto",
1312
+ "market-data"
1313
+ ],
1314
+ publishConfig: {
1315
+ access: "public"
1316
+ },
3309
1317
  scripts: {
3310
1318
  build: "node scripts/build.mjs",
3311
1319
  prepack: "node scripts/build.mjs",
3312
- test: "node scripts/test.mjs",
1320
+ test: "node scripts/test.mjs && node scripts/test-exec.mjs",
3313
1321
  "test:net": "node scripts/test-net.mjs"
3314
1322
  },
3315
1323
  devDependencies: {
@@ -3330,12 +1338,17 @@ globalThis.fetch = (input, init = {}) => {
3330
1338
  if (!h.has("user-agent")) h.set("user-agent", UA);
3331
1339
  return realFetch(input, { ...init, headers: h });
3332
1340
  };
1341
+ var execCfg = loadExecConfig();
1342
+ if (!execCfg.enabled && execCfg.reason) console.error(`zypher-mcp: ${execCfg.reason}`);
1343
+ if (execCfg.enabled && execCfg.warning) console.error(`zypher-mcp: ${execCfg.warning}`);
1344
+ var tools2 = execCfg.enabled ? [...tools, ...execTools(execCfg)] : tools;
1345
+ setExecEnabled(execCfg.enabled);
3333
1346
  var server = new McpServer({ name: "zypher-agent", version: package_default.version });
3334
- for (const t of tools) {
1347
+ for (const t of tools2) {
3335
1348
  server.tool(t.name, t.description, t.shape, async (args) => {
3336
1349
  const res = await runTool(t, args);
3337
1350
  return { content: [{ type: "text", text: JSON.stringify(res, null, 2) }], isError: !res.ok };
3338
1351
  });
3339
1352
  }
3340
1353
  await server.connect(new StdioServerTransport());
3341
- console.error(`zypher-mcp ${package_default.version} ready (stdio, read-only, ${tools.length} tools, hyperliquid ${env.network})`);
1354
+ console.error(`zypher-mcp ${package_default.version} ready (stdio, ${execCfg.enabled ? `read + EXECUTION via Agent Wallet API token (${new URL(execCfg.base).host})` : "read-only"}, ${tools2.length} tools, hyperliquid ${env.network}, HyperEVM 999 + Elysium 99901 reads)`);