@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.
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/abi/ParabolicBondingCurve.json +1223 -0
- package/abi/ParabolicLaunchFactory.json +2212 -0
- package/abi/ParabolicLauncherToken.json +577 -0
- package/abi/ParabolicMemoRouter.json +950 -0
- package/dist/abi.d.ts +17 -0
- package/dist/abi.js +37 -0
- package/dist/abi.js.map +1 -0
- package/dist/chain.d.ts +32 -0
- package/dist/chain.js +48 -0
- package/dist/chain.js.map +1 -0
- package/dist/coins.d.ts +52 -0
- package/dist/coins.js +75 -0
- package/dist/coins.js.map +1 -0
- package/dist/config.d.ts +55 -0
- package/dist/config.js +86 -0
- package/dist/config.js.map +1 -0
- package/dist/curve.d.ts +92 -0
- package/dist/curve.js +127 -0
- package/dist/curve.js.map +1 -0
- package/dist/docs.d.ts +3 -0
- package/dist/docs.js +63 -0
- package/dist/docs.js.map +1 -0
- package/dist/format.d.ts +19 -0
- package/dist/format.js +30 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/parabolic.d.ts +780 -0
- package/dist/parabolic.js +710 -0
- package/dist/parabolic.js.map +1 -0
- package/dist/server.d.ts +32 -0
- package/dist/server.js +149 -0
- package/dist/server.js.map +1 -0
- package/dist/subgraph.d.ts +85 -0
- package/dist/subgraph.js +43 -0
- package/dist/subgraph.js.map +1 -0
- package/dist/tx.d.ts +115 -0
- package/dist/tx.js +105 -0
- package/dist/tx.js.map +1 -0
- package/package.json +64 -0
- package/src/abi.ts +41 -0
- package/src/chain.ts +60 -0
- package/src/coins.ts +118 -0
- package/src/config.ts +123 -0
- package/src/curve.ts +171 -0
- package/src/docs.ts +63 -0
- package/src/format.ts +34 -0
- package/src/index.ts +14 -0
- package/src/parabolic.ts +753 -0
- package/src/server.ts +229 -0
- package/src/subgraph.ts +61 -0
- package/src/tx.ts +146 -0
package/dist/tx.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { encodeAbiParameters, encodeFunctionData, erc20Abi, formatUnits, keccak256, parseAbi, toHex } from "viem";
|
|
2
|
+
import { curveAbi, factoryAbi, memoRouterAbi } from "./abi.js";
|
|
3
|
+
import { USDC_VIEW } from "./config.js";
|
|
4
|
+
const tx = (to, data, value, chainId, description) => ({
|
|
5
|
+
to,
|
|
6
|
+
data,
|
|
7
|
+
value: toHex(value),
|
|
8
|
+
valueWei: value.toString(),
|
|
9
|
+
chainId,
|
|
10
|
+
description,
|
|
11
|
+
});
|
|
12
|
+
/** `curve.buy(quoteIn, minTokensOut, recipient)`; native-quoted curves take the quote as msg.value, ERC-20 pairs pull it via allowance. */
|
|
13
|
+
export function encodeBuy(curve, quoteIn, minTokensOut, recipient, native, chainId) {
|
|
14
|
+
const data = encodeFunctionData({ abi: curveAbi, functionName: "buy", args: [quoteIn, minTokensOut, recipient] });
|
|
15
|
+
return tx(curve, data, native ? quoteIn : 0n, chainId, `ParabolicBondingCurve.buy(quoteIn=${quoteIn}, minTokensOut=${minTokensOut}, recipient=${recipient})`);
|
|
16
|
+
}
|
|
17
|
+
/** `curve.sell(tokensIn, minQuoteOut, recipient)`; the curve pulls the tokens, so it needs an allowance first. */
|
|
18
|
+
export function encodeSell(curve, tokensIn, minQuoteOut, recipient, chainId) {
|
|
19
|
+
const data = encodeFunctionData({ abi: curveAbi, functionName: "sell", args: [tokensIn, minQuoteOut, recipient] });
|
|
20
|
+
return tx(curve, data, 0n, chainId, `ParabolicBondingCurve.sell(tokensIn=${tokensIn}, minQuoteOut=${minQuoteOut}, recipient=${recipient})`);
|
|
21
|
+
}
|
|
22
|
+
/** ERC-20 `approve(spender, amount)`. */
|
|
23
|
+
export function encodeApprove(token, spender, amount, chainId, what = "token") {
|
|
24
|
+
const data = encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [spender, amount] });
|
|
25
|
+
return tx(token, data, 0n, chainId, `approve ${what}: ${spender} may spend ${amount} (skip if the allowance already covers it)`);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* `factory.launchToken(params, launchConfigId, pairToken)` with the launch fee as msg.value.
|
|
29
|
+
*
|
|
30
|
+
* There is no exemption-list overload. It was deleted from the factory on 11 September 2026: only the creator's own
|
|
31
|
+
* address is exempt from the snipe tax, which is what "no bundles, ever" means on the site.
|
|
32
|
+
*/
|
|
33
|
+
export function encodeLaunch(factory, params, launchConfigId, pairToken, launchFee, chainId) {
|
|
34
|
+
const data = encodeFunctionData({ abi: factoryAbi, functionName: "launchToken", args: [params, launchConfigId, pairToken] });
|
|
35
|
+
return tx(factory, data, launchFee, chainId, `ParabolicLaunchFactory.launchToken(${params.symbol}, launchConfigId=${launchConfigId}, pairToken=${pairToken}) with launch fee ${formatUnits(launchFee, 18)} USDC as value`);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Fair launch through ParabolicMemoRouter, called directly (any wallet; the memo-wrapped form is EOA-only and cannot
|
|
39
|
+
* carry value). Native quote: `launchAndBuy` with msg.value = launchFee + buyQuoteIn. ERC-20 quote:
|
|
40
|
+
* `launchAndBuyWithToken` with msg.value = launchFee and buyQuoteIn pulled from the caller after an approval of the
|
|
41
|
+
* router. The router forwards the caller as deployer (factory.launchTokenFor) and places the opening buy through
|
|
42
|
+
* curve.buyFor(caller, …), so it is snipe-tax exempt; it forwards no exemption list and requires
|
|
43
|
+
* params.creatorFeeRecipient to be zero or the caller (FeeRecipientMustBeCreator).
|
|
44
|
+
*/
|
|
45
|
+
export function encodeLaunchAndBuy(router, params, launchConfigId, pairToken, buyQuoteIn, minTokensOut, launchFee, native, chainId) {
|
|
46
|
+
if (buyQuoteIn <= 0n)
|
|
47
|
+
throw new Error("buyQuoteIn must be positive (the router reverts with ZeroBuyAmount)");
|
|
48
|
+
const functionName = native ? "launchAndBuy" : "launchAndBuyWithToken";
|
|
49
|
+
const data = encodeFunctionData({ abi: memoRouterAbi, functionName, args: [params, launchConfigId, pairToken, buyQuoteIn, minTokensOut] });
|
|
50
|
+
const value = native ? launchFee + buyQuoteIn : launchFee;
|
|
51
|
+
return {
|
|
52
|
+
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`),
|
|
53
|
+
approval: native ? null : encodeApprove(pairToken, router, buyQuoteIn, chainId, "pair token (opening buy)"),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// ---- Memo-routed buy (Arc `Memo` system contract + ParabolicMemoRouter) -------------------------------------------
|
|
57
|
+
//
|
|
58
|
+
// `Memo.memo(target, data, memoId, memoData)` runs `target.call(data)` through Arc's callFrom precompile so the
|
|
59
|
+
// router sees the signing EOA as msg.sender, then emits `Memo(...)` with the payload (referral code + note) that the
|
|
60
|
+
// indexer attributes. Constraints inherited from Arc: EOA-only (the Memo contract's caller must be tx.origin, so a
|
|
61
|
+
// smart-account or contract wallet cannot use it) and no value forwarding, so the router pulls whole 6-decimal USDC
|
|
62
|
+
// units through the ERC-20 view of native USDC instead, which needs a one-time approval of the router.
|
|
63
|
+
export const memoAbi = parseAbi(["function memo(address target, bytes data, bytes32 memoId, bytes memoData)"]);
|
|
64
|
+
export const MEMO_VERSION = 1;
|
|
65
|
+
export const MEMO_ACTION = { LAUNCH: 1, BUY: 2, SELL: 3, POST: 4 };
|
|
66
|
+
export const MEMO_TOPIC = "parabolic.memo.v1";
|
|
67
|
+
export const MAX_NOTE_BYTES = 280;
|
|
68
|
+
export const ZERO_REFERRAL = "0x0000000000000000";
|
|
69
|
+
const NATIVE_PER_USDC_UNIT = 1000000000000n;
|
|
70
|
+
/** A wallet's referral code is bytes8 of its address: the first 16 hex characters after 0x. Accepts a code or a full address. */
|
|
71
|
+
export function referralBytes8(codeOrAddress) {
|
|
72
|
+
if (!codeOrAddress)
|
|
73
|
+
return ZERO_REFERRAL;
|
|
74
|
+
const s = codeOrAddress.trim().replace(/^0x/i, "").toLowerCase();
|
|
75
|
+
if (/^[0-9a-f]{40}$/.test(s))
|
|
76
|
+
return `0x${s.slice(0, 16)}`;
|
|
77
|
+
if (/^[0-9a-f]{16}$/.test(s))
|
|
78
|
+
return `0x${s}`;
|
|
79
|
+
throw new Error("referral must be a 16-hex referral code or a wallet address");
|
|
80
|
+
}
|
|
81
|
+
export function encodeMemoPayload(action, referral, note = "") {
|
|
82
|
+
if (new TextEncoder().encode(note).length > MAX_NOTE_BYTES)
|
|
83
|
+
throw new Error(`memo note is longer than ${MAX_NOTE_BYTES} bytes`);
|
|
84
|
+
return encodeAbiParameters([{ type: "uint8" }, { type: "uint8" }, { type: "bytes8" }, { type: "string" }], [MEMO_VERSION, action, referral, note]);
|
|
85
|
+
}
|
|
86
|
+
/** keccak256(abi.encode("parabolic.memo.v1", action, subject)); the indexer filters Memo logs by this id. */
|
|
87
|
+
export const memoIdOf = (action, subject) => keccak256(encodeAbiParameters([{ type: "string" }, { type: "uint8" }, { type: "address" }], [MEMO_TOPIC, action, subject]));
|
|
88
|
+
/** Whole 6-decimal USDC units the router pulls through the ERC-20 view for `native` 18-decimal wei (rounded up). */
|
|
89
|
+
export const toUsdcUnits = (native) => (native + NATIVE_PER_USDC_UNIT - 1n) / NATIVE_PER_USDC_UNIT;
|
|
90
|
+
export function encodeMemoBuy(p) {
|
|
91
|
+
const inner = encodeFunctionData({ abi: memoRouterAbi, functionName: "buy", args: [p.curve, p.quoteIn, p.minTokensOut, p.recipient] });
|
|
92
|
+
const memoId = memoIdOf(MEMO_ACTION.BUY, p.curve);
|
|
93
|
+
const payload = encodeMemoPayload(MEMO_ACTION.BUY, referralBytes8(p.referral), p.note ?? "");
|
|
94
|
+
const data = encodeFunctionData({ abi: memoAbi, functionName: "memo", args: [p.router, inner, memoId, payload] });
|
|
95
|
+
const units = toUsdcUnits(p.quoteIn);
|
|
96
|
+
return {
|
|
97
|
+
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)`),
|
|
98
|
+
approval: encodeApprove(USDC_VIEW, p.router, units, p.chainId, "USDC (ERC-20 view)"),
|
|
99
|
+
usdcUnits: units.toString(),
|
|
100
|
+
eoaOnly: true,
|
|
101
|
+
memoId,
|
|
102
|
+
payload,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=tx.js.map
|
package/dist/tx.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tx.js","sourceRoot":"","sources":["../src/tx.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAA0B,MAAM,MAAM,CAAC;AAC1I,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAexC,MAAM,EAAE,GAAG,CAAC,EAAW,EAAE,IAAS,EAAE,KAAa,EAAE,OAAe,EAAE,WAAmB,EAAc,EAAE,CAAC,CAAC;IACvG,EAAE;IACF,IAAI;IACJ,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC;IACnB,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE;IAC1B,OAAO;IACP,WAAW;CACZ,CAAC,CAAC;AAEH,2IAA2I;AAC3I,MAAM,UAAU,SAAS,CAAC,KAAc,EAAE,OAAe,EAAE,YAAoB,EAAE,SAAkB,EAAE,MAAe,EAAE,OAAe;IACnI,MAAM,IAAI,GAAG,kBAAkB,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,YAAY,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC;IAClH,OAAO,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,OAAO,EAAE,qCAAqC,OAAO,kBAAkB,YAAY,eAAe,SAAS,GAAG,CAAC,CAAC;AAChK,CAAC;AAED,kHAAkH;AAClH,MAAM,UAAU,UAAU,CAAC,KAAc,EAAE,QAAgB,EAAE,WAAmB,EAAE,SAAkB,EAAE,OAAe;IACnH,MAAM,IAAI,GAAG,kBAAkB,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC;IACnH,OAAO,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,uCAAuC,QAAQ,iBAAiB,WAAW,eAAe,SAAS,GAAG,CAAC,CAAC;AAC9I,CAAC;AAED,yCAAyC;AACzC,MAAM,UAAU,aAAa,CAAC,KAAc,EAAE,OAAgB,EAAE,MAAc,EAAE,OAAe,EAAE,IAAI,GAAG,OAAO;IAC7G,MAAM,IAAI,GAAG,kBAAkB,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,YAAY,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACrG,OAAO,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,WAAW,IAAI,KAAK,OAAO,cAAc,MAAM,4CAA4C,CAAC,CAAC;AACnI,CAAC;AAoBD;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,OAAgB,EAAE,MAAmB,EAAE,cAAsB,EAAE,SAAkB,EAAE,SAAiB,EAAE,OAAe;IAChJ,MAAM,IAAI,GAAG,kBAAkB,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,cAAc,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC;IAC7H,OAAO,EAAE,CAAC,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,sCAAsC,MAAM,CAAC,MAAM,oBAAoB,cAAc,eAAe,SAAS,qBAAqB,WAAW,CAAC,SAAS,EAAE,EAAE,CAAC,gBAAgB,CAAC,CAAC;AAC7N,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAe,EAAE,MAAmB,EAAE,cAAsB,EAAE,SAAkB,EAAE,UAAkB,EAAE,YAAoB,EAAE,SAAiB,EAAE,MAAe,EAAE,OAAe;IAChN,IAAI,UAAU,IAAI,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,qEAAqE,CAAC,CAAC;IAC7G,MAAM,YAAY,GAAG,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,uBAAuB,CAAC;IACvE,MAAM,IAAI,GAAG,kBAAkB,CAAC,EAAE,GAAG,EAAE,aAAa,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,cAAc,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,CAAC,EAAE,CAAC,CAAC;IAC3I,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,SAAS,GAAG,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC;IAC1D,OAAO;QACL,EAAE,EAAE,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,uBAAuB,YAAY,IAAI,MAAM,CAAC,MAAM,oBAAoB,cAAc,eAAe,SAAS,gBAAgB,UAAU,kBAAkB,YAAY,UAAU,MAAM,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC,CAAC,YAAY,KAAK,WAAW,CAAC,KAAK,EAAE,EAAE,CAAC,iBAAiB,CAAC;QACpT,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,0BAA0B,CAAC;KAC5G,CAAC;AACJ,CAAC;AAED,sHAAsH;AACtH,EAAE;AACF,gHAAgH;AAChH,qHAAqH;AACrH,mHAAmH;AACnH,oHAAoH;AACpH,uGAAuG;AAEvG,MAAM,CAAC,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,2EAA2E,CAAC,CAAC,CAAC;AAC/G,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC;AAC9B,MAAM,CAAC,MAAM,WAAW,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAW,CAAC;AAC5E,MAAM,CAAC,MAAM,UAAU,GAAG,mBAAmB,CAAC;AAC9C,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,CAAC;AAClC,MAAM,CAAC,MAAM,aAAa,GAAQ,oBAAoB,CAAC;AACvD,MAAM,oBAAoB,GAAG,cAAkB,CAAC;AAEhD,iIAAiI;AACjI,MAAM,UAAU,cAAc,CAAC,aAAiC;IAC9D,IAAI,CAAC,aAAa;QAAE,OAAO,aAAa,CAAC;IACzC,MAAM,CAAC,GAAG,aAAa,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACjE,IAAI,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;IAC3D,IAAI,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC,EAAE,CAAC;IAC9C,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;AACjF,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,MAAc,EAAE,QAAa,EAAE,IAAI,GAAG,EAAE;IACxE,IAAI,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,cAAc;QAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,cAAc,QAAQ,CAAC,CAAC;IAChI,OAAO,mBAAmB,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;AACrJ,CAAC;AAED,6GAA6G;AAC7G,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,MAAc,EAAE,OAAgB,EAAO,EAAE,CAChE,SAAS,CAAC,mBAAmB,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;AAE9H,oHAAoH;AACpH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAAc,EAAU,EAAE,CAAC,CAAC,MAAM,GAAG,oBAAoB,GAAG,EAAE,CAAC,GAAG,oBAAoB,CAAC;AAInH,MAAM,UAAU,aAAa,CAAC,CAAsK;IAClM,MAAM,KAAK,GAAG,kBAAkB,CAAC,EAAE,GAAG,EAAE,aAAa,EAAE,YAAY,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACvI,MAAM,MAAM,GAAG,QAAQ,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;IAClD,MAAM,OAAO,GAAG,iBAAiB,CAAC,WAAW,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IAC7F,MAAM,IAAI,GAAG,kBAAkB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IAClH,MAAM,KAAK,GAAG,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IACrC,OAAO;QACL,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,kDAAkD,CAAC,CAAC,KAAK,aAAa,CAAC,CAAC,OAAO,uBAAuB,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC,0CAA0C,KAAK,kCAAkC,CAAC;QACzP,QAAQ,EAAE,aAAa,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,oBAAoB,CAAC;QACpF,SAAS,EAAE,KAAK,CAAC,QAAQ,EAAE;QAC3B,OAAO,EAAE,IAAI;QACb,MAAM;QACN,OAAO;KACR,CAAC;AACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@parabolicfamily/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Parabolic, the dollar-native token launchpad on Arc: discover coins, quote curve trades and build unsigned transactions. Never holds keys.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"parabolic-mcp": "dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"main": "dist/server.js",
|
|
14
|
+
"types": "dist/server.d.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src",
|
|
18
|
+
"abi",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"sync-abis": "node scripts/sync-abis.mjs",
|
|
24
|
+
"build": "tsc -p tsconfig.json",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"start": "node dist/index.js",
|
|
27
|
+
"smoke": "node scripts/smoke.mjs",
|
|
28
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
29
|
+
"prepublishOnly": "npm run build && npm test && npm run smoke",
|
|
30
|
+
"prepack": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
34
|
+
"viem": "^2.56.3",
|
|
35
|
+
"zod": "^4.5.4"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^22.19.0",
|
|
39
|
+
"typescript": "^5.9.0",
|
|
40
|
+
"vitest": "^4.1.11"
|
|
41
|
+
},
|
|
42
|
+
"author": "Parabolic",
|
|
43
|
+
"homepage": "https://www.parabolic.family",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"email": "contact@parabolic.family"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"mcp",
|
|
49
|
+
"model-context-protocol",
|
|
50
|
+
"arc",
|
|
51
|
+
"circle",
|
|
52
|
+
"usdc",
|
|
53
|
+
"launchpad",
|
|
54
|
+
"bonding-curve",
|
|
55
|
+
"defi",
|
|
56
|
+
"uniswap-v4",
|
|
57
|
+
"erc-8004",
|
|
58
|
+
"agent",
|
|
59
|
+
"web3"
|
|
60
|
+
],
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/abi.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import type { Abi } from "viem";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ABI loader. `scripts/sync-abis.mjs` copies the `abi` field of the Foundry artifacts in
|
|
8
|
+
* ../contracts/out into mcp/abi/*.json; this module reads them at import time. The directory is
|
|
9
|
+
* one level above both src/ (tests) and dist/ (build), so the same relative path works for both.
|
|
10
|
+
* Override with PARABOLIC_ABI_DIR when the package is bundled elsewhere.
|
|
11
|
+
*/
|
|
12
|
+
export const ABI_DIR = process.env.PARABOLIC_ABI_DIR ?? resolve(dirname(fileURLToPath(import.meta.url)), "..", "abi");
|
|
13
|
+
|
|
14
|
+
export const ABI_NAMES = ["ParabolicLaunchFactory", "ParabolicBondingCurve", "ParabolicLauncherToken", "ParabolicMemoRouter"] as const;
|
|
15
|
+
export type AbiName = (typeof ABI_NAMES)[number];
|
|
16
|
+
|
|
17
|
+
const cache = new Map<AbiName, Abi>();
|
|
18
|
+
|
|
19
|
+
export function loadAbi(name: AbiName): Abi {
|
|
20
|
+
const hit = cache.get(name);
|
|
21
|
+
if (hit) return hit;
|
|
22
|
+
const file = join(ABI_DIR, `${name}.json`);
|
|
23
|
+
let parsed: unknown;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
26
|
+
} catch (e) {
|
|
27
|
+
throw new Error(`Cannot read the bundled ABI ${file}. The package ships abi/*.json; a missing file means a damaged install, so reinstall @parabolicfamily/mcp (${(e as Error).message})`);
|
|
28
|
+
}
|
|
29
|
+
if (!Array.isArray(parsed)) throw new Error(`ABI ${file} is not a JSON array`);
|
|
30
|
+
const abi = parsed as Abi;
|
|
31
|
+
cache.set(name, abi);
|
|
32
|
+
return abi;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const factoryAbi: Abi = loadAbi("ParabolicLaunchFactory");
|
|
36
|
+
export const curveAbi: Abi = loadAbi("ParabolicBondingCurve");
|
|
37
|
+
export const tokenAbi: Abi = loadAbi("ParabolicLauncherToken");
|
|
38
|
+
export const memoRouterAbi: Abi = loadAbi("ParabolicMemoRouter");
|
|
39
|
+
|
|
40
|
+
/** Names of all functions in an ABI (used to document what the factory can and cannot enumerate). */
|
|
41
|
+
export const functionNames = (abi: Abi): string[] => abi.flatMap((e) => (e.type === "function" ? [e.name] : []));
|
package/src/chain.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createPublicClient, defineChain, getAbiItem, http, type Abi, type AbiEvent, type Address, type Chain, type Hex } from "viem";
|
|
2
|
+
import { chainName, explorerBase } from "./config.js";
|
|
3
|
+
|
|
4
|
+
/** The narrow RPC surface the server needs; tests substitute an in-memory implementation. */
|
|
5
|
+
export type ReadArgs = { address: Address; abi: Abi; functionName: string; args?: readonly unknown[] };
|
|
6
|
+
export type LogsArgs = { address: Address; abi: Abi; eventName: string; fromBlock: bigint; toBlock?: bigint };
|
|
7
|
+
export type DecodedLog = { args: Record<string, unknown>; blockNumber: bigint; transactionHash: Hex; logIndex: number };
|
|
8
|
+
|
|
9
|
+
export interface Rpc {
|
|
10
|
+
read(a: ReadArgs): Promise<unknown>;
|
|
11
|
+
logs(a: LogsArgs): Promise<DecodedLog[]>;
|
|
12
|
+
blockNumber(): Promise<bigint>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Arc chain definition for viem: native USDC with 18-decimal native accounting. */
|
|
16
|
+
export function arcChain(chainId: number, rpcUrl: string): Chain {
|
|
17
|
+
const explorer = explorerBase(chainId);
|
|
18
|
+
return defineChain({
|
|
19
|
+
id: chainId,
|
|
20
|
+
name: chainName(chainId),
|
|
21
|
+
nativeCurrency: { name: "USDC", symbol: "USDC", decimals: 18 },
|
|
22
|
+
rpcUrls: { default: { http: [rpcUrl] } },
|
|
23
|
+
...(explorer ? { blockExplorers: { default: { name: "Arcscan", url: explorer } } } : {}),
|
|
24
|
+
testnet: chainId !== 5042,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** viem-backed Rpc. HTTP batching folds the many small reads of get_coin / list_coins into a few JSON-RPC requests. */
|
|
29
|
+
export function viemRpc(rpcUrl: string, chainId: number): Rpc {
|
|
30
|
+
const client = createPublicClient({ chain: arcChain(chainId, rpcUrl), transport: http(rpcUrl, { batch: { batchSize: 50, wait: 5 } }) });
|
|
31
|
+
return {
|
|
32
|
+
read: (a) => client.readContract({ address: a.address, abi: a.abi, functionName: a.functionName, args: a.args ?? [] }),
|
|
33
|
+
async logs(a) {
|
|
34
|
+
const event = getAbiItem({ abi: a.abi, name: a.eventName }) as AbiEvent | undefined;
|
|
35
|
+
if (!event || event.type !== "event") throw new Error(`No event ${a.eventName} in ABI`);
|
|
36
|
+
const logs = await client.getLogs({ address: a.address, event, fromBlock: a.fromBlock, toBlock: a.toBlock ?? "latest", strict: false });
|
|
37
|
+
return logs.map((l) => ({
|
|
38
|
+
args: (l.args ?? {}) as Record<string, unknown>,
|
|
39
|
+
blockNumber: l.blockNumber ?? 0n,
|
|
40
|
+
transactionHash: (l.transactionHash ?? "0x") as Hex,
|
|
41
|
+
logIndex: l.logIndex ?? 0,
|
|
42
|
+
}));
|
|
43
|
+
},
|
|
44
|
+
blockNumber: () => client.getBlockNumber(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Runs `fn` over `items` with at most `limit` in flight, preserving order. */
|
|
49
|
+
export async function mapLimit<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]> {
|
|
50
|
+
const out: R[] = new Array(items.length);
|
|
51
|
+
let next = 0;
|
|
52
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
53
|
+
while (next < items.length) {
|
|
54
|
+
const i = next++;
|
|
55
|
+
out[i] = await fn(items[i] as T, i);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
await Promise.all(workers);
|
|
59
|
+
return out;
|
|
60
|
+
}
|
package/src/coins.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { formatUnits, getAddress, type Address } from "viem";
|
|
2
|
+
import { NATIVE_QUOTE } from "./config.js";
|
|
3
|
+
import { toNumber } from "./curve.js";
|
|
4
|
+
import type { GqlCoin } from "./subgraph.js";
|
|
5
|
+
|
|
6
|
+
export type StatusKey = "climbing" | "graduated";
|
|
7
|
+
export const SORT_KEYS = ["trending", "new", "near_graduation", "market_cap"] as const;
|
|
8
|
+
export type SortKey = (typeof SORT_KEYS)[number];
|
|
9
|
+
|
|
10
|
+
export type CoinSummary = {
|
|
11
|
+
address: Address;
|
|
12
|
+
curve: Address;
|
|
13
|
+
name: string;
|
|
14
|
+
ticker: string;
|
|
15
|
+
creator: Address;
|
|
16
|
+
pair: "USDC" | "EURC";
|
|
17
|
+
pairToken: Address;
|
|
18
|
+
quoteDecimals: number;
|
|
19
|
+
status: StatusKey;
|
|
20
|
+
/** Quote raised on the curve (net of fees), whole units; for a graduated coin the threshold it crossed. */
|
|
21
|
+
raised: string;
|
|
22
|
+
raisedRaw: bigint;
|
|
23
|
+
threshold: string;
|
|
24
|
+
thresholdRaw: bigint;
|
|
25
|
+
progressPct: number;
|
|
26
|
+
/** Whole quote units per whole token. */
|
|
27
|
+
price: number;
|
|
28
|
+
marketCap: number;
|
|
29
|
+
holders?: number;
|
|
30
|
+
volume24h?: number;
|
|
31
|
+
change24h?: number;
|
|
32
|
+
tradeCount?: number;
|
|
33
|
+
createdAt?: number;
|
|
34
|
+
lastTradeAt?: number;
|
|
35
|
+
graduatedAt?: number;
|
|
36
|
+
poolId?: string;
|
|
37
|
+
logo?: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
launchConfigId?: number;
|
|
40
|
+
source: "subgraph" | "chain";
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const isNative = (pairToken: string): boolean => pairToken.toLowerCase() === NATIVE_QUOTE;
|
|
44
|
+
export const quoteDecimalsOf = (pairToken: string): number => (isNative(pairToken) ? 18 : 6);
|
|
45
|
+
|
|
46
|
+
/** Curve progress in percent with three decimals, capped at 100. */
|
|
47
|
+
export const progressPct = (raised: bigint, threshold: bigint): number => (threshold <= 0n ? 0 : Math.min(100, Number((raised * 100_000n) / threshold) / 1000));
|
|
48
|
+
|
|
49
|
+
const dayOf = (sec: number) => Math.floor(sec / 86_400);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 24h figures from the coin's two most recent UTC day buckets, the same derivation as web/lib/data.ts:
|
|
53
|
+
* volume = today + yesterday; change = current price against yesterday's close (or today's open).
|
|
54
|
+
*/
|
|
55
|
+
export function dayStats(g: GqlCoin, now: number, quoteDecimals: number): { volume24h: number; change24h: number } {
|
|
56
|
+
const today = dayOf(now);
|
|
57
|
+
const buckets = g.dayData ?? [];
|
|
58
|
+
const volume24h = buckets.filter((d) => d.day >= today - 1).reduce((sum, d) => sum + toNumber(BigInt(d.volumeQuote), quoteDecimals), 0);
|
|
59
|
+
const todayBucket = buckets.find((d) => d.day === today);
|
|
60
|
+
const yesterday = buckets.find((d) => d.day === today - 1);
|
|
61
|
+
const ref = yesterday ? Number(yesterday.closePrice) : todayBucket ? Number(todayBucket.openPrice) : 0;
|
|
62
|
+
const current = Number(g.price);
|
|
63
|
+
const change24h = ref > 0 && current > 0 ? ((current - ref) / ref) * 100 : 0;
|
|
64
|
+
return { volume24h, change24h };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function summaryFromGql(g: GqlCoin, now = Math.floor(Date.now() / 1000)): CoinSummary {
|
|
68
|
+
const native = isNative(g.pairToken);
|
|
69
|
+
const quoteDecimals = native ? 18 : 6;
|
|
70
|
+
const graduated = g.status !== "CLIMBING";
|
|
71
|
+
const raisedRaw = BigInt(g.raised);
|
|
72
|
+
const thresholdRaw = BigInt(g.graduationThreshold);
|
|
73
|
+
const { volume24h, change24h } = dayStats(g, now, quoteDecimals);
|
|
74
|
+
return {
|
|
75
|
+
address: getAddress(g.id),
|
|
76
|
+
curve: getAddress(g.curve),
|
|
77
|
+
name: g.name,
|
|
78
|
+
ticker: g.symbol,
|
|
79
|
+
creator: getAddress(g.deployer),
|
|
80
|
+
pair: native ? "USDC" : "EURC",
|
|
81
|
+
pairToken: getAddress(g.pairToken),
|
|
82
|
+
quoteDecimals,
|
|
83
|
+
status: graduated ? "graduated" : "climbing",
|
|
84
|
+
raised: formatUnits(graduated ? thresholdRaw : raisedRaw, quoteDecimals),
|
|
85
|
+
raisedRaw: graduated ? thresholdRaw : raisedRaw,
|
|
86
|
+
threshold: formatUnits(thresholdRaw, quoteDecimals),
|
|
87
|
+
thresholdRaw,
|
|
88
|
+
progressPct: graduated ? 100 : progressPct(raisedRaw, thresholdRaw),
|
|
89
|
+
price: Number(g.price),
|
|
90
|
+
marketCap: Number(g.marketCap),
|
|
91
|
+
holders: Number(g.holderCount),
|
|
92
|
+
volume24h,
|
|
93
|
+
change24h,
|
|
94
|
+
tradeCount: Number(g.tradeCount),
|
|
95
|
+
createdAt: Number(g.createdAt),
|
|
96
|
+
lastTradeAt: Number(g.lastTradeAt),
|
|
97
|
+
graduatedAt: g.graduatedAt ? Number(g.graduatedAt) : undefined,
|
|
98
|
+
poolId: g.poolId ?? undefined,
|
|
99
|
+
logo: g.logo || undefined,
|
|
100
|
+
description: g.description || undefined,
|
|
101
|
+
launchConfigId: Number(g.launchConfigId),
|
|
102
|
+
source: "subgraph",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const cmpBig = (a: bigint, b: bigint): number => (a < b ? -1 : a > b ? 1 : 0);
|
|
107
|
+
const statusRank = (c: CoinSummary): number => (c.status === "climbing" ? 0 : 1);
|
|
108
|
+
|
|
109
|
+
/** Pure sort used by list_coins; `near_graduation` ranks climbing coins by progress and puts graduated ones last. */
|
|
110
|
+
export function sortCoins(list: CoinSummary[], sort: SortKey): CoinSummary[] {
|
|
111
|
+
const by: Record<SortKey, (a: CoinSummary, b: CoinSummary) => number> = {
|
|
112
|
+
trending: (a, b) => (b.volume24h ?? 0) - (a.volume24h ?? 0) || (b.lastTradeAt ?? 0) - (a.lastTradeAt ?? 0) || cmpBig(b.raisedRaw, a.raisedRaw),
|
|
113
|
+
new: (a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0),
|
|
114
|
+
near_graduation: (a, b) => statusRank(a) - statusRank(b) || b.progressPct - a.progressPct || cmpBig(b.raisedRaw, a.raisedRaw),
|
|
115
|
+
market_cap: (a, b) => b.marketCap - a.marketCap,
|
|
116
|
+
};
|
|
117
|
+
return [...list].sort(by[sort]);
|
|
118
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { getAddress, isAddress, type Address } from "viem";
|
|
2
|
+
|
|
3
|
+
/** Arc's system `Memo` contract (CREATE2, zero salt), verified on Arc Testnet; expected to match on mainnet. */
|
|
4
|
+
export const ARC_MEMO_DEFAULT: Address = "0x5294E9927c3306DcBaDb03fe70b92e01cCede505";
|
|
5
|
+
/** ERC-20 view of native USDC on Arc (6 decimals, same balance as the 18-decimal native asset). */
|
|
6
|
+
export const USDC_VIEW: Address = "0x3600000000000000000000000000000000000000";
|
|
7
|
+
/** `address(0)` as the pair token means the curve is quoted in native USDC. */
|
|
8
|
+
export const NATIVE_QUOTE: Address = "0x0000000000000000000000000000000000000000";
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_CHAIN_ID = 5042002;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Deployed Parabolic addresses per chain, so the server is useful with no configuration at all.
|
|
14
|
+
* The source repository is private; these are the same addresses the site publishes at
|
|
15
|
+
* https://www.parabolic.family/docs#contracts and that are verified on Arcscan.
|
|
16
|
+
*
|
|
17
|
+
* Arc mainnet (5042) opens on 16 September 2026. Its addresses ship in the release that follows the
|
|
18
|
+
* deployment; until then a mainnet chain id resolves the RPC and nothing else, and any tool that
|
|
19
|
+
* needs the factory says so.
|
|
20
|
+
*/
|
|
21
|
+
export const DEPLOYMENTS: Record<number, {
|
|
22
|
+
rpcUrl: string;
|
|
23
|
+
factory?: Address;
|
|
24
|
+
memoRouter?: Address;
|
|
25
|
+
eurc?: Address;
|
|
26
|
+
/** Deployment block. The no-subgraph listing scans TokenLaunched from here; a public RPC refuses a scan from 0. */
|
|
27
|
+
startBlock?: number;
|
|
28
|
+
}> = {
|
|
29
|
+
5042002: {
|
|
30
|
+
rpcUrl: "https://rpc.testnet.arc.io",
|
|
31
|
+
factory: "0xDA14d24385483c5876e0E170c6135F14Fc3BCEc3",
|
|
32
|
+
memoRouter: "0x32E6e071eA1Fe58DF86C4D3f419a8BAF2118f505",
|
|
33
|
+
eurc: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a",
|
|
34
|
+
startBlock: 61040913,
|
|
35
|
+
},
|
|
36
|
+
5042: {
|
|
37
|
+
rpcUrl: "https://rpc.arc.io",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const DEFAULT_RPC_URL = DEPLOYMENTS[DEFAULT_CHAIN_ID].rpcUrl;
|
|
42
|
+
|
|
43
|
+
export type Config = {
|
|
44
|
+
/** JSON-RPC endpoint for Arc. */
|
|
45
|
+
rpcUrl: string;
|
|
46
|
+
/** 5042002 = Arc Testnet, 5042 = Arc mainnet (id unofficial until Circle publishes it). */
|
|
47
|
+
chainId: number;
|
|
48
|
+
/** ParabolicLaunchFactory. Required for on-chain coin lookups and for building launch transactions. */
|
|
49
|
+
factory?: Address;
|
|
50
|
+
/** ParabolicMemoRouter. Optional; enables the memo-routed (referral-carrying) buy variant. */
|
|
51
|
+
memoRouter?: Address;
|
|
52
|
+
/** Parabolic subgraph GraphQL endpoint. Recommended: listing, holders, volume and stats come from here. */
|
|
53
|
+
subgraphUrl?: string;
|
|
54
|
+
/** ERC-20 EURC on Arc, required only to build EURC-quoted launches. */
|
|
55
|
+
eurc?: Address;
|
|
56
|
+
/** ParabolicHook (Uniswap v4). Optional; read from `factory.memeHook()` when unset. Used to derive pool ids. */
|
|
57
|
+
hook?: Address;
|
|
58
|
+
/** Arc's Memo system contract. */
|
|
59
|
+
arcMemo: Address;
|
|
60
|
+
/** Launch config index on the factory; preset 0 is the adopted $5K / $12,500 curve. */
|
|
61
|
+
launchConfigId: bigint;
|
|
62
|
+
/** First block to scan for `TokenLaunched` logs when listing without a subgraph. */
|
|
63
|
+
factoryStartBlock: bigint;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Environment variables the server reads, with their meaning (also rendered by `parabolic.docs`). */
|
|
67
|
+
export const ENV_VARS: Array<[name: string, meaning: string]> = [
|
|
68
|
+
["PARABOLIC_RPC_URL", `Arc JSON-RPC endpoint (default ${DEFAULT_RPC_URL})`],
|
|
69
|
+
["PARABOLIC_CHAIN_ID", `chain id (default ${DEFAULT_CHAIN_ID}, Arc Testnet)`],
|
|
70
|
+
["PARABOLIC_FACTORY", "ParabolicLaunchFactory address (on-chain lookups, launch transactions, log-scan listing)"],
|
|
71
|
+
["PARABOLIC_MEMO_ROUTER", "ParabolicMemoRouter address (memo-routed buys carrying a referral code; EOA-only)"],
|
|
72
|
+
["PARABOLIC_SUBGRAPH_URL", "Parabolic subgraph endpoint (listing, holders, volume, protocol stats)"],
|
|
73
|
+
["PARABOLIC_EURC", "ERC-20 EURC address (EURC-quoted launches only)"],
|
|
74
|
+
["PARABOLIC_HOOK", "ParabolicHook address (optional; read from the factory when unset)"],
|
|
75
|
+
["PARABOLIC_ARC_MEMO", `Arc Memo system contract (default ${ARC_MEMO_DEFAULT})`],
|
|
76
|
+
["PARABOLIC_LAUNCH_CONFIG_ID", "factory launch config index (default 0)"],
|
|
77
|
+
["PARABOLIC_FACTORY_START_BLOCK", "first block for the TokenLaunched log scan (default 0)"],
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
function addr(name: string, v: string | undefined): Address | undefined {
|
|
81
|
+
if (v === undefined || v.trim() === "") return undefined;
|
|
82
|
+
const s = v.trim();
|
|
83
|
+
if (!isAddress(s, { strict: false })) throw new Error(`${name} is not an address: ${s}`);
|
|
84
|
+
return getAddress(s);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function int(name: string, v: string | undefined, fallback: number): number {
|
|
88
|
+
if (v === undefined || v.trim() === "") return fallback;
|
|
89
|
+
const n = Number(v.trim());
|
|
90
|
+
if (!Number.isInteger(n) || n < 0) throw new Error(`${name} must be a non-negative integer, got ${v}`);
|
|
91
|
+
return n;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function configFromEnv(env: Record<string, string | undefined> = process.env): Config {
|
|
95
|
+
// Chain id first: every other default is read off the deployment for that chain, so setting
|
|
96
|
+
// PARABOLIC_CHAIN_ID=5042 alone moves the RPC and the addresses together instead of leaving a
|
|
97
|
+
// mainnet chain id pointed at the testnet endpoint.
|
|
98
|
+
const chainId = int("PARABOLIC_CHAIN_ID", env.PARABOLIC_CHAIN_ID, DEFAULT_CHAIN_ID);
|
|
99
|
+
const known = DEPLOYMENTS[chainId];
|
|
100
|
+
return {
|
|
101
|
+
rpcUrl: env.PARABOLIC_RPC_URL?.trim() || known?.rpcUrl || DEFAULT_RPC_URL,
|
|
102
|
+
chainId,
|
|
103
|
+
factory: addr("PARABOLIC_FACTORY", env.PARABOLIC_FACTORY) ?? known?.factory,
|
|
104
|
+
memoRouter: addr("PARABOLIC_MEMO_ROUTER", env.PARABOLIC_MEMO_ROUTER) ?? known?.memoRouter,
|
|
105
|
+
subgraphUrl: env.PARABOLIC_SUBGRAPH_URL?.trim() || undefined,
|
|
106
|
+
eurc: addr("PARABOLIC_EURC", env.PARABOLIC_EURC) ?? known?.eurc,
|
|
107
|
+
hook: addr("PARABOLIC_HOOK", env.PARABOLIC_HOOK),
|
|
108
|
+
arcMemo: addr("PARABOLIC_ARC_MEMO", env.PARABOLIC_ARC_MEMO) ?? ARC_MEMO_DEFAULT,
|
|
109
|
+
launchConfigId: BigInt(int("PARABOLIC_LAUNCH_CONFIG_ID", env.PARABOLIC_LAUNCH_CONFIG_ID, 0)),
|
|
110
|
+
factoryStartBlock: BigInt(int("PARABOLIC_FACTORY_START_BLOCK", env.PARABOLIC_FACTORY_START_BLOCK, known?.startBlock ?? 0)),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const chainName = (chainId: number): string => (chainId === 5042002 ? "Arc Testnet" : chainId === 5042 ? "Arc" : `chain ${chainId}`);
|
|
115
|
+
|
|
116
|
+
/** Arcscan base URL for the chain, undefined where there is no explorer (Anvil). */
|
|
117
|
+
export const explorerBase = (chainId: number): string | undefined =>
|
|
118
|
+
chainId === 5042002 ? "https://testnet.arcscan.app" : chainId === 5042 ? "https://arcscan.app" : undefined;
|
|
119
|
+
|
|
120
|
+
export const explorerUrl = (chainId: number, kind: "address" | "tx", value: string): string | undefined => {
|
|
121
|
+
const base = explorerBase(chainId);
|
|
122
|
+
return base ? `${base}/${kind}/${value}` : undefined;
|
|
123
|
+
};
|