@parabolicfamily/mcp 0.1.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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -0
  3. package/abi/ParabolicBondingCurve.json +1223 -0
  4. package/abi/ParabolicLaunchFactory.json +2212 -0
  5. package/abi/ParabolicLauncherToken.json +577 -0
  6. package/abi/ParabolicMemoRouter.json +950 -0
  7. package/dist/abi.d.ts +17 -0
  8. package/dist/abi.js +37 -0
  9. package/dist/abi.js.map +1 -0
  10. package/dist/chain.d.ts +32 -0
  11. package/dist/chain.js +48 -0
  12. package/dist/chain.js.map +1 -0
  13. package/dist/coins.d.ts +52 -0
  14. package/dist/coins.js +75 -0
  15. package/dist/coins.js.map +1 -0
  16. package/dist/config.d.ts +55 -0
  17. package/dist/config.js +86 -0
  18. package/dist/config.js.map +1 -0
  19. package/dist/curve.d.ts +92 -0
  20. package/dist/curve.js +127 -0
  21. package/dist/curve.js.map +1 -0
  22. package/dist/docs.d.ts +3 -0
  23. package/dist/docs.js +63 -0
  24. package/dist/docs.js.map +1 -0
  25. package/dist/format.d.ts +19 -0
  26. package/dist/format.js +30 -0
  27. package/dist/format.js.map +1 -0
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.js +15 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/parabolic.d.ts +780 -0
  32. package/dist/parabolic.js +710 -0
  33. package/dist/parabolic.js.map +1 -0
  34. package/dist/server.d.ts +32 -0
  35. package/dist/server.js +149 -0
  36. package/dist/server.js.map +1 -0
  37. package/dist/subgraph.d.ts +85 -0
  38. package/dist/subgraph.js +43 -0
  39. package/dist/subgraph.js.map +1 -0
  40. package/dist/tx.d.ts +115 -0
  41. package/dist/tx.js +105 -0
  42. package/dist/tx.js.map +1 -0
  43. package/package.json +64 -0
  44. package/src/abi.ts +41 -0
  45. package/src/chain.ts +60 -0
  46. package/src/coins.ts +118 -0
  47. package/src/config.ts +123 -0
  48. package/src/curve.ts +171 -0
  49. package/src/docs.ts +63 -0
  50. package/src/format.ts +34 -0
  51. package/src/index.ts +14 -0
  52. package/src/parabolic.ts +753 -0
  53. package/src/server.ts +229 -0
  54. package/src/subgraph.ts +61 -0
  55. package/src/tx.ts +146 -0
package/src/server.ts ADDED
@@ -0,0 +1,229 @@
1
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import type { Rpc } from "./chain.js";
4
+ import { SORT_KEYS } from "./coins.js";
5
+ import { configFromEnv, type Config } from "./config.js";
6
+ import { jsonText, toPlain } from "./format.js";
7
+ import { Parabolic } from "./parabolic.js";
8
+ import type { Fetch } from "./subgraph.js";
9
+ import { readFileSync } from "node:fs";
10
+
11
+ export { Parabolic } from "./parabolic.js";
12
+ export { configFromEnv, type Config } from "./config.js";
13
+ export type { Rpc } from "./chain.js";
14
+
15
+ // Read from package.json so the version an MCP client sees can never drift from the published one.
16
+ const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as { version: string };
17
+ export const SERVER_INFO = { name: "parabolic", version } as const;
18
+
19
+ export const TOOL_NAMES = [
20
+ "parabolic.list_coins",
21
+ "parabolic.get_coin",
22
+ "parabolic.quote_buy",
23
+ "parabolic.quote_sell",
24
+ "parabolic.build_buy_tx",
25
+ "parabolic.build_sell_tx",
26
+ "parabolic.build_launch_tx",
27
+ "parabolic.protocol_stats",
28
+ "parabolic.docs",
29
+ ] as const;
30
+
31
+ export type ServerOptions = {
32
+ /** Overrides applied on top of the environment-derived config. */
33
+ config?: Partial<Config>;
34
+ /** Environment to read (defaults to process.env); pass {} in tests to ignore the real environment. */
35
+ env?: Record<string, string | undefined>;
36
+ /** RPC implementation (tests inject an in-memory one). */
37
+ rpc?: Rpc;
38
+ /** fetch used for the subgraph (tests inject a fake). */
39
+ fetch?: Fetch;
40
+ };
41
+
42
+ /**
43
+ * Tool errors are read by an agent and often pasted into a transcript, so they must not carry the RPC
44
+ * endpoint. viem embeds the full request URL and body in HttpRequestError.message, and a user who set
45
+ * PARABOLIC_RPC_URL to a keyed endpoint would have that key handed back by any failing call. Our own
46
+ * thrown messages pass through unchanged; anything carrying a URL is reduced to its first line with
47
+ * the endpoint redacted.
48
+ */
49
+ export function safeMessage(e: unknown): string {
50
+ const raw = e instanceof Error ? e.message : String(e);
51
+ if (!/https?:\/\//.test(raw)) return raw;
52
+ const firstLine = raw.split("\n")[0].trim();
53
+ return `${firstLine.replace(/https?:\/\/[^\s"']+/g, "<rpc endpoint>")} (endpoint redacted; check the server log)`;
54
+ }
55
+
56
+ type ToolResult = { content: { type: "text"; text: string }[]; structuredContent?: Record<string, unknown>; isError?: boolean };
57
+
58
+ async function run(fn: () => Promise<unknown>): Promise<ToolResult> {
59
+ try {
60
+ const plain = toPlain(await fn());
61
+ return { content: [{ type: "text", text: jsonText(plain) }], structuredContent: plain };
62
+ } catch (e) {
63
+ return { isError: true, content: [{ type: "text", text: `Error: ${safeMessage(e)}` }] };
64
+ }
65
+ }
66
+
67
+ const INSTRUCTIONS =
68
+ "Parabolic is a dollar-native token launchpad on Arc (bonding curves quoted in native USDC, graduation into a locked Uniswap v4 pool). " +
69
+ "Use list_coins / get_coin to discover coins, quote_buy / quote_sell for exact curve math read from chain, and build_*_tx for unsigned transactions " +
70
+ "({ to, data, value, chainId }) that the user's own wallet signs. This server never holds keys, never signs and never broadcasts. " +
71
+ "Amounts are whole units (\"25\" = 25 USDC); results carry both whole-unit and raw values. Read parabolic://docs/parameters for the adopted parameters and Arc facts.";
72
+
73
+ /** Builds the MCP server without connecting a transport, so it can be served over stdio (src/index.ts) or HTTP. */
74
+ export function createServer(opts: ServerOptions = {}): McpServer {
75
+ const config: Config = { ...configFromEnv(opts.env ?? process.env), ...opts.config };
76
+ const api = new Parabolic(config, { rpc: opts.rpc, fetch: opts.fetch });
77
+ const server = new McpServer(SERVER_INFO, { instructions: INSTRUCTIONS });
78
+
79
+ const address = z.string().describe("Token address (0x…). The curve address is accepted too.");
80
+ const whole = z.union([z.string(), z.number()]);
81
+ const slippageBps = z.number().int().min(0).max(9999).optional().describe("Slippage tolerance in basis points used to derive the minimum output (default 100 = 1%)");
82
+ const buyer = z.string().optional().describe("Wallet that will send the buy; when given, its snipe-tax exemption is checked on the curve");
83
+ const readOnly = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } as const;
84
+
85
+ server.registerTool(
86
+ "parabolic.list_coins",
87
+ {
88
+ title: "List Parabolic coins",
89
+ description: "Coins on Parabolic with status, raised / threshold / progress, price and market cap. From the subgraph when PARABOLIC_SUBGRAPH_URL is set (adds holders, 24h volume, 24h change), otherwise from a TokenLaunched log scan of the factory (newest 50).",
90
+ inputSchema: {
91
+ status: z.enum(["climbing", "graduated"]).optional().describe("climbing = still on the bonding curve; graduated = in its Uniswap v4 pool"),
92
+ sort: z.enum(SORT_KEYS).optional().describe("trending (24h volume, then last trade), new (launch time), near_graduation (curve progress, climbing first), market_cap"),
93
+ limit: z.number().int().min(1).max(100).optional().describe("Max coins to return (default 20)"),
94
+ },
95
+ annotations: readOnly,
96
+ },
97
+ (args) => run(() => api.listCoins(args)),
98
+ );
99
+
100
+ server.registerTool(
101
+ "parabolic.get_coin",
102
+ {
103
+ title: "Get a Parabolic coin",
104
+ description: "Everything about one coin, read from the factory, curve and token (plus the subgraph when configured): name, ticker, creator, curve, status/phase, raised/threshold/progress, price, market cap, holders, pool id, fee terms and split, snipe-tax window and the current snipe-tax bps.",
105
+ inputSchema: { address },
106
+ annotations: readOnly,
107
+ },
108
+ (args) => run(() => api.getCoin(args.address)),
109
+ );
110
+
111
+ server.registerTool(
112
+ "parabolic.quote_buy",
113
+ {
114
+ title: "Quote a curve buy",
115
+ description: "Tokens received for `quoteIn` USDC (or EURC) using the curve's exact math on live reserves: fee, creator tax and snipe tax breakdown, spot vs effective price, price impact, whether the buy graduates the coin, and a suggested minTokensOut.",
116
+ inputSchema: { address, quoteIn: whole.describe("Quote to spend, whole units (e.g. \"50\" = 50 USDC)"), buyer, slippageBps },
117
+ annotations: readOnly,
118
+ },
119
+ (args) => run(() => api.quoteBuy(args)),
120
+ );
121
+
122
+ server.registerTool(
123
+ "parabolic.quote_sell",
124
+ {
125
+ title: "Quote a curve sell",
126
+ description: "Quote received for `tokensIn` coins using the curve's exact math on live reserves: gross, fee and creator tax, effective price, price impact and a suggested minQuoteOut. Sells carry no snipe tax.",
127
+ inputSchema: { address, tokensIn: whole.describe("Coins to sell, whole tokens (e.g. \"1000000\")"), slippageBps },
128
+ annotations: readOnly,
129
+ },
130
+ (args) => run(() => api.quoteSell(args)),
131
+ );
132
+
133
+ server.registerTool(
134
+ "parabolic.build_buy_tx",
135
+ {
136
+ title: "Build an unsigned buy",
137
+ description: "Unsigned `curve.buy(quoteIn, minTokensOut, recipient)` as { to, data, value, chainId } for the caller's wallet to sign. Native-USDC curves take the quote as value; EURC curves return an approval step too. Pass `memo` for the memo-routed variant (referral code in an Arc memo; EOA-only).",
138
+ inputSchema: {
139
+ address,
140
+ quoteIn: whole.describe("Quote to spend, whole units"),
141
+ minTokensOut: whole.optional().describe("Minimum coins to receive, whole tokens; derived from the live quote and slippageBps when omitted"),
142
+ recipient: z.string().describe("Address that receives the coins (normally the signer)"),
143
+ buyer,
144
+ slippageBps,
145
+ memo: z.object({ referral: z.string().optional().describe("Referrer's 16-hex referral code or wallet address"), note: z.string().max(280).optional() }).optional().describe("Route through ParabolicMemoRouter via Arc's Memo contract (EOA-only, needs PARABOLIC_MEMO_ROUTER)"),
146
+ },
147
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
148
+ },
149
+ (args) => run(() => api.buildBuyTx(args)),
150
+ );
151
+
152
+ server.registerTool(
153
+ "parabolic.build_sell_tx",
154
+ {
155
+ title: "Build an unsigned sell",
156
+ description: "Unsigned ERC-20 approval plus `curve.sell(tokensIn, minQuoteOut, recipient)` as { to, data, value, chainId } for the caller's wallet to sign.",
157
+ inputSchema: {
158
+ address,
159
+ tokensIn: whole.describe("Coins to sell, whole tokens"),
160
+ minQuoteOut: whole.optional().describe("Minimum quote to receive, whole units; derived from the live quote and slippageBps when omitted"),
161
+ recipient: z.string().describe("Address that receives the quote (normally the signer)"),
162
+ slippageBps,
163
+ },
164
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
165
+ },
166
+ (args) => run(() => api.buildSellTx(args)),
167
+ );
168
+
169
+ server.registerTool(
170
+ "parabolic.build_launch_tx",
171
+ {
172
+ title: "Build an unsigned launch",
173
+ description: "Unsigned `factory.launchToken(TokenParams, launchConfigId, pairToken)` with the launch fee as value, using the same TokenParams shape as the web Create form (expectedEconomics pinned from the factory, random salt). Optional devBuy returns the follow-up buy calldata for the creator to send once the curve address is known.",
174
+ inputSchema: {
175
+ name: z.string().min(1).max(64),
176
+ ticker: z.string().min(1).max(16).describe("Symbol, letters and digits"),
177
+ image: z.string().max(512).optional().describe("https:// or ipfs:// pointer to a square image"),
178
+ description: z.string().max(2048).optional(),
179
+ socials: z.object({ twitter: z.string().max(256).optional(), telegram: z.string().max(256).optional(), discord: z.string().max(256).optional(), website: z.string().max(256).optional(), farcaster: z.string().max(256).optional() }).optional(),
180
+ pair: z.enum(["USDC", "EURC"]).optional().describe("Quote asset (default USDC = native)"),
181
+ devBuy: whole.optional().describe("Creator's own first buy, whole quote units; returned as a second, snipe-tax-exempt transaction"),
182
+ creatorTaxBps: z.number().int().min(0).max(1000).optional().describe("Creator tax in basis points, 0–500 (5%) on the adopted parameters"),
183
+ creator: z.string().describe("Wallet that signs the launch; becomes deployer and creatorFeeRecipient"),
184
+ buybackEnabled: z.boolean().optional().describe("Default true"),
185
+ },
186
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
187
+ },
188
+ (args) => run(() => api.buildLaunchTx(args)),
189
+ );
190
+
191
+ server.registerTool(
192
+ "parabolic.protocol_stats",
193
+ {
194
+ title: "Protocol stats",
195
+ description: "Launches, graduations, trades, volume and fees from the subgraph (with a daily series), plus the factory's live launch fee, curve preset, snipe-tax terms and contract addresses.",
196
+ annotations: readOnly,
197
+ },
198
+ () => run(() => api.protocolStats()),
199
+ );
200
+
201
+ server.registerTool(
202
+ "parabolic.docs",
203
+ {
204
+ title: "Parabolic docs",
205
+ description: "The adopted product parameters, curve mechanics, verified Arc network facts and this server's configuration, as text.",
206
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
207
+ },
208
+ () => Promise.resolve({ content: [{ type: "text" as const, text: api.docs() }] }),
209
+ );
210
+
211
+ server.registerResource(
212
+ "parameters",
213
+ "parabolic://docs/parameters",
214
+ { title: "Parabolic parameters and Arc facts", description: "Adopted parameters, curve mechanics, Arc network facts, server configuration", mimeType: "text/plain" },
215
+ (uri) => Promise.resolve({ contents: [{ uri: uri.href, mimeType: "text/plain", text: api.docs() }] }),
216
+ );
217
+
218
+ server.registerResource(
219
+ "coin",
220
+ new ResourceTemplate("parabolic://coins/{address}", { list: undefined }),
221
+ { title: "Parabolic coin", description: "Live coin detail (same payload as parabolic.get_coin) as JSON", mimeType: "application/json" },
222
+ async (uri, variables) => {
223
+ const a = Array.isArray(variables.address) ? variables.address[0] : variables.address;
224
+ return { contents: [{ uri: uri.href, mimeType: "application/json", text: jsonText(await api.getCoin(String(a))) }] };
225
+ },
226
+ );
227
+
228
+ return server;
229
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Parabolic subgraph client (indexer/schema.graphql). Amounts are 18-decimal native USDC (or 6-decimal EURC)
3
+ * BigInt strings; `price` and `marketCap` are BigDecimal strings already scaled to whole quote units.
4
+ * The queries mirror web/lib/data.ts so the MCP server and the site read the same shapes.
5
+ */
6
+ export type Fetch = typeof globalThis.fetch;
7
+
8
+ export const COIN_FIELDS =
9
+ "id curve creator { id } deployer pairToken launchConfigId name symbol logo description totalSupply status createdAt createdAtBlock graduatedAt poolId positionId graduationThreshold raised tokensOnCurve price marketCap volumeQuote tradeCount buyCount sellCount holderCount feesQuote snipeTaxQuote lastTradeAt dayData(first: 2, orderBy: day, orderDirection: desc) { day volumeQuote openPrice closePrice }";
10
+
11
+ export const DAYS = 7;
12
+
13
+ export const queries = {
14
+ coins: `query Coins($first: Int!, $where: Coin_filter) { coins(first: $first, orderBy: lastTradeAt, orderDirection: desc, where: $where) { ${COIN_FIELDS} } }`,
15
+ coin: `query Coin($id: ID!) { coin(id: $id) { ${COIN_FIELDS} } }`,
16
+ coinByCurve: `query CoinByCurve($curve: Bytes!) { coins(first: 1, where: { curve: $curve }) { ${COIN_FIELDS} } }`,
17
+ protocol: `query Protocol { protocol(id: "parabolic") { coinsLaunched coinsGraduated tradeCount volumeQuote feesQuote updatedAt } dailyStats(first: ${DAYS}, orderBy: day, orderDirection: desc) { day coinsLaunched coinsGraduated tradeCount volumeQuote feesQuote } }`,
18
+ } as const;
19
+
20
+ export type GqlCoinDay = { day: number; volumeQuote: string; openPrice: string; closePrice: string };
21
+ export type GqlCoinStatus = "CLIMBING" | "GRADUATED" | "SWEPT" | "RESCUED";
22
+ export type GqlCoin = {
23
+ id: string; curve: string; creator: { id: string }; deployer: string; pairToken: string; launchConfigId: string; name: string; symbol: string; logo: string; description: string;
24
+ totalSupply: string; status: GqlCoinStatus; createdAt: string; createdAtBlock?: string; graduatedAt: string | null; poolId: string | null; positionId?: string | null;
25
+ graduationThreshold: string; raised: string; tokensOnCurve: string; price: string; marketCap: string; volumeQuote: string; tradeCount: string; buyCount: string; sellCount: string;
26
+ holderCount: string; feesQuote?: string; snipeTaxQuote?: string; lastTradeAt: string; dayData?: GqlCoinDay[];
27
+ };
28
+ export type GqlProtocol = { coinsLaunched: string; coinsGraduated: string; tradeCount: string; volumeQuote: string; feesQuote: string; updatedAt?: string };
29
+ export type GqlDaily = { day: number; coinsLaunched: string; coinsGraduated: string; tradeCount: string; volumeQuote: string; feesQuote: string };
30
+
31
+ export class Subgraph {
32
+ constructor(readonly url: string, private readonly fetchImpl: Fetch = globalThis.fetch) {}
33
+
34
+ async query<T>(query: string, variables: Record<string, unknown> = {}): Promise<T> {
35
+ const res = await this.fetchImpl(this.url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ query, variables }) });
36
+ if (!res.ok) throw new Error(`Subgraph responded ${res.status}`);
37
+ const json = (await res.json()) as { data?: T; errors?: { message: string }[] };
38
+ if (json.errors?.length) throw new Error(`Subgraph error: ${json.errors.map((e) => e.message).join("; ")}`);
39
+ if (!json.data) throw new Error("Subgraph returned no data");
40
+ return json.data;
41
+ }
42
+
43
+ async coins(where?: Record<string, unknown>, first = 200): Promise<GqlCoin[]> {
44
+ const d = await this.query<{ coins: GqlCoin[] }>(queries.coins, { first, where: where ?? null });
45
+ return d.coins;
46
+ }
47
+
48
+ async coin(address: string): Promise<GqlCoin | null> {
49
+ const d = await this.query<{ coin: GqlCoin | null }>(queries.coin, { id: address.toLowerCase() });
50
+ return d.coin;
51
+ }
52
+
53
+ async coinByCurve(curve: string): Promise<GqlCoin | null> {
54
+ const d = await this.query<{ coins: GqlCoin[] }>(queries.coinByCurve, { curve: curve.toLowerCase() });
55
+ return d.coins[0] ?? null;
56
+ }
57
+
58
+ async protocol(): Promise<{ protocol: GqlProtocol | null; dailyStats: GqlDaily[] }> {
59
+ return this.query(queries.protocol);
60
+ }
61
+ }
package/src/tx.ts ADDED
@@ -0,0 +1,146 @@
1
+ import { encodeAbiParameters, encodeFunctionData, erc20Abi, formatUnits, keccak256, parseAbi, toHex, type Address, type Hex } from "viem";
2
+ import { curveAbi, factoryAbi, memoRouterAbi } from "./abi.js";
3
+ import { USDC_VIEW } from "./config.js";
4
+
5
+ /**
6
+ * Unsigned transaction builders. Nothing here signs or sends: the caller's wallet does. `value` is a 0x-hex quantity
7
+ * (what eth_sendTransaction expects); `valueWei` is the same number in decimal for convenience.
8
+ */
9
+ export type UnsignedTx = {
10
+ to: Address;
11
+ data: Hex;
12
+ value: Hex;
13
+ valueWei: string;
14
+ chainId: number;
15
+ description: string;
16
+ };
17
+
18
+ const tx = (to: Address, data: Hex, value: bigint, chainId: number, description: string): UnsignedTx => ({
19
+ to,
20
+ data,
21
+ value: toHex(value),
22
+ valueWei: value.toString(),
23
+ chainId,
24
+ description,
25
+ });
26
+
27
+ /** `curve.buy(quoteIn, minTokensOut, recipient)`; native-quoted curves take the quote as msg.value, ERC-20 pairs pull it via allowance. */
28
+ export function encodeBuy(curve: Address, quoteIn: bigint, minTokensOut: bigint, recipient: Address, native: boolean, chainId: number): UnsignedTx {
29
+ const data = encodeFunctionData({ abi: curveAbi, functionName: "buy", args: [quoteIn, minTokensOut, recipient] });
30
+ return tx(curve, data, native ? quoteIn : 0n, chainId, `ParabolicBondingCurve.buy(quoteIn=${quoteIn}, minTokensOut=${minTokensOut}, recipient=${recipient})`);
31
+ }
32
+
33
+ /** `curve.sell(tokensIn, minQuoteOut, recipient)`; the curve pulls the tokens, so it needs an allowance first. */
34
+ export function encodeSell(curve: Address, tokensIn: bigint, minQuoteOut: bigint, recipient: Address, chainId: number): UnsignedTx {
35
+ const data = encodeFunctionData({ abi: curveAbi, functionName: "sell", args: [tokensIn, minQuoteOut, recipient] });
36
+ return tx(curve, data, 0n, chainId, `ParabolicBondingCurve.sell(tokensIn=${tokensIn}, minQuoteOut=${minQuoteOut}, recipient=${recipient})`);
37
+ }
38
+
39
+ /** ERC-20 `approve(spender, amount)`. */
40
+ export function encodeApprove(token: Address, spender: Address, amount: bigint, chainId: number, what = "token"): UnsignedTx {
41
+ const data = encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [spender, amount] });
42
+ return tx(token, data, 0n, chainId, `approve ${what}: ${spender} may spend ${amount} (skip if the allowance already covers it)`);
43
+ }
44
+
45
+ export type Socials = { twitter: string; telegram: string; discord: string; website: string; farcaster: string };
46
+
47
+ /** ParabolicLaunchFactory.TokenParams, the same shape web/components/CreateForm.tsx submits. */
48
+ export type TokenParams = {
49
+ name: string;
50
+ symbol: string;
51
+ logo: string;
52
+ description: string;
53
+ socials: Socials;
54
+ creatorFeeRecipient: Address;
55
+ creatorTaxBps: number;
56
+ buybackEnabled: boolean;
57
+ /** factory.previewLaunchEconomics(launchConfigId, pairToken): pins the curve terms the launch expects. */
58
+ expectedEconomics: Hex;
59
+ /** Random 32 bytes; forwarded to the deployer (reserved for deterministic deployments). */
60
+ salt: Hex;
61
+ };
62
+
63
+ /**
64
+ * `factory.launchToken(params, launchConfigId, pairToken)` with the launch fee as msg.value.
65
+ *
66
+ * There is no exemption-list overload. It was deleted from the factory on 11 September 2026: only the creator's own
67
+ * address is exempt from the snipe tax, which is what "no bundles, ever" means on the site.
68
+ */
69
+ export function encodeLaunch(factory: Address, params: TokenParams, launchConfigId: bigint, pairToken: Address, launchFee: bigint, chainId: number): UnsignedTx {
70
+ const data = encodeFunctionData({ abi: factoryAbi, functionName: "launchToken", args: [params, launchConfigId, pairToken] });
71
+ return tx(factory, data, launchFee, chainId, `ParabolicLaunchFactory.launchToken(${params.symbol}, launchConfigId=${launchConfigId}, pairToken=${pairToken}) with launch fee ${formatUnits(launchFee, 18)} USDC as value`);
72
+ }
73
+
74
+ /**
75
+ * Fair launch through ParabolicMemoRouter, called directly (any wallet; the memo-wrapped form is EOA-only and cannot
76
+ * carry value). Native quote: `launchAndBuy` with msg.value = launchFee + buyQuoteIn. ERC-20 quote:
77
+ * `launchAndBuyWithToken` with msg.value = launchFee and buyQuoteIn pulled from the caller after an approval of the
78
+ * router. The router forwards the caller as deployer (factory.launchTokenFor) and places the opening buy through
79
+ * curve.buyFor(caller, …), so it is snipe-tax exempt; it forwards no exemption list and requires
80
+ * params.creatorFeeRecipient to be zero or the caller (FeeRecipientMustBeCreator).
81
+ */
82
+ export function encodeLaunchAndBuy(router: Address, params: TokenParams, launchConfigId: bigint, pairToken: Address, buyQuoteIn: bigint, minTokensOut: bigint, launchFee: bigint, native: boolean, chainId: number): { tx: UnsignedTx; approval: UnsignedTx | null } {
83
+ if (buyQuoteIn <= 0n) throw new Error("buyQuoteIn must be positive (the router reverts with ZeroBuyAmount)");
84
+ const functionName = native ? "launchAndBuy" : "launchAndBuyWithToken";
85
+ const data = encodeFunctionData({ abi: memoRouterAbi, functionName, args: [params, launchConfigId, pairToken, buyQuoteIn, minTokensOut] });
86
+ const value = native ? launchFee + buyQuoteIn : launchFee;
87
+ return {
88
+ tx: tx(router, data, value, chainId, `ParabolicMemoRouter.${functionName}(${params.symbol}, launchConfigId=${launchConfigId}, pairToken=${pairToken}, buyQuoteIn=${buyQuoteIn}, minTokensOut=${minTokensOut}) with ${native ? "launch fee + opening buy" : "launch fee"} (${formatUnits(value, 18)} USDC) as value`),
89
+ approval: native ? null : encodeApprove(pairToken, router, buyQuoteIn, chainId, "pair token (opening buy)"),
90
+ };
91
+ }
92
+
93
+ // ---- Memo-routed buy (Arc `Memo` system contract + ParabolicMemoRouter) -------------------------------------------
94
+ //
95
+ // `Memo.memo(target, data, memoId, memoData)` runs `target.call(data)` through Arc's callFrom precompile so the
96
+ // router sees the signing EOA as msg.sender, then emits `Memo(...)` with the payload (referral code + note) that the
97
+ // indexer attributes. Constraints inherited from Arc: EOA-only (the Memo contract's caller must be tx.origin, so a
98
+ // smart-account or contract wallet cannot use it) and no value forwarding, so the router pulls whole 6-decimal USDC
99
+ // units through the ERC-20 view of native USDC instead, which needs a one-time approval of the router.
100
+
101
+ export const memoAbi = parseAbi(["function memo(address target, bytes data, bytes32 memoId, bytes memoData)"]);
102
+ export const MEMO_VERSION = 1;
103
+ export const MEMO_ACTION = { LAUNCH: 1, BUY: 2, SELL: 3, POST: 4 } as const;
104
+ export const MEMO_TOPIC = "parabolic.memo.v1";
105
+ export const MAX_NOTE_BYTES = 280;
106
+ export const ZERO_REFERRAL: Hex = "0x0000000000000000";
107
+ const NATIVE_PER_USDC_UNIT = 1_000_000_000_000n;
108
+
109
+ /** A wallet's referral code is bytes8 of its address: the first 16 hex characters after 0x. Accepts a code or a full address. */
110
+ export function referralBytes8(codeOrAddress: string | undefined): Hex {
111
+ if (!codeOrAddress) return ZERO_REFERRAL;
112
+ const s = codeOrAddress.trim().replace(/^0x/i, "").toLowerCase();
113
+ if (/^[0-9a-f]{40}$/.test(s)) return `0x${s.slice(0, 16)}`;
114
+ if (/^[0-9a-f]{16}$/.test(s)) return `0x${s}`;
115
+ throw new Error("referral must be a 16-hex referral code or a wallet address");
116
+ }
117
+
118
+ export function encodeMemoPayload(action: number, referral: Hex, note = ""): Hex {
119
+ if (new TextEncoder().encode(note).length > MAX_NOTE_BYTES) throw new Error(`memo note is longer than ${MAX_NOTE_BYTES} bytes`);
120
+ return encodeAbiParameters([{ type: "uint8" }, { type: "uint8" }, { type: "bytes8" }, { type: "string" }], [MEMO_VERSION, action, referral, note]);
121
+ }
122
+
123
+ /** keccak256(abi.encode("parabolic.memo.v1", action, subject)); the indexer filters Memo logs by this id. */
124
+ export const memoIdOf = (action: number, subject: Address): Hex =>
125
+ keccak256(encodeAbiParameters([{ type: "string" }, { type: "uint8" }, { type: "address" }], [MEMO_TOPIC, action, subject]));
126
+
127
+ /** Whole 6-decimal USDC units the router pulls through the ERC-20 view for `native` 18-decimal wei (rounded up). */
128
+ export const toUsdcUnits = (native: bigint): bigint => (native + NATIVE_PER_USDC_UNIT - 1n) / NATIVE_PER_USDC_UNIT;
129
+
130
+ export type MemoBuy = { tx: UnsignedTx; approval: UnsignedTx; usdcUnits: string; eoaOnly: true; memoId: Hex; payload: Hex };
131
+
132
+ export function encodeMemoBuy(p: { arcMemo: Address; router: Address; curve: Address; quoteIn: bigint; minTokensOut: bigint; recipient: Address; referral?: string; note?: string; chainId: number }): MemoBuy {
133
+ const inner = encodeFunctionData({ abi: memoRouterAbi, functionName: "buy", args: [p.curve, p.quoteIn, p.minTokensOut, p.recipient] });
134
+ const memoId = memoIdOf(MEMO_ACTION.BUY, p.curve);
135
+ const payload = encodeMemoPayload(MEMO_ACTION.BUY, referralBytes8(p.referral), p.note ?? "");
136
+ const data = encodeFunctionData({ abi: memoAbi, functionName: "memo", args: [p.router, inner, memoId, payload] });
137
+ const units = toUsdcUnits(p.quoteIn);
138
+ return {
139
+ tx: tx(p.arcMemo, data, 0n, p.chainId, `Arc Memo.memo -> ParabolicMemoRouter.buy(curve=${p.curve}, quoteIn=${p.quoteIn}) carrying referral ${referralBytes8(p.referral)}; EOA-only, no value (the router pulls ${units} USDC units via the ERC-20 view)`),
140
+ approval: encodeApprove(USDC_VIEW, p.router, units, p.chainId, "USDC (ERC-20 view)"),
141
+ usdcUnits: units.toString(),
142
+ eoaOnly: true,
143
+ memoId,
144
+ payload,
145
+ };
146
+ }