hookit-mcp 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/index.js +384 -103
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Lets an AI agent launch a coin on [hookit.fun](https://www.hookit.fun) (Ink) by
|
|
|
18
18
|
|
|
19
19
|
The key signs on this machine and is never sent anywhere. Give it a wallet funded for what the agent should do and nothing else.
|
|
20
20
|
|
|
21
|
-
Optional env: `HOOKIT_API_URL` (default `https://www.hookit.fun`), `HOOKIT_RPC_URL` (default Ink public RPC), `HOOKIT_MAX_BUY_ETH` (largest single `buy_token` paid in ETH, default `0.5`), `HOOKIT_MAX_BUY_USD` (largest single `buy_token` paid in a token, in dollars, default `1000`), `HOOKIT_SEND_ETH_TO` (comma-separated addresses `send_eth` may pay, for example your Kraken deposit address on Ink; unset, any address), `HOOKIT_MEMORY_EVOLVE` / `HOOKIT_MEMORY_LENS` / `HOOKIT_MEMORY_FEE1` / `HOOKIT_MEMORY_FEE2` (override Memory on Evolve's pinned addresses), `HOOKIT_VELOCITY_FEE_PLUGIN` / `HOOKIT_VELOCITY_EVOLVE` / `HOOKIT_VELOCITY_EVOLVE_FEE1` / `HOOKIT_VELOCITY_EVOLVE_FEE2` (override Velocity Fee's pinned addresses), `HOOKIT_MASTER_HOOK_D` (overrides the pinned hook D, the Master hook new launches go to; coins keep trading on their own PoolKey's hook either way, this only names it in `coin_info`).
|
|
21
|
+
Optional env: `HOOKIT_API_URL` (default `https://www.hookit.fun`), `HOOKIT_RPC_URL` (default Ink public RPC), `HOOKIT_RELAY_API_URL` (default `https://api.relay.link`) and `HOOKIT_RELAY_API_KEY` (a Relay API key, for higher quote limits), `HOOKIT_MAX_BUY_ETH` (largest single `buy_token` paid in ETH, default `0.5`), `HOOKIT_MAX_BUY_USD` (largest single `buy_token` paid in a token, in dollars, default `1000`), `HOOKIT_SEND_ETH_TO` (comma-separated addresses `send_eth` may pay, for example your Kraken deposit address on Ink; unset, any address), `HOOKIT_MEMORY_EVOLVE` / `HOOKIT_MEMORY_LENS` / `HOOKIT_MEMORY_FEE1` / `HOOKIT_MEMORY_FEE2` (override Memory on Evolve's pinned addresses), `HOOKIT_VELOCITY_FEE_PLUGIN` / `HOOKIT_VELOCITY_EVOLVE` / `HOOKIT_VELOCITY_EVOLVE_FEE1` / `HOOKIT_VELOCITY_EVOLVE_FEE2` (override Velocity Fee's pinned addresses), `HOOKIT_MASTER_HOOK_D` (overrides the pinned hook D, the Master hook new launches go to; coins keep trading on their own PoolKey's hook either way, this only names it in `coin_info`). `HOOKIT_EVOLVE_PLUGIN` (overrides the pinned Evolve v2 plugin, where new Evolve coins launch) and `HOOKIT_EVOLVE_V2_MEMORY_EVOLVE` / `_MEMORY_LENS` / `_MEMORY_FEE1` / `_MEMORY_FEE2` / `_VELOCITY_EVOLVE` / `_VELOCITY_EVOLVE_FEE1` / `_VELOCITY_EVOLVE_FEE2` (override Memory's and Velocity's Evolve v2 deployments; the `HOOKIT_MEMORY_*` and `HOOKIT_VELOCITY_EVOLVE*` ones are Evolve v1's).
|
|
22
22
|
|
|
23
23
|
Coin names, tickers and descriptions are written by whoever launched the coin. The server hands them to the agent as untrusted data: `coin_info` withholds a description that carries a link, a handle, an address or a tool name, names and tickers that read as instructions are replaced, and every tool that signs is annotated so your MCP client can ask you before each call. Keep that confirmation on for a funded wallet, and set `HOOKIT_SEND_ETH_TO` if the agent only ever cashes out to one place.
|
|
24
24
|
|
|
@@ -65,15 +65,15 @@ Paying or receiving a token approves exactly that amount to the contract that tr
|
|
|
65
65
|
|
|
66
66
|
### Paying with something the coin does not trade in (Relay)
|
|
67
67
|
|
|
68
|
-
`coin_info` lists what a coin trades in (`tradeIn`). When `payWith` or `receive` is not one of them, the server swaps through [Relay](https://relay.link) on Ink, the same route hookit.fun's own swap box uses: it asks `https://
|
|
68
|
+
`coin_info` lists what a coin trades in (`tradeIn`). When `payWith` or `receive` is not one of them, the server swaps through [Relay](https://relay.link) on Ink, the same route hookit.fun's own swap box uses: it asks Relay's API (`https://api.relay.link`) for the swap directly, so hookit.fun never shapes the calls the wallet signs, then signs Relay's transactions itself. A buy swaps first and buys with what Relay returned; a sale sells for the coin's pair first, then swaps the proceeds. `dryRun` shows both legs.
|
|
69
69
|
|
|
70
|
-
Before signing, Relay's transactions are checked twice. First their shape: Ink only, calls only to Relay's Ink contracts (router, approval proxy, receiver, multicaller, depository), an approval only of Relay's approval proxy (replaced by an exact approval of the amount), and no more ETH than the amount. Then their effect: the approval and every call run together in one `eth_simulateV1` from the wallet, and the swap is refused unless the wallet would give at most the amount, get at least the minimum, and lose nothing else. That minimum is never the quote's word alone: it is at least the market price (Chainlink's ETH/USD as Factory C reads it, a stock's USDG pool on Ink, else hookit.fun's price) less the slippage and 5% for Relay's fees and price impact, and a quote under that floor is refused. Anything else is refused and nothing is sent. No allowance to Relay is left behind, even when a swap fails. One Relay swap converts at most the per-buy cap (`HOOKIT_MAX_BUY_ETH` / `HOOKIT_MAX_BUY_USD`), with at most 3% slippage. Relay swaps ETH or USDG with each other, kBTC and the tokenized stocks. The RPC in `HOOKIT_RPC_URL` must support `eth_simulateV1` (Ink's public RPC does).
|
|
70
|
+
Before signing, Relay's transactions are checked twice. First their shape: Ink only, calls only to Relay's Ink contracts (router, approval proxy, receiver, multicaller, depository), an approval only of Relay's approval proxy (replaced by an exact approval of the amount), and no more ETH than the amount. Then their effect: the approval and every call run together in one validated `eth_simulateV1` from the wallet, priced with the same fee fields the swap is then signed with (so a called contract cannot tell the simulation by a zero gas price), and the swap is refused unless the wallet would give at most the amount, get at least the minimum, and lose nothing else. That minimum is never the quote's word alone: it is at least the market price (Chainlink's ETH/USD as Factory C reads it, a stock's USDG pool on Ink, else hookit.fun's price) less the slippage and 5% for Relay's fees and price impact, and a quote under that floor is refused. Anything else is refused and nothing is sent. No allowance to Relay is left behind, even when a swap fails. One Relay swap converts at most the per-buy cap (`HOOKIT_MAX_BUY_ETH` / `HOOKIT_MAX_BUY_USD`), with at most 3% slippage. Relay swaps ETH or USDG with each other, kBTC and the tokenized stocks. The RPC in `HOOKIT_RPC_URL` must support `eth_simulateV1` (Ink's public RPC does).
|
|
71
71
|
|
|
72
72
|
When the first leg went through and the second did not, the reply is `partial: true` with what the wallet now holds: do not retry the call, or the first leg runs twice.
|
|
73
73
|
|
|
74
74
|
### What the server trusts
|
|
75
75
|
|
|
76
|
-
The contracts it approves, pays or quotes from (Factory C and the Master factories, CreatorPayoutRouterC, HookitSwapRouter, the aggregator's RouteConfigC, Uniswap's V4Quoter, USDG, FloorLoanPlugin, the Gated Buy plugin, the Evolve plugin, NFT Strategy and Memory on Evolve, ModuleRegistry, Velocity Fee and Velocity on Evolve, and the Agent Pass) are pinned in the server, not read from hookit.fun: a Hookit redeploy needs a new hookit-mcp release. Since 0.7.2 the payout router is the M-01 CreatorPayoutRouterC `0x3e8E8a1F3D2279cB59042B029637c7096EeC2302` and `coin_info` names hook D `0x8a29B2c508007707bbFF1814386AA968bbeCbFff` (HookRegistry's default for new launches since 2026-10-16) next to hook C `0x4b533C432516FC1Be4b0bAEFe6BFF0942AD87FfF` (earlier coins). A launch prepared by hookit.fun is only signed when it goes to Factory C or its payout router with at most the launch fee plus `HOOKIT_MAX_BUY_ETH
|
|
76
|
+
The contracts it approves, pays or quotes from (Factory C and the Master factories, CreatorPayoutRouterC, HookitSwapRouter, the aggregator's RouteConfigC, Uniswap's V4Quoter, USDG, FloorLoanPlugin, the Gated Buy plugin, the Evolve plugin, NFT Strategy and Memory on Evolve, ModuleRegistry, Velocity Fee and Velocity on Evolve, and the Agent Pass) are pinned in the server, not read from hookit.fun: a Hookit redeploy needs a new hookit-mcp release. Since 0.7.2 the payout router is the M-01 CreatorPayoutRouterC `0x3e8E8a1F3D2279cB59042B029637c7096EeC2302` and `coin_info` names hook D `0x8a29B2c508007707bbFF1814386AA968bbeCbFff` (HookRegistry's default for new launches since 2026-10-16) next to hook C `0x4b533C432516FC1Be4b0bAEFe6BFF0942AD87FfF` (earlier coins). A launch prepared by hookit.fun is only signed when it goes to Factory C or its payout router with at most the launch fee plus `HOOKIT_MAX_BUY_ETH`, and when its calldata pays the creator fees to the `payout` asked: this wallet through Factory C, or the router's provider, account id and wallet. A TikTok handle is hashed by the server itself and a GitHub handle looked up on api.github.com before preparing; an X handle without `accountId` takes hookit.fun's lookup, and the result's `payout` shows the account id the transaction pays. Tools that sign run one at a time, even when an agent calls several at once.
|
|
77
77
|
|
|
78
78
|
## Evolve: the wallet that launches decides
|
|
79
79
|
|
|
@@ -89,6 +89,7 @@ evolve_decide({ "token": "0x…", "feePct": 2, "reason": "Heavy selling: a highe
|
|
|
89
89
|
- A setting that raises neither the fee nor the creator share applies at once. A raise is announced on the coin page and applies after the launch's notice (1 h); a new decision replaces it, `cancel: true` drops it.
|
|
90
90
|
- The reason is optional and public on chain: plain text, no links, e-mails, domains or @handles (refused before sending).
|
|
91
91
|
- The decider cannot change the code, the liquidity or the name, and cannot redirect money: the caps and the sinks are fixed at launch. A lost key leaves the coin on its live setting.
|
|
92
|
+
- Two Evolve versions (0.8.0): new Evolve coins launch on Evolve v2, the current module, read from the ModuleRegistry by the pinned v2 plugin's address (Evolve v1, module 5, is deprecated: no new launch on it). Coins launched on v1 keep working: every `evolve_*`, `memory_*` and `velocity_*` tool finds the plugin that knows the coin and uses that version's catalog and hooks. Until v2 has code at its address, the server launches on v1. A launch is signed only when its Evolve slot is the current module with exactly the 288 bytes of its settings: hookit-mcp never plugs hooks at launch (it plugs them with `evolve_plug`, `memory_set` and `velocity_set`). NFT Strategy is on Evolve v1 coins only for now.
|
|
92
93
|
|
|
93
94
|
## Evolve: hooks added later
|
|
94
95
|
|
|
@@ -211,7 +212,7 @@ launch_token({ "name": "…", "symbol": "…", "hookTaxPct": 3, "builderHooks":
|
|
|
211
212
|
|
|
212
213
|
- `deploy_hook` takes the Foundry artifact (`out/<File>.sol/<Contract>.json` after `forge build`, as an absolute path: the server runs in its own folder) and refuses anything that is not a deployable plugin: built on the SDK's `HookPluginBase`, one `address` constructor, no linked libraries, code within Ink's limits. It reads regular files only, up to 16 MiB, and never quotes a file it cannot use. The constructor argument is always the ModuleRegistry pinned here, never one the agent or a website names.
|
|
213
214
|
- `submit_hook` sends the manifest as the SDK wrote it and cleans the card exactly as the Workshop's form does, then signs the Workshop's submit message. Before signing, it checks that the manifest's codehash is the code deployed at its address and refuses a submission hookit.fun would refuse (a live plugin, or one that already has a submission in review or approved); it warns when the explorer names another deployer.
|
|
214
|
-
-
|
|
215
|
+
- Hookit's review may ask for changes (a change is a new deployment and a new submission), and it can halt a plugin at any time.
|
|
215
216
|
- Both signing tools are dry runs unless `dryRun` is `false`: deploy or submit only once the user said to.
|
|
216
217
|
|
|
217
218
|
## Backed Floor: redeem and borrow
|
package/index.js
CHANGED
|
@@ -44,7 +44,7 @@ const API = (process.env.HOOKIT_API_URL || "https://www.hookit.fun").replace(/\/
|
|
|
44
44
|
const INK_PUBLIC_RPC = "https://rpc-gel.inkonchain.com";
|
|
45
45
|
const RPC = process.env.HOOKIT_RPC_URL || INK_PUBLIC_RPC;
|
|
46
46
|
const KEY = process.env.HOOKIT_PRIVATE_KEY?.trim();
|
|
47
|
-
const VERSION = "0.
|
|
47
|
+
const VERSION = "0.8.0";
|
|
48
48
|
const EXPLORER = "https://explorer.inkonchain.com";
|
|
49
49
|
/**
|
|
50
50
|
* The Ink deployments this server signs against. Pinned here, never taken from hookit.fun's catalog: a compromised
|
|
@@ -74,7 +74,12 @@ const PINNED = {
|
|
|
74
74
|
aggregator: "0x6518b4b2D77F8351F8cE7C9BA1ca7E6D8631D210", // BalancedAggregatorC (USDG in / out on v2 coins)
|
|
75
75
|
loanPlugin: "0xA9e9347175B1d128478D0DdA475dE5a11C33DBB4", // FloorLoanPlugin
|
|
76
76
|
gatedBuyPlugin: "0x4af18ddf7Ccbb0d30DE397B8ebdc873f98BFA9f3", // GatedBuyPlugin
|
|
77
|
-
|
|
77
|
+
// ---- Evolve (spec/v2/plugins/evolve.md): v2 (§10.6, launch plugs and relays) takes every new launch; v1 keeps the
|
|
78
|
+
// coins launched on it (deprecated in the ModuleRegistry by the last v2 list, c-evolve-v1-deprecate: no new launch,
|
|
79
|
+
// its coins keep running). Each coin's tools use the plugin that knows it (`evolvePluginOf`). Until v2 has code at
|
|
80
|
+
// its address, the current Evolve is v1. HOOKIT_EVOLVE_PLUGIN overrides the v2 address.
|
|
81
|
+
evolvePlugin: "0x9E648Cbb0cf76197056aE5f61fdD9b6b95cdA1c4", // EvolvePlugin v2 (CreateX label hookit.v2.c1:c.EvolvePlugin.v2, list c-evolve-57073-58845088: module 28, EvolveExtensions 0x57c574e2764040cb1e325E591bC3475b3c035D75, relays 0x37292dEBc156Aa3Df8aD5311d1F7b41eA5198e28 / 0x3D855467718c16F106956C740E2e77eD70798D6A as modules 29 / 30); the module id is read from the ModuleRegistry
|
|
82
|
+
evolvePluginV1: "0xC78B0195Fa8b3DeECF5f6CCe1F6F2A7902588112", // EvolvePlugin v1 (module 5; creates EvolveExtensions 0x0ef655FEC85BF298a8154c8A2f4DA5CB4Dd4aBcE), tx list c-evolve-57073-58022489
|
|
78
83
|
// NftStrategyEvolve f3 (KNOWN-ISSUES R9a, list c-evolve-nft-strategy-f3): Evolve hook id 8, raffle
|
|
79
84
|
// 0x49eF571af836DD777C223Bcf90Fc5A4F8441B074. The first version 0x5C731E518dB36a542DF9907DEb37967a39dAAF0d (id 1) is
|
|
80
85
|
// halted and was never plugged by any coin (no Announced on EvolveExtensions, 0 raffles), so nothing reads it any more.
|
|
@@ -104,6 +109,19 @@ const PINNED = {
|
|
|
104
109
|
velocityEvolveFee2: "0x48C6b65554cB87cB04855426f7955bD355Ba6209", // VelocityEvolveFee rank 2 (asks the 3rd percent)
|
|
105
110
|
agentPass: "0xe847CCD9EcD9889c39B381C83B33e0251759Cf67", // HookitAgentPass
|
|
106
111
|
agentValidator: "0x7A9365784b438D327D468b717dd644fd0fB09fa7", // HookitAgentValidator (ERC-8004)
|
|
112
|
+
// ---- The Evolve v2 deployments of Memory and Velocity on Evolve (lists c-evolve-memory-57073-58845088, catalog ids
|
|
113
|
+
// 5-7, and c-velocity-evolve-57073-58845088, ids 0-2, against the v2 catalog): the memory* / velocityEvolve* pins above are v1's and serve the v1 coins. HOOKIT_EVOLVE_V2_MEMORY_EVOLVE,
|
|
114
|
+
// _MEMORY_LENS, _MEMORY_FEE1, _MEMORY_FEE2, _VELOCITY_EVOLVE, _VELOCITY_EVOLVE_FEE1, _VELOCITY_EVOLVE_FEE2 override them;
|
|
115
|
+
// nothing is used until code is there and the contracts point at the v2 plugin and catalog.
|
|
116
|
+
evolveV2: {
|
|
117
|
+
memoryEvolve: "0x68B0dE60217DC4Ca971eDc23B0c9B4B6D3A710A1",
|
|
118
|
+
memoryLens: "0xD33EAe11B39c6348e6e803Ee8a43176AeD727924",
|
|
119
|
+
memoryFee1: "0x22228e62EfEaC31da195257b4D432A0CcC707942",
|
|
120
|
+
memoryFee2: "0x76eAdF20b3Bf3e7e172A875C866bc4C796BC8828",
|
|
121
|
+
velocityEvolve: "0x0BdE0e84a8b1Faee372616AE069c1b85D74386CC",
|
|
122
|
+
velocityEvolveFee1: "0xAea32E49E128eE3472fe4CD962515Ec96f758b1E",
|
|
123
|
+
velocityEvolveFee2: "0xF73b51849495788773A1433C9980EAC1983F8cF3",
|
|
124
|
+
},
|
|
107
125
|
};
|
|
108
126
|
/** Tickers payWith / receive accept, besides "eth" and "usdg" (the catalog's list is only a fallback). */
|
|
109
127
|
const KNOWN_ASSETS = {
|
|
@@ -937,7 +955,7 @@ async function convertAtUsd(c, from, to, amount) {
|
|
|
937
955
|
return Number.isFinite(v) && v > 0 ? parseUnits(v.toFixed(to.decimals), to.decimals) : null;
|
|
938
956
|
}
|
|
939
957
|
|
|
940
|
-
/** Whether
|
|
958
|
+
/** Whether a Relay leg may swap `from` into `to` on Ink: one side must be ETH or USDG, as on hookit.fun. */
|
|
941
959
|
function relayable(c, from, to) {
|
|
942
960
|
if (same(from, to)) return false;
|
|
943
961
|
const funding = (a) => same(a, zeroAddress) || same(a, at(c, "usdg"));
|
|
@@ -1178,6 +1196,59 @@ function feeOf(receipt) {
|
|
|
1178
1196
|
|
|
1179
1197
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1180
1198
|
|
|
1199
|
+
/** Relay's API, asked directly: hookit.fun never shapes the calls this wallet signs. */
|
|
1200
|
+
const RELAY_API = (process.env.HOOKIT_RELAY_API_URL || "https://api.relay.link").replace(/\/$/, "");
|
|
1201
|
+
const RELAY_API_KEY = process.env.HOOKIT_RELAY_API_KEY?.trim();
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* An Ink EXACT_INPUT quote for this wallet from Relay's API: { body } with the quote's requestId, amountOut,
|
|
1205
|
+
* minimumAmount, impactPercent and calls (every transaction step's items), or { error, status }.
|
|
1206
|
+
*/
|
|
1207
|
+
async function relayFetchQuote(from, to, amount, slippageBps) {
|
|
1208
|
+
let res;
|
|
1209
|
+
try {
|
|
1210
|
+
res = await fetch(`${RELAY_API}/quote/v2`, {
|
|
1211
|
+
method: "POST",
|
|
1212
|
+
headers: { "content-type": "application/json", ...(RELAY_API_KEY ? { "x-api-key": RELAY_API_KEY } : {}) },
|
|
1213
|
+
body: JSON.stringify({
|
|
1214
|
+
user: account.address,
|
|
1215
|
+
recipient: account.address,
|
|
1216
|
+
originChainId: ink.id,
|
|
1217
|
+
destinationChainId: ink.id,
|
|
1218
|
+
originCurrency: from,
|
|
1219
|
+
destinationCurrency: to,
|
|
1220
|
+
amount: amount.toString(),
|
|
1221
|
+
tradeType: "EXACT_INPUT",
|
|
1222
|
+
slippageTolerance: String(slippageBps),
|
|
1223
|
+
}),
|
|
1224
|
+
});
|
|
1225
|
+
} catch {
|
|
1226
|
+
return { error: "Relay's quote API did not answer. Retry later.", status: 0 };
|
|
1227
|
+
}
|
|
1228
|
+
const payload = await res.json().catch(() => null);
|
|
1229
|
+
if (!res.ok || !payload || payload.errorCode) {
|
|
1230
|
+
return { error: typeof payload?.message === "string" ? payload.message.slice(0, 200) : `Relay answered ${res.status}`, status: res.status };
|
|
1231
|
+
}
|
|
1232
|
+
const steps = Array.isArray(payload.steps) ? payload.steps : [];
|
|
1233
|
+
// A signature step (permit, authorize) cannot be sent as a transaction.
|
|
1234
|
+
if (steps.some((step) => step?.kind != null && step.kind !== "transaction")) return { error: "the route needs a signature", status: 502 };
|
|
1235
|
+
const calls = steps
|
|
1236
|
+
.flatMap((step) => (Array.isArray(step?.items) ? step.items : []))
|
|
1237
|
+
.map((item) => item?.data)
|
|
1238
|
+
.filter((data) => data && typeof data === "object")
|
|
1239
|
+
.map((data) => ({ chainId: Number(data.chainId), to: data.to, data: data.data, value: String(data.value ?? "0") }));
|
|
1240
|
+
const out = payload.details?.currencyOut;
|
|
1241
|
+
return {
|
|
1242
|
+
body: {
|
|
1243
|
+
requestId: payload.requestId ?? "",
|
|
1244
|
+
amountOut: out?.amount,
|
|
1245
|
+
minimumAmount: /^\d+$/.test(out?.minimumAmount ?? "") ? out.minimumAmount : out?.amount,
|
|
1246
|
+
impactPercent: payload.details?.totalImpact?.percent ?? null,
|
|
1247
|
+
calls,
|
|
1248
|
+
},
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1181
1252
|
/**
|
|
1182
1253
|
* Relay's contracts on Ink (api.relay.link/chains): a conversion only ever calls one of these, or approves one of them
|
|
1183
1254
|
* on the token it spends. A quote that asks for anything else is refused before signing.
|
|
@@ -1231,37 +1302,19 @@ async function relayFairOut(inMeta, outMeta, amount) {
|
|
|
1231
1302
|
}
|
|
1232
1303
|
|
|
1233
1304
|
/**
|
|
1234
|
-
* A Relay quote to swap `amount` of `from` into `to` on Ink for this wallet,
|
|
1235
|
-
*
|
|
1305
|
+
* A Relay quote to swap `amount` of `from` into `to` on Ink for this wallet, from Relay's API:
|
|
1306
|
+
* { quote: { requestId, out, min, impact, calls } } or { error }.
|
|
1236
1307
|
*/
|
|
1237
1308
|
async function relayQuote(from, to, amount, slippageBps) {
|
|
1238
1309
|
const [inMeta, outMeta] = await Promise.all([assetMeta(from), assetMeta(to)]);
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
headers: { "content-type": "application/json" },
|
|
1244
|
-
body: JSON.stringify({
|
|
1245
|
-
user: account.address,
|
|
1246
|
-
originChainId: ink.id,
|
|
1247
|
-
destinationChainId: ink.id,
|
|
1248
|
-
originCurrency: from,
|
|
1249
|
-
destinationCurrency: to,
|
|
1250
|
-
amount: amount.toString(),
|
|
1251
|
-
slippageBps,
|
|
1252
|
-
}),
|
|
1253
|
-
});
|
|
1254
|
-
} catch {
|
|
1255
|
-
return { error: "hookit.fun's Relay quote did not answer. Retry later." };
|
|
1256
|
-
}
|
|
1257
|
-
const body = await res.json().catch(() => null);
|
|
1258
|
-
if (!res.ok) {
|
|
1259
|
-
const why = body?.error ?? `hookit.fun answered ${res.status}`;
|
|
1260
|
-
if (why === "Unsupported Relay pair") return { error: `Relay does not swap ${inMeta.symbol} into ${outMeta.symbol} through hookit.fun.`, unsupported: true };
|
|
1261
|
-
return { error: `Relay has no ${inMeta.symbol} to ${outMeta.symbol} route right now (${why}).` };
|
|
1310
|
+
const fetched = await relayFetchQuote(from, to, amount, slippageBps);
|
|
1311
|
+
if (fetched.error) {
|
|
1312
|
+
if (fetched.status === 0) return { error: fetched.error };
|
|
1313
|
+
return { error: `Relay has no ${inMeta.symbol} to ${outMeta.symbol} route right now (${fetched.error}).` };
|
|
1262
1314
|
}
|
|
1315
|
+
const { body } = fetched;
|
|
1263
1316
|
if (!/^\d+$/.test(body?.amountOut ?? "") || !/^\d+$/.test(body?.minimumAmount ?? "") || !Array.isArray(body?.calls)) {
|
|
1264
|
-
return { error: "
|
|
1317
|
+
return { error: "Relay's quote came back malformed. Nothing was sent." };
|
|
1265
1318
|
}
|
|
1266
1319
|
const out = BigInt(body.amountOut);
|
|
1267
1320
|
if (out <= 0n) return { error: `Relay would return no ${outMeta.symbol}.` };
|
|
@@ -1343,24 +1396,28 @@ const ETH_TRACE = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
|
|
1343
1396
|
/**
|
|
1344
1397
|
* Runs the approval and Relay's transactions together in one eth_simulateV1 from this wallet (nothing is sent) and
|
|
1345
1398
|
* checks what the wallet would send and get: at most `amount` of `from` out, at least `min` of `to` in, nothing else
|
|
1346
|
-
* out. Relay contracts run arbitrary calls, so this is what stops a forged quote from paying someone else.
|
|
1347
|
-
*
|
|
1399
|
+
* out. Relay contracts run arbitrary calls, so this is what stops a forged quote from paying someone else. The
|
|
1400
|
+
* simulation is validated and priced with `fees`, the fee fields the swap is then signed with, so a called contract
|
|
1401
|
+
* cannot tell it from the real transaction by a zero gas price or base fee. null when sound, else why not.
|
|
1348
1402
|
*/
|
|
1349
|
-
async function relaySimulate(from, to, amount, min, plan) {
|
|
1403
|
+
async function relaySimulate(from, to, amount, min, plan, fees) {
|
|
1350
1404
|
const wallet = account.address;
|
|
1405
|
+
const priced = { maxFeePerGas: toHex(fees.maxFeePerGas), maxPriorityFeePerGas: toHex(fees.maxPriorityFeePerGas) };
|
|
1351
1406
|
const calls = [];
|
|
1352
1407
|
if (from !== zeroAddress && plan.spender) {
|
|
1353
|
-
calls.push({ from: wallet, to: from, data: encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [plan.spender, amount] }) });
|
|
1408
|
+
calls.push({ from: wallet, to: from, data: encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [plan.spender, amount] }), ...priced });
|
|
1354
1409
|
}
|
|
1355
|
-
for (const tx of plan.txs) calls.push({ from: wallet, to: tx.to, data: tx.data, value: toHex(tx.value) });
|
|
1410
|
+
for (const tx of plan.txs) calls.push({ from: wallet, to: tx.to, data: tx.data, value: toHex(tx.value), ...priced });
|
|
1356
1411
|
let result;
|
|
1357
1412
|
try {
|
|
1358
1413
|
result = await publicClient.request({
|
|
1359
1414
|
method: "eth_simulateV1",
|
|
1360
|
-
params: [{ blockStateCalls: [{ calls }], traceTransfers: true, validation:
|
|
1415
|
+
params: [{ blockStateCalls: [{ calls }], traceTransfers: true, validation: true }, "latest"],
|
|
1361
1416
|
});
|
|
1362
1417
|
} catch (err) {
|
|
1363
|
-
|
|
1418
|
+
const why = shortError(err);
|
|
1419
|
+
if (/insufficient funds|nonce|fee cap|gas limit/i.test(why)) return `Relay's swap fails when simulated (${why})`;
|
|
1420
|
+
return `this RPC cannot simulate Relay's transactions (eth_simulateV1: ${why}). Set HOOKIT_RPC_URL to an Ink RPC that supports it, such as https://rpc-gel.inkonchain.com.`;
|
|
1364
1421
|
}
|
|
1365
1422
|
const net = new Map();
|
|
1366
1423
|
const add = (asset, v) => net.set(asset, (net.get(asset) ?? 0n) + v);
|
|
@@ -1384,12 +1441,18 @@ async function relaySimulate(from, to, amount, min, plan) {
|
|
|
1384
1441
|
return null;
|
|
1385
1442
|
}
|
|
1386
1443
|
|
|
1387
|
-
/**
|
|
1444
|
+
/**
|
|
1445
|
+
* The structural and the simulated check of a Relay quote: { plan, fees } or { error } (refused, nothing sent). `fees`
|
|
1446
|
+
* are the fee fields the check simulated with; the swap must be signed with them.
|
|
1447
|
+
*/
|
|
1388
1448
|
async function relayCheck(from, to, amount, quote) {
|
|
1389
1449
|
const plan = relayPlan(quote.calls, from, amount, quote.inMeta.symbol);
|
|
1390
1450
|
if (plan.error) return plan;
|
|
1391
|
-
const
|
|
1392
|
-
|
|
1451
|
+
const estimated = await publicClient.estimateFeesPerGas().catch(() => null);
|
|
1452
|
+
if (!estimated?.maxFeePerGas) return { error: "Could not read Ink's gas price, so Relay's swap cannot be simulated. Nothing was sent." };
|
|
1453
|
+
const fees = { maxFeePerGas: estimated.maxFeePerGas, maxPriorityFeePerGas: estimated.maxPriorityFeePerGas ?? 0n };
|
|
1454
|
+
const why = await relaySimulate(from, to, amount, quote.min, plan, fees);
|
|
1455
|
+
return why ? { error: `Refused Relay's transactions: ${why}. Nothing was sent.` } : { plan, fees };
|
|
1393
1456
|
}
|
|
1394
1457
|
|
|
1395
1458
|
/** Sets this wallet's allowance of `asset` to `spender` to exactly `amount` (0 revokes). { hash } or { error }. */
|
|
@@ -1436,7 +1499,7 @@ async function relayStatus(requestId) {
|
|
|
1436
1499
|
async function relayConvert(from, to, amount, quote) {
|
|
1437
1500
|
const checked = await relayCheck(from, to, amount, quote);
|
|
1438
1501
|
if (checked.error) return { error: checked.error, txs: [] };
|
|
1439
|
-
const { plan } = checked;
|
|
1502
|
+
const { plan, fees: feeFields } = checked;
|
|
1440
1503
|
const txs = [];
|
|
1441
1504
|
const receipts = [];
|
|
1442
1505
|
const approves = from !== zeroAddress && plan.spender;
|
|
@@ -1452,13 +1515,14 @@ async function relayConvert(from, to, amount, quote) {
|
|
|
1452
1515
|
for (const tx of plan.txs) {
|
|
1453
1516
|
let gas;
|
|
1454
1517
|
try {
|
|
1455
|
-
await publicClient.call({ account, ...tx });
|
|
1456
|
-
const estimated = await publicClient.estimateGas({ account, ...tx });
|
|
1518
|
+
await publicClient.call({ account, ...tx, ...feeFields });
|
|
1519
|
+
const estimated = await publicClient.estimateGas({ account, ...tx, ...feeFields });
|
|
1457
1520
|
gas = estimated + estimated / 3n;
|
|
1458
1521
|
} catch (err) {
|
|
1459
1522
|
return { error: `Relay's swap would fail: ${shortError(err)}`, txs };
|
|
1460
1523
|
}
|
|
1461
|
-
|
|
1524
|
+
// Signed with the fee fields the swap was simulated with.
|
|
1525
|
+
const hash = await walletClient.sendTransaction({ ...tx, ...feeFields, gas: gas > MAX_TX_GAS ? MAX_TX_GAS : gas });
|
|
1462
1526
|
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
1463
1527
|
txs.push(hash);
|
|
1464
1528
|
receipts.push(receipt);
|
|
@@ -1749,6 +1813,87 @@ function launchRefusal(prepared) {
|
|
|
1749
1813
|
return null;
|
|
1750
1814
|
}
|
|
1751
1815
|
|
|
1816
|
+
/** CreatorPayoutRouterC's provider ids (web/src/lib/social-payout.ts). */
|
|
1817
|
+
const PAYOUT_PROVIDER = { wallet: 0, github: 1, x: 2, tiktok: 3 };
|
|
1818
|
+
|
|
1819
|
+
/** "@Name", "name" or a profile URL → "name", as hookit.fun reads a payout handle; null when not one. */
|
|
1820
|
+
function payoutHandle(kind, raw) {
|
|
1821
|
+
if (typeof raw !== "string" || raw.length > 200) return null;
|
|
1822
|
+
const handle = raw
|
|
1823
|
+
.trim()
|
|
1824
|
+
.replace(/^https?:\/\/(www\.)?(github\.com|x\.com|twitter\.com|tiktok\.com)\//i, "")
|
|
1825
|
+
.replace(/^@/, "")
|
|
1826
|
+
.replace(/[/?#].*$/, "");
|
|
1827
|
+
const pattern = { github: /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/, x: /^[A-Za-z0-9_]{1,15}$/, tiktok: /^[A-Za-z0-9_.]{2,24}$/ }[kind];
|
|
1828
|
+
return pattern?.test(handle) ? handle : null;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
/** A GitHub account's numeric id from api.github.com (not hookit.fun), so a forged lookup cannot pick the payee. */
|
|
1832
|
+
async function githubUserId(handle) {
|
|
1833
|
+
try {
|
|
1834
|
+
const res = await fetch(`https://api.github.com/users/${encodeURIComponent(handle)}`, {
|
|
1835
|
+
headers: { accept: "application/vnd.github+json", "user-agent": "hookit-mcp" },
|
|
1836
|
+
});
|
|
1837
|
+
const body = await res.json().catch(() => null);
|
|
1838
|
+
return res.ok && Number.isSafeInteger(body?.id) && body.id > 0 ? String(body.id) : null;
|
|
1839
|
+
} catch {
|
|
1840
|
+
return null;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
/**
|
|
1845
|
+
* Who the launch hookit.fun prepared pays the creator fees to, read from its calldata: { kind: "self" } through Factory
|
|
1846
|
+
* C, else the payout router's { provider, accountId, wallet }; null when the router call does not decode.
|
|
1847
|
+
*/
|
|
1848
|
+
function launchPayoutOf(prepared) {
|
|
1849
|
+
if (same(prepared.tx?.to, PINNED.factory)) return { kind: "self" };
|
|
1850
|
+
try {
|
|
1851
|
+
const [, provider, accountId, wallet] = decodeFunctionData({ abi: payoutRouterLaunchAbi, data: prepared.tx.data }).args;
|
|
1852
|
+
return { kind: "router", provider, accountId, wallet };
|
|
1853
|
+
} catch {
|
|
1854
|
+
return null;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
/**
|
|
1859
|
+
* Why the launch must not be signed because it pays someone other than `payout` asked, or null. A TikTok handle is
|
|
1860
|
+
* hashed here and a GitHub one looked up before preparing, so only an X handle without accountId takes hookit.fun's
|
|
1861
|
+
* lookup (the result shows the id the calldata pays).
|
|
1862
|
+
*/
|
|
1863
|
+
function launchPayoutRefusal(prepared, payout) {
|
|
1864
|
+
const got = launchPayoutOf(prepared);
|
|
1865
|
+
const kind = payout?.kind ?? "wallet";
|
|
1866
|
+
const refuse = (who) => `hookit answered with a launch that pays the creator fees to someone else than ${who}. Nothing was sent.`;
|
|
1867
|
+
if (!got) return refuse("asked (its payout router call does not decode)");
|
|
1868
|
+
if (kind === "wallet") {
|
|
1869
|
+
if (!payout?.address || same(payout.address, account?.address)) return got.kind === "self" ? null : refuse("this wallet");
|
|
1870
|
+
const paid = got.kind === "router" && got.provider === PAYOUT_PROVIDER.wallet && got.accountId === 0n;
|
|
1871
|
+
return paid && same(got.wallet, payout.address) ? null : refuse(payout.address);
|
|
1872
|
+
}
|
|
1873
|
+
const who = payout.handle ? `@${payout.handle} on ${kind}` : `${kind} account ${payout.accountId}`;
|
|
1874
|
+
if (got.kind !== "router" || got.provider !== PAYOUT_PROVIDER[kind] || !same(got.wallet, zeroAddress)) return refuse(who);
|
|
1875
|
+
let expected = null;
|
|
1876
|
+
try {
|
|
1877
|
+
if (payout.accountId) expected = BigInt(payout.accountId);
|
|
1878
|
+
else if (kind === "tiktok") {
|
|
1879
|
+
const handle = payoutHandle("tiktok", payout.handle);
|
|
1880
|
+
expected = handle ? BigInt(keccak256(toHex(`tiktok:${handle.toLowerCase()}`))) : null;
|
|
1881
|
+
}
|
|
1882
|
+
} catch {
|
|
1883
|
+
return refuse(who);
|
|
1884
|
+
}
|
|
1885
|
+
if (expected !== null) return got.accountId === expected ? null : refuse(who);
|
|
1886
|
+
return kind === "x" && got.accountId > 0n ? null : refuse(who);
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
/** The creator-fee payee as the signed calldata has it (never hookit.fun's plan). */
|
|
1890
|
+
function launchPayoutView(prepared) {
|
|
1891
|
+
const got = launchPayoutOf(prepared);
|
|
1892
|
+
if (!got || got.kind === "self") return { to: "this wallet" };
|
|
1893
|
+
const kind = Object.keys(PAYOUT_PROVIDER).find((k) => PAYOUT_PROVIDER[k] === got.provider) ?? `provider ${got.provider}`;
|
|
1894
|
+
return kind === "wallet" ? { kind, address: got.wallet } : { kind, accountId: got.accountId.toString() };
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1752
1897
|
/**
|
|
1753
1898
|
* Runs the tools that sign one at a time: an agent may call several at once, and two of them sharing the wallet's
|
|
1754
1899
|
* nonce or its balance deltas could spend what the other one just received.
|
|
@@ -2157,9 +2302,20 @@ server.registerTool(
|
|
|
2157
2302
|
} else {
|
|
2158
2303
|
delete args.builderHooks;
|
|
2159
2304
|
}
|
|
2305
|
+
// Who is paid is checked in the calldata hookit.fun prepares; a GitHub handle is pinned to its id here first.
|
|
2306
|
+
if (args.payout?.kind === "github" && !args.payout.accountId) {
|
|
2307
|
+
const handle = payoutHandle("github", args.payout.handle);
|
|
2308
|
+
const id = handle ? await githubUserId(handle) : null;
|
|
2309
|
+
if (!id) return fail("Could not look up that GitHub handle on api.github.com. Pass payout.accountId (the account's numeric id) instead.");
|
|
2310
|
+
args.payout = { ...args.payout, accountId: id };
|
|
2311
|
+
}
|
|
2312
|
+
// The current Evolve's module id (v2 once it has code, else v1), read from the ModuleRegistry by plugin address.
|
|
2313
|
+
const evolveModuleId = args.evolve ? await currentEvolveModuleId() : null;
|
|
2314
|
+
if (args.evolve && evolveModuleId === null) return fail("Evolve is not registered in the ModuleRegistry (or is deprecated or halted): launch without evolve.");
|
|
2160
2315
|
const refusalOf = (prepared) =>
|
|
2161
2316
|
launchRefusal(prepared) ??
|
|
2162
|
-
(
|
|
2317
|
+
launchPayoutRefusal(prepared, args.payout) ??
|
|
2318
|
+
(args.evolve ? evolveLaunchRefusal(prepared, args, evolveModuleId) : null) ??
|
|
2163
2319
|
(velocity ? velocityLaunchRefusal(prepared, velocity.asked, velocity.ids) : null) ??
|
|
2164
2320
|
(builder ? builderHooksLaunchRefusal(prepared, builder.expected, builder.entries) : null) ??
|
|
2165
2321
|
(!args.evolve && !velocity && !builder ? unaskedSlotsRefusal(prepared) : null);
|
|
@@ -2188,6 +2344,7 @@ server.registerTool(
|
|
|
2188
2344
|
? `The launch would fail: ${launchSimulationError(prepared.simulation.error)}`
|
|
2189
2345
|
: "The launch is valid.",
|
|
2190
2346
|
plan: prepared.plan,
|
|
2347
|
+
payout: launchPayoutView(prepared),
|
|
2191
2348
|
...(args.evolve ? { evolve: evolveLaunchView(prepared) } : {}),
|
|
2192
2349
|
...(velocity ? { velocity: velocityLaunchView(prepared) } : {}),
|
|
2193
2350
|
...(builder ? { builderHooks: builderHooksLaunchView(prepared, builder.expected, builder.entries) } : {}),
|
|
@@ -2225,7 +2382,7 @@ server.registerTool(
|
|
|
2225
2382
|
page: token ? `${API}/token/${token}` : null,
|
|
2226
2383
|
tx: `https://explorer.inkonchain.com/tx/${hash}`,
|
|
2227
2384
|
settings: prepared.plan.settings,
|
|
2228
|
-
payout: prepared
|
|
2385
|
+
payout: launchPayoutView(prepared),
|
|
2229
2386
|
payoutAgent: agent,
|
|
2230
2387
|
note: agent
|
|
2231
2388
|
? "Creator fees go to the payout target through this launch's CreatorPayoutAgent. This wallet is its controller."
|
|
@@ -2580,7 +2737,7 @@ server.registerTool(
|
|
|
2580
2737
|
let firstError = null;
|
|
2581
2738
|
// Land where the coin is cheapest: the markets of one coin can be priced far apart.
|
|
2582
2739
|
const landings = (await landingAssets(c, coin, asset)).filter((a) => relayable(c, asset, a.address));
|
|
2583
|
-
if (!landings.length) firstError = `Relay does not swap ${pay.symbol} into what this coin trades in
|
|
2740
|
+
if (!landings.length) firstError = `Relay does not swap ${pay.symbol} into what this coin trades in (one side must be ETH or USDG).`;
|
|
2584
2741
|
for (const landing of (await rankBuyLandings(c, coin, pay, value, landings)).slice(0, 3)) {
|
|
2585
2742
|
const q = await relayQuote(asset, landing.address, value, relayBps);
|
|
2586
2743
|
const checked = q.quote ? await relayCheck(asset, landing.address, value, q.quote) : q;
|
|
@@ -2797,7 +2954,7 @@ server.registerTool(
|
|
|
2797
2954
|
);
|
|
2798
2955
|
}
|
|
2799
2956
|
const assets = await tradeAssets(c, coin);
|
|
2800
|
-
const none = landings.length ? "Relay cannot swap what this coin trades in." : `Relay does not swap what this coin trades in into ${out.symbol}
|
|
2957
|
+
const none = landings.length ? "Relay cannot swap what this coin trades in." : `Relay does not swap what this coin trades in into ${out.symbol} (one side must be ETH or USDG).`;
|
|
2801
2958
|
return fail(`${relayError ?? firstError ?? none} This coin trades in ${assets.join(", ")}: receive ${assets.length > 1 ? "one of those" : "that"} instead.`);
|
|
2802
2959
|
}
|
|
2803
2960
|
const capped = await relayCapRefusal(c, leg, route.out);
|
|
@@ -3388,7 +3545,10 @@ const EVOLVE_PARAMS = parseAbiParameters(
|
|
|
3388
3545
|
);
|
|
3389
3546
|
|
|
3390
3547
|
/** Evolve's id in ModuleRegistry (v2 module 5, as its tx list records it). */
|
|
3548
|
+
/** Evolve v1's ModuleRegistry id (deprecated by the v2 deployment); a launch is checked against the current Evolve's id. */
|
|
3391
3549
|
const EVOLVE_MODULE_ID = 5;
|
|
3550
|
+
/** `abi.encode(EvolvePlugin.Params)`: 9 static words. hookit-mcp never plugs hooks at launch (Evolve v2's packed plugs). */
|
|
3551
|
+
const EVOLVE_PARAMS_BYTES = 288;
|
|
3392
3552
|
/** hookit.fun's defaults for what launch_token's evolve leaves out (web/src/lib/evolve-params.ts): cap 3 %, fee 1 %. */
|
|
3393
3553
|
const EVOLVE_DEFAULT_MAX_FEE_BPS = 300;
|
|
3394
3554
|
const EVOLVE_DEFAULT_FEE_BPS = 100;
|
|
@@ -3420,7 +3580,8 @@ function evolveLaunchDecoded(prepared) {
|
|
|
3420
3580
|
if (cfg.plugins.length !== 1) return { slots: cfg.plugins.length };
|
|
3421
3581
|
const slot = cfg.plugins[0];
|
|
3422
3582
|
const [params] = decodeAbiParameters(EVOLVE_PARAMS, slot.initData);
|
|
3423
|
-
|
|
3583
|
+
const initBytes = (String(slot.initData).length - 2) / 2;
|
|
3584
|
+
return { slots: 1, moduleId: Number(slot.moduleId), potBps: Number(slot.potBps), params, initBytes };
|
|
3424
3585
|
} catch {
|
|
3425
3586
|
return null;
|
|
3426
3587
|
}
|
|
@@ -3434,14 +3595,19 @@ function evolveLaunchDecoded(prepared) {
|
|
|
3434
3595
|
* the pot when no preset but clean and no `modules` key routes the pot elsewhere, else Factory C's rule that every
|
|
3435
3596
|
* route totals 100 %).
|
|
3436
3597
|
*/
|
|
3437
|
-
function evolveLaunchRefusal(prepared, args = {}) {
|
|
3598
|
+
function evolveLaunchRefusal(prepared, args = {}, evolveModuleId = EVOLVE_MODULE_ID) {
|
|
3438
3599
|
const nothing = " Nothing was sent; update hookit-mcp or launch without evolve.";
|
|
3439
3600
|
const d = evolveLaunchDecoded(prepared);
|
|
3440
3601
|
if (!d || d.slots !== 1 || !same(d.params.decider, account.address)) {
|
|
3441
3602
|
return `hookit answered with an Evolve launch whose decider is not this wallet (or with another plugin slot than Evolve's).${nothing}`;
|
|
3442
3603
|
}
|
|
3443
|
-
if (d.moduleId !==
|
|
3444
|
-
return `hookit answered with a launch whose plugin slot is module ${d.moduleId}, not Evolve (module ${
|
|
3604
|
+
if (d.moduleId !== evolveModuleId) {
|
|
3605
|
+
return `hookit answered with a launch whose plugin slot is module ${d.moduleId}, not Evolve (module ${evolveModuleId}).${nothing}`;
|
|
3606
|
+
}
|
|
3607
|
+
// Audit L-1: bytes after the Params are hooks plugged at launch (Evolve v2), which this server never asks for, and
|
|
3608
|
+
// which Evolve v1 would ignore silently.
|
|
3609
|
+
if (d.initBytes !== EVOLVE_PARAMS_BYTES) {
|
|
3610
|
+
return `hookit answered with ${d.initBytes} bytes of Evolve launch data, not the ${EVOLVE_PARAMS_BYTES} bytes of its settings: hookit-mcp never plugs hooks at launch.${nothing}`;
|
|
3445
3611
|
}
|
|
3446
3612
|
const asked = args.evolve && typeof args.evolve === "object" ? args.evolve : {};
|
|
3447
3613
|
const bps = (v) => (v === undefined || v === null ? undefined : Math.round(Number(v) * 100));
|
|
@@ -3526,9 +3692,108 @@ function evolveReason(raw) {
|
|
|
3526
3692
|
return t;
|
|
3527
3693
|
}
|
|
3528
3694
|
|
|
3695
|
+
/** An Evolve pin as an address, or null when it is unset, a placeholder, zero or not an address. */
|
|
3696
|
+
function pinnedAddress(a) {
|
|
3697
|
+
return typeof a === "string" && isAddress(a) && BigInt(a) !== 0n ? getAddress(a) : null;
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
/** HOOKIT_EVOLVE_PLUGIN (v2) and HOOKIT_EVOLVE_V2_*: override the Evolve v2 pins; not an address: off. */
|
|
3701
|
+
{
|
|
3702
|
+
const raw = process.env.HOOKIT_EVOLVE_PLUGIN?.trim();
|
|
3703
|
+
if (raw) PINNED.evolvePlugin = isAddress(raw) ? getAddress(raw) : null;
|
|
3704
|
+
for (const [name, env] of [
|
|
3705
|
+
["memoryEvolve", "HOOKIT_EVOLVE_V2_MEMORY_EVOLVE"],
|
|
3706
|
+
["memoryLens", "HOOKIT_EVOLVE_V2_MEMORY_LENS"],
|
|
3707
|
+
["memoryFee1", "HOOKIT_EVOLVE_V2_MEMORY_FEE1"],
|
|
3708
|
+
["memoryFee2", "HOOKIT_EVOLVE_V2_MEMORY_FEE2"],
|
|
3709
|
+
["velocityEvolve", "HOOKIT_EVOLVE_V2_VELOCITY_EVOLVE"],
|
|
3710
|
+
["velocityEvolveFee1", "HOOKIT_EVOLVE_V2_VELOCITY_EVOLVE_FEE1"],
|
|
3711
|
+
["velocityEvolveFee2", "HOOKIT_EVOLVE_V2_VELOCITY_EVOLVE_FEE2"],
|
|
3712
|
+
]) {
|
|
3713
|
+
const v = process.env[env]?.trim();
|
|
3714
|
+
if (v) PINNED.evolveV2[name] = isAddress(v) ? getAddress(v) : null;
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
const evolveCodeCache = new Map();
|
|
3719
|
+
async function evolveHasCode(a) {
|
|
3720
|
+
if (!a) return false;
|
|
3721
|
+
if (evolveCodeCache.get(a)) return true;
|
|
3722
|
+
const code = await publicClient.getCode({ address: a }).catch(() => undefined);
|
|
3723
|
+
const has = !!code && code !== "0x";
|
|
3724
|
+
if (has) evolveCodeCache.set(a, true);
|
|
3725
|
+
return has;
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3728
|
+
/** Every Evolve plugin version, current (v2) first, then v1. */
|
|
3729
|
+
function evolvePlugins() {
|
|
3730
|
+
return [pinnedAddress(PINNED.evolvePlugin), pinnedAddress(PINNED.evolvePluginV1)].filter(Boolean);
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
/** The Evolve new launches go to: v2 once it has code at its address, else v1 (null: none). */
|
|
3734
|
+
async function currentEvolvePlugin() {
|
|
3735
|
+
for (const p of evolvePlugins()) if (await evolveHasCode(p)) return p;
|
|
3736
|
+
return null;
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3739
|
+
/** The current Evolve's ModuleRegistry id, found by scanning the registry for its address (live entries only), or null. */
|
|
3740
|
+
async function currentEvolveModuleId() {
|
|
3741
|
+
const plugin = await currentEvolvePlugin();
|
|
3742
|
+
const registry = pinnedAddress(PINNED.moduleRegistry);
|
|
3743
|
+
if (!plugin || !registry) return null;
|
|
3744
|
+
try {
|
|
3745
|
+
const n = Number(await read(registry, moduleRegistryAbi, "moduleCount"));
|
|
3746
|
+
for (let id = n; id >= 1; id--) {
|
|
3747
|
+
const e = await read(registry, moduleRegistryAbi, "moduleEntry", [id]);
|
|
3748
|
+
if (same(e.plugin, plugin)) return e.deprecated || e.halted ? null : id;
|
|
3749
|
+
}
|
|
3750
|
+
} catch {
|
|
3751
|
+
// fall through
|
|
3752
|
+
}
|
|
3753
|
+
return null;
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3756
|
+
const evolvePluginOfCache = new Map();
|
|
3757
|
+
/**
|
|
3758
|
+
* The Evolve plugin a coin runs on: the first version (v2, then v1) whose `extensionContext(token)` names a decider;
|
|
3759
|
+
* null for a coin no Evolve knows (or not an address). A coin's plugin never changes, so a hit is cached.
|
|
3760
|
+
*/
|
|
3761
|
+
async function evolvePluginOf(token) {
|
|
3762
|
+
if (typeof token !== "string" || !isAddress(token)) return null;
|
|
3763
|
+
const key = token.toLowerCase();
|
|
3764
|
+
if (evolvePluginOfCache.has(key)) return evolvePluginOfCache.get(key);
|
|
3765
|
+
for (const p of evolvePlugins()) {
|
|
3766
|
+
if (!(await evolveHasCode(p))) continue;
|
|
3767
|
+
const ctx = await read(p, evolveAbi, "extensionContext", [getAddress(token)]).catch(() => null);
|
|
3768
|
+
if (ctx && !same(ctx[0], zeroAddress)) {
|
|
3769
|
+
evolvePluginOfCache.set(key, p);
|
|
3770
|
+
return p;
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
return null;
|
|
3774
|
+
}
|
|
3775
|
+
|
|
3776
|
+
/** The coin's plugin, else the current one (whose reads then answer "This coin does not run Evolve."). */
|
|
3777
|
+
async function evolvePluginFor(token) {
|
|
3778
|
+
return (await evolvePluginOf(token)) ?? (await currentEvolvePlugin()) ?? pinnedAddress(PINNED.evolvePluginV1);
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
/** Whether `a` is the `key` contract (memoryEvolve, velocityEvolveFee1…) of either Evolve version. */
|
|
3782
|
+
function isEvolvePin(a, key) {
|
|
3783
|
+
return [PINNED[key], PINNED.evolveV2[key]].some((p) => {
|
|
3784
|
+
const pin = pinnedAddress(p);
|
|
3785
|
+
return !!pin && same(a, pin);
|
|
3786
|
+
});
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
/** The Memory / Velocity contracts of an Evolve version: v1's pins for v1, the evolveV2 pins for v2. */
|
|
3790
|
+
function evolveExtensionPins(plugin) {
|
|
3791
|
+
return same(plugin, PINNED.evolvePluginV1) ? PINNED : PINNED.evolveV2;
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3529
3794
|
/** The coin's Evolve state as a swap sees it now (a due setting counted as applied), or { error }. */
|
|
3530
3795
|
async function evolveCoin(token) {
|
|
3531
|
-
const plugin =
|
|
3796
|
+
const plugin = await evolvePluginFor(token);
|
|
3532
3797
|
if (!plugin) return { error: "Evolve is not live in this hookit-mcp release yet." };
|
|
3533
3798
|
let st;
|
|
3534
3799
|
try {
|
|
@@ -3892,7 +4157,7 @@ server.registerTool(
|
|
|
3892
4157
|
annotations: { readOnlyHint: true },
|
|
3893
4158
|
},
|
|
3894
4159
|
async () => {
|
|
3895
|
-
const plugin =
|
|
4160
|
+
const plugin = await currentEvolvePlugin();
|
|
3896
4161
|
if (!plugin) return fail("Evolve is not live in this hookit-mcp release yet.");
|
|
3897
4162
|
const { ext, entries, truncated } = await evolveCatalog(plugin);
|
|
3898
4163
|
return reply({
|
|
@@ -3980,14 +4245,14 @@ function nftStrategyConfigView(cfg) {
|
|
|
3980
4245
|
/** A hook's config as the decider sends it, decoded for the hooks this release knows (NFT Strategy, Memory, Boss Raid). */
|
|
3981
4246
|
async function evolveConfigView(x, cfg, quote) {
|
|
3982
4247
|
if (same(x.ext, PINNED.evolveNftStrategy)) return { hook: "NFT Strategy", ...nftStrategyConfigView(cfg) };
|
|
3983
|
-
if (
|
|
4248
|
+
if (isEvolvePin(x.ext, "memoryEvolve")) {
|
|
3984
4249
|
return { hook: "Memory", ...memoryConfigView(cfg, await assetMeta(getAddress(quote)).catch(() => null)) };
|
|
3985
4250
|
}
|
|
3986
|
-
if (
|
|
4251
|
+
if (isEvolvePin(x.ext, "memoryFee1") || isEvolvePin(x.ext, "memoryFee2")) {
|
|
3987
4252
|
return { hook: "Memory fee companion", ...(cfg === "0x" ? { config: "empty, as it must be" } : { raw: cfg, invalid: "a companion takes no config (0x)" }) };
|
|
3988
4253
|
}
|
|
3989
|
-
if (
|
|
3990
|
-
if (
|
|
4254
|
+
if (isEvolvePin(x.ext, "velocityEvolve")) return { hook: "Velocity", ...velocityConfigView(cfg) };
|
|
4255
|
+
if (isEvolvePin(x.ext, "velocityEvolveFee1") || isEvolvePin(x.ext, "velocityEvolveFee2")) {
|
|
3991
4256
|
return { hook: "Velocity fee companion", ...(cfg === "0x" ? { config: "empty, as it must be" } : { raw: cfg, invalid: "a companion takes no config (0x)" }) };
|
|
3992
4257
|
}
|
|
3993
4258
|
if (bytes32Text(x.name) === "Boss Raid") {
|
|
@@ -4020,7 +4285,7 @@ server.registerTool(
|
|
|
4020
4285
|
if (!account) return needKey();
|
|
4021
4286
|
if (!isAddress(token)) return fail(`Not an address: ${token}`);
|
|
4022
4287
|
token = getAddress(token);
|
|
4023
|
-
const plugin =
|
|
4288
|
+
const plugin = await evolvePluginFor(token);
|
|
4024
4289
|
if (!plugin) return fail("Evolve is not live in this hookit-mcp release yet.");
|
|
4025
4290
|
const ctx = await read(plugin, evolveAbi, "extensionContext", [token]).catch(() => null);
|
|
4026
4291
|
if (!ctx) return fail("This coin does not run Evolve.");
|
|
@@ -4071,7 +4336,7 @@ server.registerTool(
|
|
|
4071
4336
|
return fail(`Hook ${hookId} is already in slot ${j + 1}: one slot per hook.`);
|
|
4072
4337
|
}
|
|
4073
4338
|
}
|
|
4074
|
-
if (
|
|
4339
|
+
if (isEvolvePin(x.ext, "memoryEvolve")) {
|
|
4075
4340
|
// Memory's onAnnounce refuses a decider that is not an AI agent and a setup out of its bounds: say it first.
|
|
4076
4341
|
if ((await read(x.ext, memoryAbi, "isAgent", [decider]).catch(() => null)) === false) return fail(memoryNotAgent(decider));
|
|
4077
4342
|
const d = decodeMemorySetup(cfg);
|
|
@@ -4079,18 +4344,18 @@ server.registerTool(
|
|
|
4079
4344
|
if (refused) return fail(`Memory would refuse this config. ${refused} memory_set builds it from plain settings.`);
|
|
4080
4345
|
announceAbi = memoryAnnounceAbi;
|
|
4081
4346
|
hookFailure = memoryFailure;
|
|
4082
|
-
} else if (
|
|
4347
|
+
} else if (isEvolvePin(x.ext, "memoryFee1") || isEvolvePin(x.ext, "memoryFee2")) {
|
|
4083
4348
|
if (cfg !== "0x") return fail("A Memory fee companion takes no config (0x).");
|
|
4084
4349
|
announceAbi = memoryAnnounceAbi;
|
|
4085
4350
|
hookFailure = memoryFailure;
|
|
4086
|
-
} else if (
|
|
4351
|
+
} else if (isEvolvePin(x.ext, "velocityEvolve")) {
|
|
4087
4352
|
// Velocity's onAnnounce refuses a setup out of its bounds and a cashback without a share (or a share without one).
|
|
4088
4353
|
const d = decodeVelocitySetup(cfg);
|
|
4089
4354
|
const refused = d.error ?? velocitySetupRefusal(d.setup, shareBps);
|
|
4090
4355
|
if (refused) return fail(`Velocity would refuse this config. ${refused} velocity_set builds it from a preset and plugs its fee companions.`);
|
|
4091
4356
|
announceAbi = velocityAnnounceAbi;
|
|
4092
4357
|
hookFailure = velocityFailure;
|
|
4093
|
-
} else if (
|
|
4358
|
+
} else if (isEvolvePin(x.ext, "velocityEvolveFee1") || isEvolvePin(x.ext, "velocityEvolveFee2")) {
|
|
4094
4359
|
if (cfg !== "0x") return fail("A Velocity fee companion takes no config (0x).");
|
|
4095
4360
|
announceAbi = velocityAnnounceAbi;
|
|
4096
4361
|
hookFailure = velocityFailure;
|
|
@@ -4110,12 +4375,13 @@ server.registerTool(
|
|
|
4110
4375
|
// Memory leaving a slot (or its announced plug-in dropped): its companions ask while Memory runs the plug-in they
|
|
4111
4376
|
// follow, which Memory keeps running after it leaves (until another of its plug-ins trades) unless it is retired.
|
|
4112
4377
|
let memoryNote = null;
|
|
4113
|
-
const
|
|
4378
|
+
const pins = evolveExtensionPins(plugin);
|
|
4379
|
+
const memoryExt = pinnedAddress(pins.memoryEvolve);
|
|
4114
4380
|
const leavingKey = !memoryExt ? null : cancel ? (announced && same(p.ext, memoryExt) ? p.key : null) : same(l.ext, memoryExt) ? l.key : null;
|
|
4115
4381
|
const companionSlots =
|
|
4116
4382
|
leavingKey === null
|
|
4117
4383
|
? []
|
|
4118
|
-
: [
|
|
4384
|
+
: [pinnedAddress(pins.memoryFee1), pinnedAddress(pins.memoryFee2)].flatMap((f) => {
|
|
4119
4385
|
const at = f ? memoryCompanionAt(h, f) : null;
|
|
4120
4386
|
return at && at.index !== i ? [at.index + 1] : [];
|
|
4121
4387
|
});
|
|
@@ -4133,11 +4399,11 @@ server.registerTool(
|
|
|
4133
4399
|
}
|
|
4134
4400
|
}
|
|
4135
4401
|
// Velocity leaving a slot: its companions keep asking a (fading) malus from its last mean until they are unplugged too.
|
|
4136
|
-
const velocityExt = velocityAddress(
|
|
4402
|
+
const velocityExt = velocityAddress(pins.velocityEvolve);
|
|
4137
4403
|
const velocityLeaving = !!velocityExt && !cancel && same(l.ext, velocityExt) && !same(h.entries[hookId]?.ext, velocityExt);
|
|
4138
4404
|
const velocityCompanionSlots = !velocityLeaving
|
|
4139
4405
|
? []
|
|
4140
|
-
: [
|
|
4406
|
+
: [pins.velocityEvolveFee1, pins.velocityEvolveFee2].flatMap((f) => {
|
|
4141
4407
|
const a = velocityAddress(f);
|
|
4142
4408
|
const at = a ? memoryCompanionAt(h, a) : null;
|
|
4143
4409
|
return at?.live && at.index !== i && !at.leaving ? [at.index + 1] : [];
|
|
@@ -4232,6 +4498,8 @@ const NFT_EVOLVE_COLLECTIONS_MAX = 64;
|
|
|
4232
4498
|
const NFT_EVOLVE_CHALLENGE_SEC = 900;
|
|
4233
4499
|
|
|
4234
4500
|
const NFT_EVOLVE_OFF = "NFT Strategy on Evolve is not live in this hookit-mcp release yet.";
|
|
4501
|
+
const NFT_EVOLVE_V2 =
|
|
4502
|
+
"This coin runs Evolve v2, whose hooks do not list NFT Strategy yet (it comes back as NFT Strategy v3 after its review). evolve_hooks lists what it can plug.";
|
|
4235
4503
|
|
|
4236
4504
|
/** NFT Strategy's catalog entry ({ id, entry }) in EvolveExtensions' hook list, found by its pinned address, or null. */
|
|
4237
4505
|
function nftEvolveEntry(entries) {
|
|
@@ -4325,9 +4593,11 @@ server.registerTool(
|
|
|
4325
4593
|
},
|
|
4326
4594
|
async ({ token }) => {
|
|
4327
4595
|
const nft = PINNED.evolveNftStrategy;
|
|
4328
|
-
|
|
4596
|
+
// NFT Strategy (f3) is listed on Evolve v1's catalog only; v2 gets NFT Strategy v3 after its audit.
|
|
4597
|
+
const plugin = pinnedAddress(PINNED.evolvePluginV1);
|
|
4329
4598
|
if (!nft || !plugin) return fail(NFT_EVOLVE_OFF);
|
|
4330
4599
|
if (token !== undefined && !isAddress(token)) return fail(`Not an address: ${token}`);
|
|
4600
|
+
if (token !== undefined && (await evolvePluginOf(token)) && !same(await evolvePluginOf(token), plugin)) return fail(NFT_EVOLVE_V2);
|
|
4331
4601
|
const [sweeper, raffle, floatNfts, catalog] = await Promise.all([
|
|
4332
4602
|
read(nft, nftEvolveAbi, "SWEEPER"),
|
|
4333
4603
|
read(nft, nftEvolveAbi, "RAFFLE"),
|
|
@@ -4434,10 +4704,11 @@ server.registerTool(
|
|
|
4434
4704
|
serialized(async ({ token, collection, outcome, sharePct, slot, unplug, cancel, dryRun }) => {
|
|
4435
4705
|
if (!account) return needKey();
|
|
4436
4706
|
const nft = PINNED.evolveNftStrategy;
|
|
4437
|
-
const plugin = PINNED.
|
|
4707
|
+
const plugin = pinnedAddress(PINNED.evolvePluginV1);
|
|
4438
4708
|
if (!nft || !plugin) return fail(NFT_EVOLVE_OFF);
|
|
4439
4709
|
if (!isAddress(token)) return fail(`Not an address: ${token}`);
|
|
4440
4710
|
token = getAddress(token);
|
|
4711
|
+
if ((await evolvePluginOf(token)) && !same(await evolvePluginOf(token), plugin)) return fail(NFT_EVOLVE_V2);
|
|
4441
4712
|
const ctx = await read(plugin, evolveAbi, "extensionContext", [token]).catch(() => null);
|
|
4442
4713
|
if (!ctx) return fail("This coin does not run Evolve.");
|
|
4443
4714
|
const [decider, delay, quote] = ctx;
|
|
@@ -5352,17 +5623,21 @@ function memoryGrudge(word, now) {
|
|
|
5352
5623
|
return mark === 2 ? { until, rule: Number((b >> 168n) & 0xffn), tag: (b >> 176n) & 0xffffffffn } : null;
|
|
5353
5624
|
}
|
|
5354
5625
|
|
|
5355
|
-
|
|
5626
|
+
/** Memory's contracts per Evolve plugin (v1's pins for v1 coins, the evolveV2 pins for v2 coins). */
|
|
5627
|
+
const memoryCache = new Map();
|
|
5356
5628
|
/**
|
|
5357
5629
|
* Memory's contracts once their code is there and they belong together (the lens reads this Memory, Memory runs on
|
|
5358
5630
|
* Evolve's EvolveExtensions, each companion follows this Memory with its rank): { memory, lens, rules, fees: [{ rank,
|
|
5359
5631
|
* ext }] }, or null. A companion that is not configured, has no code or is not this Memory's is left out: the fee then
|
|
5360
5632
|
* stops below its percent.
|
|
5361
5633
|
*/
|
|
5362
|
-
async function memoryContracts() {
|
|
5363
|
-
if (
|
|
5364
|
-
|
|
5365
|
-
|
|
5634
|
+
async function memoryContracts(evolvePlugin = PINNED.evolvePluginV1) {
|
|
5635
|
+
if (!evolvePlugin) return null;
|
|
5636
|
+
if (memoryCache.has(evolvePlugin)) return memoryCache.get(evolvePlugin);
|
|
5637
|
+
const pins = evolveExtensionPins(evolvePlugin);
|
|
5638
|
+
const memory = pinnedAddress(pins.memoryEvolve);
|
|
5639
|
+
const lens = pinnedAddress(pins.memoryLens);
|
|
5640
|
+
if (!memory || !lens) return null;
|
|
5366
5641
|
const hasCode = async (a) => {
|
|
5367
5642
|
const code = await publicClient.getCode({ address: a }).catch(() => undefined);
|
|
5368
5643
|
return !!code && code !== "0x";
|
|
@@ -5377,16 +5652,18 @@ async function memoryContracts() {
|
|
|
5377
5652
|
]);
|
|
5378
5653
|
if (!same(lensOf, memory) || !same(ext, extensions)) return null;
|
|
5379
5654
|
const fees = [];
|
|
5380
|
-
for (const [rank,
|
|
5381
|
-
[1,
|
|
5382
|
-
[2,
|
|
5655
|
+
for (const [rank, raw] of [
|
|
5656
|
+
[1, pins.memoryFee1],
|
|
5657
|
+
[2, pins.memoryFee2],
|
|
5383
5658
|
]) {
|
|
5659
|
+
const a = pinnedAddress(raw);
|
|
5384
5660
|
if (!a || !(await hasCode(a))) continue;
|
|
5385
5661
|
const [m, r] = await Promise.all([read(a, memoryFeeAbi, "MEMORY").catch(() => null), read(a, memoryFeeAbi, "RANK").catch(() => null)]);
|
|
5386
5662
|
if (same(m, memory) && Number(r) === rank) fees.push({ rank, ext: getAddress(a) });
|
|
5387
5663
|
}
|
|
5388
|
-
|
|
5389
|
-
|
|
5664
|
+
const found = { memory: getAddress(memory), lens: getAddress(lens), rules: getAddress(rules), fees };
|
|
5665
|
+
memoryCache.set(evolvePlugin, found);
|
|
5666
|
+
return found;
|
|
5390
5667
|
} catch {
|
|
5391
5668
|
return null;
|
|
5392
5669
|
}
|
|
@@ -5498,11 +5775,11 @@ server.registerTool(
|
|
|
5498
5775
|
annotations: { readOnlyHint: true },
|
|
5499
5776
|
},
|
|
5500
5777
|
async ({ token }) => {
|
|
5501
|
-
const c = await memoryContracts();
|
|
5778
|
+
const c = await memoryContracts(await evolvePluginFor(token));
|
|
5502
5779
|
if (!c) return fail(MEMORY_OFF);
|
|
5503
5780
|
if (!isAddress(token)) return fail(`Not an address: ${token}`);
|
|
5504
5781
|
token = getAddress(token);
|
|
5505
|
-
const plugin =
|
|
5782
|
+
const plugin = await evolvePluginFor(token);
|
|
5506
5783
|
const ctx = await read(plugin, evolveAbi, "extensionContext", [token]).catch(() => null);
|
|
5507
5784
|
if (!ctx) return fail("This coin does not run Evolve.");
|
|
5508
5785
|
const [decider, delay, quoteAddress] = ctx;
|
|
@@ -5671,11 +5948,11 @@ server.registerTool(
|
|
|
5671
5948
|
},
|
|
5672
5949
|
serialized(async ({ token, rules, allies, feeCapPct, botTrades, reason, sharePct, slot, cancel, dryRun }) => {
|
|
5673
5950
|
if (!account) return needKey();
|
|
5674
|
-
const c = await memoryContracts();
|
|
5951
|
+
const c = await memoryContracts(await evolvePluginFor(token));
|
|
5675
5952
|
if (!c) return fail(MEMORY_OFF);
|
|
5676
5953
|
if (!isAddress(token)) return fail(`Not an address: ${token}`);
|
|
5677
5954
|
token = getAddress(token);
|
|
5678
|
-
const plugin =
|
|
5955
|
+
const plugin = await evolvePluginFor(token);
|
|
5679
5956
|
const ctx = await read(plugin, evolveAbi, "extensionContext", [token]).catch(() => null);
|
|
5680
5957
|
if (!ctx) return fail("This coin does not run Evolve.");
|
|
5681
5958
|
const [decider, delay, quoteAddress] = ctx;
|
|
@@ -5926,14 +6203,14 @@ server.registerTool(
|
|
|
5926
6203
|
annotations: { readOnlyHint: true },
|
|
5927
6204
|
},
|
|
5928
6205
|
async ({ token, wallet }) => {
|
|
5929
|
-
const c = await memoryContracts();
|
|
6206
|
+
const c = await memoryContracts(await evolvePluginFor(token));
|
|
5930
6207
|
if (!c) return fail(MEMORY_OFF);
|
|
5931
6208
|
if (!isAddress(token)) return fail(`Not an address: ${token}`);
|
|
5932
6209
|
if (wallet === undefined && !account) return fail("Give wallet (no key is set, so there is no default wallet).");
|
|
5933
6210
|
if (wallet !== undefined && !isAddress(wallet)) return fail(`Not an address: ${wallet}`);
|
|
5934
6211
|
token = getAddress(token);
|
|
5935
6212
|
wallet = getAddress(wallet ?? account.address);
|
|
5936
|
-
const ctx = await read(
|
|
6213
|
+
const ctx = await read(await evolvePluginFor(token), evolveAbi, "extensionContext", [token]).catch(() => null);
|
|
5937
6214
|
if (!ctx) return fail("This coin does not run Evolve.");
|
|
5938
6215
|
const [coin, wo, quote, coinMeta, block, balance] = await Promise.all([
|
|
5939
6216
|
read(c.memory, memoryAbi, "coinOf", [token]),
|
|
@@ -6032,7 +6309,7 @@ server.registerTool(
|
|
|
6032
6309
|
},
|
|
6033
6310
|
serialized(async ({ token, wallets, dryRun }) => {
|
|
6034
6311
|
if (!account) return needKey();
|
|
6035
|
-
const c = await memoryContracts();
|
|
6312
|
+
const c = await memoryContracts(await evolvePluginFor(token));
|
|
6036
6313
|
if (!c) return fail(MEMORY_OFF);
|
|
6037
6314
|
if (!isAddress(token)) return fail(`Not an address: ${token}`);
|
|
6038
6315
|
token = getAddress(token);
|
|
@@ -6040,7 +6317,7 @@ server.registerTool(
|
|
|
6040
6317
|
if (bad !== undefined) return fail(`Not an address: ${bad}`);
|
|
6041
6318
|
const list = [...new Map(wallets.map((x) => [x.toLowerCase(), getAddress(x)])).values()];
|
|
6042
6319
|
if (list.length > MEMORY_LIMITS.pardons) return fail(`At most ${MEMORY_LIMITS.pardons} wallets per pardon.`);
|
|
6043
|
-
const plugin =
|
|
6320
|
+
const plugin = await evolvePluginFor(token);
|
|
6044
6321
|
const ctx = await read(plugin, evolveAbi, "extensionContext", [token]).catch(() => null);
|
|
6045
6322
|
if (!ctx) return fail("This coin does not run Evolve.");
|
|
6046
6323
|
if (!same(ctx[0], account.address)) return fail(`This wallet does not decide this coin: ${getAddress(ctx[0])} does (the wallet that launched it).`);
|
|
@@ -7067,18 +7344,21 @@ async function velocityLaunchCheck(args) {
|
|
|
7067
7344
|
return { asked: a.asked, ids };
|
|
7068
7345
|
}
|
|
7069
7346
|
|
|
7070
|
-
|
|
7347
|
+
/** Velocity on Evolve's contracts per Evolve plugin (v1's pins for v1 coins, the evolveV2 pins for v2 coins). */
|
|
7348
|
+
const velocityEvolveCache = new Map();
|
|
7071
7349
|
/**
|
|
7072
7350
|
* Velocity on Evolve once its code is there and it belongs together (Velocity runs on Evolve's EvolveExtensions, each
|
|
7073
7351
|
* companion follows this Velocity with its rank): { velocity, ext, poolManager, fees: [{ rank, ext }] }, or null. A
|
|
7074
7352
|
* companion that is not configured, has no code or is not this Velocity's is left out: the malus then stops below its
|
|
7075
7353
|
* percent.
|
|
7076
7354
|
*/
|
|
7077
|
-
async function velocityEvolveContracts() {
|
|
7078
|
-
if (
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7355
|
+
async function velocityEvolveContracts(evolvePlugin = PINNED.evolvePluginV1) {
|
|
7356
|
+
if (!evolvePlugin) return null;
|
|
7357
|
+
if (velocityEvolveCache.has(evolvePlugin)) return velocityEvolveCache.get(evolvePlugin);
|
|
7358
|
+
// v1's pins for a v1 coin, the evolveV2 pins for a v2 coin (`evolveExtensionPins`).
|
|
7359
|
+
const pins = same(evolvePlugin, PINNED.evolvePluginV1) ? PINNED : PINNED.evolveV2;
|
|
7360
|
+
const velocity = velocityAddress(pins.velocityEvolve);
|
|
7361
|
+
if (!velocity) return null;
|
|
7082
7362
|
const hasCode = async (a) => {
|
|
7083
7363
|
const code = await publicClient.getCode({ address: a }).catch(() => undefined);
|
|
7084
7364
|
return !!code && code !== "0x";
|
|
@@ -7094,8 +7374,8 @@ async function velocityEvolveContracts() {
|
|
|
7094
7374
|
if (!same(ext, extensions) || !same(evolve, evolvePlugin)) return null;
|
|
7095
7375
|
const fees = [];
|
|
7096
7376
|
for (const [rank, raw] of [
|
|
7097
|
-
[1,
|
|
7098
|
-
[2,
|
|
7377
|
+
[1, pins.velocityEvolveFee1],
|
|
7378
|
+
[2, pins.velocityEvolveFee2],
|
|
7099
7379
|
]) {
|
|
7100
7380
|
const a = velocityAddress(raw);
|
|
7101
7381
|
if (!a || !(await hasCode(a))) continue;
|
|
@@ -7106,8 +7386,9 @@ async function velocityEvolveContracts() {
|
|
|
7106
7386
|
]);
|
|
7107
7387
|
if (same(v, velocity) && Number(r) === rank && same(x, ext)) fees.push({ rank, ext: a });
|
|
7108
7388
|
}
|
|
7109
|
-
|
|
7110
|
-
|
|
7389
|
+
const found = { velocity, ext: getAddress(ext), poolManager: getAddress(poolManager), fees };
|
|
7390
|
+
velocityEvolveCache.set(evolvePlugin, found);
|
|
7391
|
+
return found;
|
|
7111
7392
|
} catch {
|
|
7112
7393
|
return null;
|
|
7113
7394
|
}
|
|
@@ -7348,7 +7629,7 @@ function velocityFailure(err) {
|
|
|
7348
7629
|
|
|
7349
7630
|
/** The coin's Evolve context, its slots and Velocity's catalog ids, for velocity_set and velocity_status: { … } or { error }. */
|
|
7350
7631
|
async function velocityCoin(c, token) {
|
|
7351
|
-
const plugin =
|
|
7632
|
+
const plugin = await evolvePluginFor(token);
|
|
7352
7633
|
const ctx = await read(plugin, evolveAbi, "extensionContext", [token]).catch(() => null);
|
|
7353
7634
|
if (!ctx) return { error: "This coin does not run Evolve. Velocity on Evolve is for Evolve coins; a coin without Evolve gets Velocity Fee at launch (launch_token velocity)." };
|
|
7354
7635
|
const [decider, delay, quoteAddress] = ctx;
|
|
@@ -7389,7 +7670,7 @@ server.registerTool(
|
|
|
7389
7670
|
},
|
|
7390
7671
|
serialized(async ({ token, preset, malusPct, cashback, cashbackPct, sharePct, slot, unplug, cancel, dryRun }) => {
|
|
7391
7672
|
if (!account) return needKey();
|
|
7392
|
-
const c = await velocityEvolveContracts();
|
|
7673
|
+
const c = await velocityEvolveContracts(await evolvePluginFor(token));
|
|
7393
7674
|
if (!c) return fail(VELOCITY_EVOLVE_OFF);
|
|
7394
7675
|
if (!isAddress(token)) return fail(`Not an address: ${token}`);
|
|
7395
7676
|
token = getAddress(token);
|
|
@@ -7539,7 +7820,7 @@ server.registerTool(
|
|
|
7539
7820
|
annotations: { readOnlyHint: true },
|
|
7540
7821
|
},
|
|
7541
7822
|
async ({ token, sellAmount, sellCoins }) => {
|
|
7542
|
-
const c = await velocityEvolveContracts();
|
|
7823
|
+
const c = await velocityEvolveContracts(await evolvePluginFor(token));
|
|
7543
7824
|
if (!c) return fail(VELOCITY_EVOLVE_OFF);
|
|
7544
7825
|
if (!isAddress(token)) return fail(`Not an address: ${token}`);
|
|
7545
7826
|
if (sellAmount !== undefined && sellCoins !== undefined) return fail("Give sellAmount or sellCoins, not both.");
|
|
@@ -7700,14 +7981,14 @@ server.registerTool(
|
|
|
7700
7981
|
annotations: { readOnlyHint: true },
|
|
7701
7982
|
},
|
|
7702
7983
|
async ({ token, wallet }) => {
|
|
7703
|
-
const c = await velocityEvolveContracts();
|
|
7984
|
+
const c = await velocityEvolveContracts(await evolvePluginFor(token));
|
|
7704
7985
|
if (!c) return fail(VELOCITY_EVOLVE_OFF);
|
|
7705
7986
|
if (!isAddress(token)) return fail(`Not an address: ${token}`);
|
|
7706
7987
|
if (wallet === undefined && !account) return needKey();
|
|
7707
7988
|
if (wallet !== undefined && !isAddress(wallet)) return fail(`Not an address: ${wallet}`);
|
|
7708
7989
|
token = getAddress(token);
|
|
7709
7990
|
const who = getAddress(wallet ?? account.address);
|
|
7710
|
-
const ctx = await read(
|
|
7991
|
+
const ctx = await read(await evolvePluginFor(token), evolveAbi, "extensionContext", [token]).catch(() => null);
|
|
7711
7992
|
if (!ctx) return fail("This coin does not run Evolve.");
|
|
7712
7993
|
const quote = await assetMeta(getAddress(ctx[2]));
|
|
7713
7994
|
const owed = await read(c.velocity, velocityEvolveAbi, "cashbackOf", [token, who]);
|
|
@@ -7736,13 +8017,13 @@ server.registerTool(
|
|
|
7736
8017
|
},
|
|
7737
8018
|
serialized(async ({ token, to, dryRun }) => {
|
|
7738
8019
|
if (!account) return needKey();
|
|
7739
|
-
const c = await velocityEvolveContracts();
|
|
8020
|
+
const c = await velocityEvolveContracts(await evolvePluginFor(token));
|
|
7740
8021
|
if (!c) return fail(VELOCITY_EVOLVE_OFF);
|
|
7741
8022
|
if (!isAddress(token)) return fail(`Not an address: ${token}`);
|
|
7742
8023
|
if (to !== undefined && !isAddress(to)) return fail(`Not an address: ${to}`);
|
|
7743
8024
|
token = getAddress(token);
|
|
7744
8025
|
const recipient = getAddress(to ?? account.address);
|
|
7745
|
-
const ctx = await read(
|
|
8026
|
+
const ctx = await read(await evolvePluginFor(token), evolveAbi, "extensionContext", [token]).catch(() => null);
|
|
7746
8027
|
if (!ctx) return fail("This coin does not run Evolve.");
|
|
7747
8028
|
const quote = await assetMeta(getAddress(ctx[2]));
|
|
7748
8029
|
const owed = await read(c.velocity, velocityEvolveAbi, "cashbackOf", [token, account.address]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hookit-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Let an AI agent launch a coin on hookit.fun (Ink) with its own hook modules and builders' Workshop hooks, find, buy and sell coins with ETH, USDG or tokenized stocks (AI-agents-only ones too, with a Hookit Agent Pass), redeem or borrow at their Backed Floor, collect its creator fees, steer the fee split, the fee and the hooks of the Evolve coins it launched, and deploy and submit its own hook to the Workshop.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": { "hookit-mcp": "index.js" },
|