web3agent 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 +62 -0
- package/WEB3_CONTEXT.md +91 -0
- package/dist/chunk-4GQR4EEY.js +8 -0
- package/dist/chunk-BF4PA46E.js +107 -0
- package/dist/index.js +44 -0
- package/dist/init-DKWQSYDZ.js +434 -0
- package/dist/startup-PHXZQ2SL.js +3912 -0
- package/package.json +80 -0
|
@@ -0,0 +1,3912 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
VERSION
|
|
4
|
+
} from "./chunk-4GQR4EEY.js";
|
|
5
|
+
import {
|
|
6
|
+
BLOCKSCOUT_DEFAULT_URL,
|
|
7
|
+
ETHERSCAN_DEFAULT_URL,
|
|
8
|
+
ValidationError,
|
|
9
|
+
getChainById,
|
|
10
|
+
getConfig,
|
|
11
|
+
isSupported,
|
|
12
|
+
parseEnv,
|
|
13
|
+
setConfig
|
|
14
|
+
} from "./chunk-BF4PA46E.js";
|
|
15
|
+
|
|
16
|
+
// src/config/health.ts
|
|
17
|
+
function formatHealthSummary(report) {
|
|
18
|
+
const lines = [
|
|
19
|
+
`[web3agent] Starting on chain ${report.activeChainId}, wallet: ${report.walletMode}, confirm: ${report.confirmWrites}`,
|
|
20
|
+
`[web3agent] Tools: ${report.totalToolCount} loaded`
|
|
21
|
+
];
|
|
22
|
+
if (report.degradedServices.length > 0) {
|
|
23
|
+
lines.push(`[web3agent] Degraded: ${report.degradedServices.join(", ")}`);
|
|
24
|
+
}
|
|
25
|
+
return lines.join("\n");
|
|
26
|
+
}
|
|
27
|
+
function createDefaultHealthStatus() {
|
|
28
|
+
return {
|
|
29
|
+
core: "ok",
|
|
30
|
+
blockscout: { name: "blockscout", status: "not_configured" },
|
|
31
|
+
etherscan: { name: "etherscan", status: "not_configured" },
|
|
32
|
+
evm: { name: "evm", status: "not_configured" },
|
|
33
|
+
goat: { name: "goat", status: "not_configured" },
|
|
34
|
+
lifi: { name: "lifi", status: "not_configured" },
|
|
35
|
+
orbs: { name: "orbs", status: "not_configured" }
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function createStartupReport(partial) {
|
|
39
|
+
return {
|
|
40
|
+
health: partial.health ?? createDefaultHealthStatus(),
|
|
41
|
+
totalToolCount: partial.totalToolCount ?? 0,
|
|
42
|
+
walletMode: partial.walletMode ?? "none",
|
|
43
|
+
confirmWrites: partial.confirmWrites ?? true,
|
|
44
|
+
activeChainId: partial.activeChainId ?? 8453,
|
|
45
|
+
degradedServices: partial.degradedServices ?? [],
|
|
46
|
+
fatalError: partial.fatalError
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/goat/provider.ts
|
|
51
|
+
import { getOnChainTools } from "@goat-sdk/adapter-model-context-protocol";
|
|
52
|
+
import { viem } from "@goat-sdk/wallet-viem";
|
|
53
|
+
import { generatePrivateKey as generatePrivateKey2, privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
54
|
+
|
|
55
|
+
// src/config/wallet-factory.ts
|
|
56
|
+
import { http, createWalletClient } from "viem";
|
|
57
|
+
var RELIABLE_FALLBACK_RPCS = {
|
|
58
|
+
56: "https://bsc-dataseed.bnbchain.org"
|
|
59
|
+
};
|
|
60
|
+
function getTransportForChain(chainId) {
|
|
61
|
+
const config = getConfig();
|
|
62
|
+
const perChainUrl = config.chainRpcUrls[chainId];
|
|
63
|
+
if (perChainUrl) return http(perChainUrl);
|
|
64
|
+
if (config.rpcUrl && chainId === config.chainId) return http(config.rpcUrl);
|
|
65
|
+
const fallback = RELIABLE_FALLBACK_RPCS[chainId];
|
|
66
|
+
if (fallback) return http(fallback);
|
|
67
|
+
return http();
|
|
68
|
+
}
|
|
69
|
+
function createWalletClientForChain(account, chainId) {
|
|
70
|
+
const chain = getChainById(chainId);
|
|
71
|
+
if (!chain) {
|
|
72
|
+
throw new Error(`Unsupported chain ID: ${chainId}`);
|
|
73
|
+
}
|
|
74
|
+
return createWalletClient({ account, chain, transport: getTransportForChain(chainId) });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/wallet/events.ts
|
|
78
|
+
import { EventEmitter } from "events";
|
|
79
|
+
var WalletEventEmitter = class extends EventEmitter {
|
|
80
|
+
emit(event, ...args) {
|
|
81
|
+
return super.emit(event, ...args);
|
|
82
|
+
}
|
|
83
|
+
on(event, listener) {
|
|
84
|
+
return super.on(event, listener);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var walletEvents = new WalletEventEmitter();
|
|
88
|
+
|
|
89
|
+
// src/wallet/persistence.ts
|
|
90
|
+
import { existsSync } from "fs";
|
|
91
|
+
import { mkdir, open, readFile, rename, unlink } from "fs/promises";
|
|
92
|
+
import { homedir } from "os";
|
|
93
|
+
import { join } from "path";
|
|
94
|
+
import { generatePrivateKey, mnemonicToAccount, privateKeyToAccount } from "viem/accounts";
|
|
95
|
+
var currentState = {
|
|
96
|
+
mode: "read-only",
|
|
97
|
+
chainId: 1,
|
|
98
|
+
accountIndex: 0,
|
|
99
|
+
addressIndex: 0
|
|
100
|
+
};
|
|
101
|
+
var currentAccount = null;
|
|
102
|
+
function getWalletDir() {
|
|
103
|
+
return join(homedir(), ".web3agent");
|
|
104
|
+
}
|
|
105
|
+
function getWalletPath() {
|
|
106
|
+
return join(getWalletDir(), "wallet.json");
|
|
107
|
+
}
|
|
108
|
+
function getWalletState() {
|
|
109
|
+
return { ...currentState };
|
|
110
|
+
}
|
|
111
|
+
function getActiveAccount() {
|
|
112
|
+
if (!currentAccount) {
|
|
113
|
+
throw new Error("Wallet not initialized \u2014 call initializeWallet() first");
|
|
114
|
+
}
|
|
115
|
+
return currentAccount;
|
|
116
|
+
}
|
|
117
|
+
function resolveFromPrivateKey(privateKey, chainId) {
|
|
118
|
+
const account = privateKeyToAccount(privateKey);
|
|
119
|
+
return {
|
|
120
|
+
account,
|
|
121
|
+
state: {
|
|
122
|
+
mode: "private-key",
|
|
123
|
+
address: account.address,
|
|
124
|
+
chainId,
|
|
125
|
+
accountIndex: 0,
|
|
126
|
+
addressIndex: 0
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function resolveFromMnemonic(mnemonic, chainId, accountIndex, addressIndex) {
|
|
131
|
+
const account = mnemonicToAccount(mnemonic, {
|
|
132
|
+
accountIndex,
|
|
133
|
+
addressIndex
|
|
134
|
+
});
|
|
135
|
+
return {
|
|
136
|
+
account,
|
|
137
|
+
state: {
|
|
138
|
+
mode: "mnemonic",
|
|
139
|
+
address: account.address,
|
|
140
|
+
chainId,
|
|
141
|
+
accountIndex,
|
|
142
|
+
addressIndex
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function resolveEphemeral(chainId) {
|
|
147
|
+
const key = generatePrivateKey();
|
|
148
|
+
const account = privateKeyToAccount(key);
|
|
149
|
+
return {
|
|
150
|
+
account,
|
|
151
|
+
state: {
|
|
152
|
+
mode: "read-only",
|
|
153
|
+
address: account.address,
|
|
154
|
+
chainId,
|
|
155
|
+
accountIndex: 0,
|
|
156
|
+
addressIndex: 0
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async function tryLoadPersistedWallet(chainId, accountIndex, addressIndex) {
|
|
161
|
+
const walletPath = getWalletPath();
|
|
162
|
+
const tmpPath = `${walletPath}.tmp`;
|
|
163
|
+
if (existsSync(tmpPath)) {
|
|
164
|
+
try {
|
|
165
|
+
await unlink(tmpPath);
|
|
166
|
+
} catch {
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (!existsSync(walletPath)) return null;
|
|
170
|
+
try {
|
|
171
|
+
const raw = await readFile(walletPath, "utf-8");
|
|
172
|
+
const data = JSON.parse(raw);
|
|
173
|
+
if (data.type === "private-key") {
|
|
174
|
+
return resolveFromPrivateKey(data.privateKey, chainId);
|
|
175
|
+
}
|
|
176
|
+
if (data.type === "mnemonic") {
|
|
177
|
+
return resolveFromMnemonic(
|
|
178
|
+
data.mnemonic,
|
|
179
|
+
chainId,
|
|
180
|
+
data.accountIndex ?? accountIndex,
|
|
181
|
+
data.addressIndex ?? addressIndex
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
async function initializeWallet(config) {
|
|
189
|
+
const { chainId, accountIndex, addressIndex } = config;
|
|
190
|
+
const envKey = config.privateKey;
|
|
191
|
+
if (envKey) {
|
|
192
|
+
const resolved = resolveFromPrivateKey(envKey, chainId);
|
|
193
|
+
currentAccount = resolved.account;
|
|
194
|
+
currentState = resolved.state;
|
|
195
|
+
walletEvents.emit("wallet-changed", currentState);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const envMnemonic = config.mnemonic;
|
|
199
|
+
if (envMnemonic) {
|
|
200
|
+
const resolved = resolveFromMnemonic(envMnemonic, chainId, accountIndex, addressIndex);
|
|
201
|
+
currentAccount = resolved.account;
|
|
202
|
+
currentState = resolved.state;
|
|
203
|
+
walletEvents.emit("wallet-changed", currentState);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const persisted = await tryLoadPersistedWallet(chainId, accountIndex, addressIndex);
|
|
207
|
+
if (persisted) {
|
|
208
|
+
currentAccount = persisted.account;
|
|
209
|
+
currentState = persisted.state;
|
|
210
|
+
walletEvents.emit("wallet-changed", currentState);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const ephemeral = resolveEphemeral(chainId);
|
|
214
|
+
currentAccount = ephemeral.account;
|
|
215
|
+
currentState = ephemeral.state;
|
|
216
|
+
walletEvents.emit("wallet-changed", currentState);
|
|
217
|
+
}
|
|
218
|
+
async function ensureWalletDir() {
|
|
219
|
+
const dir = getWalletDir();
|
|
220
|
+
if (!existsSync(dir)) {
|
|
221
|
+
await mkdir(dir, { recursive: true });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function persistWallet(data) {
|
|
225
|
+
await ensureWalletDir();
|
|
226
|
+
const walletPath = getWalletPath();
|
|
227
|
+
const tmpPath = `${walletPath}.tmp`;
|
|
228
|
+
const fd = await open(tmpPath, "w", 384);
|
|
229
|
+
try {
|
|
230
|
+
await fd.writeFile(JSON.stringify(data, null, 2));
|
|
231
|
+
await fd.sync();
|
|
232
|
+
} finally {
|
|
233
|
+
await fd.close();
|
|
234
|
+
}
|
|
235
|
+
await rename(tmpPath, walletPath);
|
|
236
|
+
}
|
|
237
|
+
async function activateWallet(params) {
|
|
238
|
+
const chainId = currentState.chainId;
|
|
239
|
+
const accountIndex = params.accountIndex ?? 0;
|
|
240
|
+
const addressIndex = params.addressIndex ?? 0;
|
|
241
|
+
if (params.privateKey) {
|
|
242
|
+
const resolved = resolveFromPrivateKey(params.privateKey, chainId);
|
|
243
|
+
await persistWallet({
|
|
244
|
+
type: "private-key",
|
|
245
|
+
privateKey: params.privateKey,
|
|
246
|
+
address: resolved.account.address
|
|
247
|
+
});
|
|
248
|
+
currentAccount = resolved.account;
|
|
249
|
+
currentState = resolved.state;
|
|
250
|
+
} else if (params.mnemonic) {
|
|
251
|
+
const resolved = resolveFromMnemonic(params.mnemonic, chainId, accountIndex, addressIndex);
|
|
252
|
+
await persistWallet({
|
|
253
|
+
type: "mnemonic",
|
|
254
|
+
mnemonic: params.mnemonic,
|
|
255
|
+
accountIndex,
|
|
256
|
+
addressIndex
|
|
257
|
+
});
|
|
258
|
+
currentAccount = resolved.account;
|
|
259
|
+
currentState = resolved.state;
|
|
260
|
+
} else {
|
|
261
|
+
throw new Error("Either privateKey or mnemonic must be provided");
|
|
262
|
+
}
|
|
263
|
+
walletEvents.emit("wallet-changed", currentState);
|
|
264
|
+
return getWalletState();
|
|
265
|
+
}
|
|
266
|
+
async function getPersistedKeyForSubprocess() {
|
|
267
|
+
if (process.env.PRIVATE_KEY) return process.env.PRIVATE_KEY;
|
|
268
|
+
if (process.env.MNEMONIC) {
|
|
269
|
+
const accountIndex = Number(process.env.WALLET_ACCOUNT_INDEX ?? 0);
|
|
270
|
+
const addressIndex = Number(process.env.WALLET_ADDRESS_INDEX ?? 0);
|
|
271
|
+
const account = mnemonicToAccount(process.env.MNEMONIC, { accountIndex, addressIndex });
|
|
272
|
+
const hdKey = account.getHdKey();
|
|
273
|
+
if (hdKey.privateKey) {
|
|
274
|
+
return `0x${Buffer.from(hdKey.privateKey).toString("hex")}`;
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
const walletPath = getWalletPath();
|
|
279
|
+
if (!existsSync(walletPath)) return null;
|
|
280
|
+
try {
|
|
281
|
+
const raw = await readFile(walletPath, "utf-8");
|
|
282
|
+
const data = JSON.parse(raw);
|
|
283
|
+
if (data.type === "private-key") return data.privateKey;
|
|
284
|
+
if (data.type === "mnemonic") {
|
|
285
|
+
const account = mnemonicToAccount(data.mnemonic, {
|
|
286
|
+
accountIndex: data.accountIndex ?? 0,
|
|
287
|
+
addressIndex: data.addressIndex ?? 0
|
|
288
|
+
});
|
|
289
|
+
const hdKey = account.getHdKey();
|
|
290
|
+
if (hdKey.privateKey) {
|
|
291
|
+
return `0x${Buffer.from(hdKey.privateKey).toString("hex")}`;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} catch {
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
async function deactivateWallet() {
|
|
299
|
+
const walletPath = getWalletPath();
|
|
300
|
+
if (existsSync(walletPath)) {
|
|
301
|
+
await unlink(walletPath);
|
|
302
|
+
}
|
|
303
|
+
const ephemeral = resolveEphemeral(currentState.chainId);
|
|
304
|
+
currentAccount = ephemeral.account;
|
|
305
|
+
currentState = ephemeral.state;
|
|
306
|
+
walletEvents.emit("wallet-changed", currentState);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/goat/plugins.ts
|
|
310
|
+
import { zeroEx } from "@goat-sdk/plugin-0x";
|
|
311
|
+
import { balancer } from "@goat-sdk/plugin-balancer";
|
|
312
|
+
import { coingecko } from "@goat-sdk/plugin-coingecko";
|
|
313
|
+
import { dexscreener } from "@goat-sdk/plugin-dexscreener";
|
|
314
|
+
import { ens } from "@goat-sdk/plugin-ens";
|
|
315
|
+
import { USDC, WETH, erc20 } from "@goat-sdk/plugin-erc20";
|
|
316
|
+
|
|
317
|
+
// node_modules/.pnpm/@goat-sdk+plugin-erc721@0.1.22_@goat-sdk+core@0.5.0_zod@3.25.76__bufferutil@4.1.0_typescript@_kuv3rgxdfmzbc55zxk3icop5ti/node_modules/@goat-sdk/plugin-erc721/dist/chunk-YSXGDEY5.mjs
|
|
318
|
+
var c = Object.defineProperty;
|
|
319
|
+
var d = (a2, b) => c(a2, "name", { value: b, configurable: true });
|
|
320
|
+
|
|
321
|
+
// node_modules/.pnpm/@goat-sdk+plugin-erc721@0.1.22_@goat-sdk+core@0.5.0_zod@3.25.76__bufferutil@4.1.0_typescript@_kuv3rgxdfmzbc55zxk3icop5ti/node_modules/@goat-sdk/plugin-erc721/dist/chunk-YVTV6MH2.mjs
|
|
322
|
+
var d2 = { symbol: "BAYC", name: "Bored Ape Yacht Club", chains: { 1: { contractAddress: "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D" } } };
|
|
323
|
+
var A = { symbol: "PUNK", name: "CryptoPunks", chains: { 1: { contractAddress: "0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB" } } };
|
|
324
|
+
function m(t2, c4) {
|
|
325
|
+
let s3 = [];
|
|
326
|
+
for (let o2 of c4) {
|
|
327
|
+
let e3 = o2.chains[t2];
|
|
328
|
+
e3 && s3.push({ chainId: t2, symbol: o2.symbol, name: o2.name, contractAddress: e3.contractAddress });
|
|
329
|
+
}
|
|
330
|
+
return s3;
|
|
331
|
+
}
|
|
332
|
+
d(m, "getTokensForNetwork");
|
|
333
|
+
|
|
334
|
+
// node_modules/.pnpm/@goat-sdk+plugin-erc721@0.1.22_@goat-sdk+core@0.5.0_zod@3.25.76__bufferutil@4.1.0_typescript@_kuv3rgxdfmzbc55zxk3icop5ti/node_modules/@goat-sdk/plugin-erc721/dist/chunk-Y4M3I524.mjs
|
|
335
|
+
import { parseAbi as e } from "viem";
|
|
336
|
+
var o = e(["function transferFrom(address from, address to, uint256 tokenId) external", "function safeTransferFrom(address from, address to, uint256 tokenId) external", "function safeTransferFrom(address from, address to, uint256 tokenId, bytes data) external", "function approve(address to, uint256 tokenId) external", "function setApprovalForAll(address operator, bool approved) external", "function getApproved(uint256 tokenId) external view returns (address)", "function isApprovedForAll(address owner, address operator) external view returns (bool)", "function balanceOf(address owner) external view returns (uint256)", "function ownerOf(uint256 tokenId) external view returns (address)"]);
|
|
337
|
+
|
|
338
|
+
// node_modules/.pnpm/@goat-sdk+plugin-erc721@0.1.22_@goat-sdk+core@0.5.0_zod@3.25.76__bufferutil@4.1.0_typescript@_kuv3rgxdfmzbc55zxk3icop5ti/node_modules/@goat-sdk/plugin-erc721/dist/chunk-7FXBTDRO.mjs
|
|
339
|
+
async function f(a2, o2, r) {
|
|
340
|
+
try {
|
|
341
|
+
return (await a2.read({ address: o2.contractAddress, abi: o, functionName: "balanceOf", args: [r.wallet] })).value.toString();
|
|
342
|
+
} catch (t2) {
|
|
343
|
+
throw Error(`Failed to fetch balance: ${t2}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
d(f, "balanceOf");
|
|
347
|
+
async function u(a2, o2, r) {
|
|
348
|
+
try {
|
|
349
|
+
let t2 = r.to;
|
|
350
|
+
return (await a2.sendTransaction({ to: o2.contractAddress, abi: o, functionName: "safeTransferFrom", args: [a2.getAddress(), t2, r.tokenId] })).hash;
|
|
351
|
+
} catch (t2) {
|
|
352
|
+
throw Error(`Failed to transfer: ${t2}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
d(u, "transfer");
|
|
356
|
+
async function l(a2, o2) {
|
|
357
|
+
try {
|
|
358
|
+
return (await a2.read({ address: o2.contractAddress, abi: o, functionName: "totalSupply" })).value.toString();
|
|
359
|
+
} catch (r) {
|
|
360
|
+
throw Error(`Failed to fetch total supply: ${r}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
d(l, "totalSupply");
|
|
364
|
+
async function p(a2, o2, r) {
|
|
365
|
+
try {
|
|
366
|
+
let t2 = r.spender;
|
|
367
|
+
return (await a2.sendTransaction({ to: o2.contractAddress, abi: o, functionName: "approve", args: [t2, r.tokenId] })).hash;
|
|
368
|
+
} catch (t2) {
|
|
369
|
+
throw Error(`Failed to approve: ${t2}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
d(p, "approve");
|
|
373
|
+
async function y(a2, o2, r) {
|
|
374
|
+
try {
|
|
375
|
+
let t2 = r.from, s3 = r.to;
|
|
376
|
+
return (await a2.sendTransaction({ to: o2.contractAddress, abi: o, functionName: "safeTransferFrom", args: [t2, s3, r.tokenId] })).hash;
|
|
377
|
+
} catch (t2) {
|
|
378
|
+
throw Error(`Failed to transfer from: ${t2}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
d(y, "transferFrom");
|
|
382
|
+
|
|
383
|
+
// node_modules/.pnpm/@goat-sdk+plugin-erc721@0.1.22_@goat-sdk+core@0.5.0_zod@3.25.76__bufferutil@4.1.0_typescript@_kuv3rgxdfmzbc55zxk3icop5ti/node_modules/@goat-sdk/plugin-erc721/dist/chunk-QJ4AOIPR.mjs
|
|
384
|
+
import { z as e2 } from "zod";
|
|
385
|
+
var s = e2.object({ wallet: e2.string().describe("The address to get the NFT balance of") });
|
|
386
|
+
var a = e2.object({ to: e2.string().describe("The address to transfer the NFT to"), tokenId: e2.string().describe("The ID of the NFT to transfer") });
|
|
387
|
+
var c2 = e2.object({});
|
|
388
|
+
var n = e2.object({ spender: e2.string().describe("The address to approve for the NFT"), tokenId: e2.string().describe("The ID of the NFT to approve") });
|
|
389
|
+
var h = e2.object({ from: e2.string().describe("The address to transfer the NFT from"), to: e2.string().describe("The address to transfer the NFT to"), tokenId: e2.string().describe("The ID of the NFT to transfer") });
|
|
390
|
+
var d3 = e2.object({ tokenId: e2.string().describe("The ID of the NFT to check ownership of") });
|
|
391
|
+
|
|
392
|
+
// node_modules/.pnpm/@goat-sdk+plugin-erc721@0.1.22_@goat-sdk+core@0.5.0_zod@3.25.76__bufferutil@4.1.0_typescript@_kuv3rgxdfmzbc55zxk3icop5ti/node_modules/@goat-sdk/plugin-erc721/dist/chunk-CBG3ZAQD.mjs
|
|
393
|
+
import { createTool as s2 } from "@goat-sdk/core";
|
|
394
|
+
function k(e3, b) {
|
|
395
|
+
let a2 = [];
|
|
396
|
+
for (let o2 of b) {
|
|
397
|
+
let d4 = s2({ name: `get_${o2.symbol}_balance`, description: `This {{tool}} gets the number of NFTs owned for ${o2.symbol}`, parameters: s }, (r) => f(e3, o2, r)), $ = s2({ name: `transfer_${o2.symbol}`, description: `This {{tool}} transfers a ${o2.symbol} NFT to the specified address`, parameters: a }, (r) => u(e3, o2, r)), u2 = s2({ name: `get_${o2.symbol}_total_supply`, description: `This {{tool}} gets the total supply of ${o2.symbol} NFTs`, parameters: c2 }, (r) => l(e3, o2)), _ = s2({ name: `approve_${o2.symbol}`, description: `This {{tool}} approves an address to transfer a specific ${o2.symbol} NFT`, parameters: n }, (r) => p(e3, o2, r)), F = s2({ name: `transfer_${o2.symbol}_from`, description: `This {{tool}} transfers a ${o2.symbol} NFT from one address to another`, parameters: h }, (r) => y(e3, o2, r));
|
|
398
|
+
a2.push(d4, $, u2, _, F);
|
|
399
|
+
}
|
|
400
|
+
return a2;
|
|
401
|
+
}
|
|
402
|
+
d(k, "getTools");
|
|
403
|
+
|
|
404
|
+
// node_modules/.pnpm/@goat-sdk+plugin-erc721@0.1.22_@goat-sdk+core@0.5.0_zod@3.25.76__bufferutil@4.1.0_typescript@_kuv3rgxdfmzbc55zxk3icop5ti/node_modules/@goat-sdk/plugin-erc721/dist/chunk-DH72JTG4.mjs
|
|
405
|
+
import { PluginBase as c3 } from "@goat-sdk/core";
|
|
406
|
+
var t = class extends c3 {
|
|
407
|
+
static {
|
|
408
|
+
d(this, "ERC721Plugin");
|
|
409
|
+
}
|
|
410
|
+
tokens;
|
|
411
|
+
constructor({ tokens: o2 }) {
|
|
412
|
+
super("erc721", []), this.tokens = o2;
|
|
413
|
+
}
|
|
414
|
+
supportsChain = d((o2) => o2.type === "evm", "supportsChain");
|
|
415
|
+
getTools(o2) {
|
|
416
|
+
let s3 = o2.getChain();
|
|
417
|
+
if (!s3.id) throw new Error("Network ID is required");
|
|
418
|
+
let p2 = m(s3.id, this.tokens);
|
|
419
|
+
return k(o2, p2);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
function w({ tokens: e3 }) {
|
|
423
|
+
return new t({ tokens: e3 });
|
|
424
|
+
}
|
|
425
|
+
d(w, "erc721");
|
|
426
|
+
|
|
427
|
+
// src/goat/plugins.ts
|
|
428
|
+
import { uniswap } from "@goat-sdk/plugin-uniswap";
|
|
429
|
+
function loadPlugins(options) {
|
|
430
|
+
const result = {
|
|
431
|
+
plugins: [],
|
|
432
|
+
loadedTier0: [],
|
|
433
|
+
loadedTier1: [],
|
|
434
|
+
loadedTier2: [],
|
|
435
|
+
failedPlugins: []
|
|
436
|
+
};
|
|
437
|
+
const tier0 = [
|
|
438
|
+
{
|
|
439
|
+
name: "erc20",
|
|
440
|
+
factory: () => erc20({ tokens: [USDC, WETH] })
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
name: "erc721",
|
|
444
|
+
factory: () => w({ tokens: [d2, A] })
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
name: "ens",
|
|
448
|
+
factory: () => ens({})
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
name: "dexscreener",
|
|
452
|
+
factory: () => dexscreener()
|
|
453
|
+
}
|
|
454
|
+
];
|
|
455
|
+
for (const { name, factory } of tier0) {
|
|
456
|
+
try {
|
|
457
|
+
result.plugins.push(factory());
|
|
458
|
+
result.loadedTier0.push(name);
|
|
459
|
+
} catch (e3) {
|
|
460
|
+
process.stderr.write(`[web3agent] GOAT plugin ${name} failed to load: ${e3}
|
|
461
|
+
`);
|
|
462
|
+
result.failedPlugins.push({ name, error: String(e3) });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (options.hasWallet) {
|
|
466
|
+
const tier1 = [
|
|
467
|
+
{
|
|
468
|
+
name: "uniswap",
|
|
469
|
+
factory: () => uniswap({
|
|
470
|
+
apiKey: "default",
|
|
471
|
+
baseUrl: "https://api.uniswap.org"
|
|
472
|
+
}),
|
|
473
|
+
condition: true
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: "balancer",
|
|
477
|
+
factory: () => balancer({ rpcUrl: options.rpcUrl ?? "https://eth.llamarpc.com" }),
|
|
478
|
+
condition: true
|
|
479
|
+
}
|
|
480
|
+
];
|
|
481
|
+
for (const { name, factory, condition } of tier1) {
|
|
482
|
+
if (!condition) continue;
|
|
483
|
+
try {
|
|
484
|
+
result.plugins.push(factory());
|
|
485
|
+
result.loadedTier1.push(name);
|
|
486
|
+
} catch (e3) {
|
|
487
|
+
process.stderr.write(`[web3agent] GOAT plugin ${name} failed to load: ${e3}
|
|
488
|
+
`);
|
|
489
|
+
result.failedPlugins.push({ name, error: String(e3) });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (options.coingeckoApiKey) {
|
|
494
|
+
try {
|
|
495
|
+
result.plugins.push(coingecko({ apiKey: options.coingeckoApiKey }));
|
|
496
|
+
result.loadedTier2.push("coingecko");
|
|
497
|
+
} catch (e3) {
|
|
498
|
+
process.stderr.write(`[web3agent] GOAT plugin coingecko failed to load: ${e3}
|
|
499
|
+
`);
|
|
500
|
+
result.failedPlugins.push({ name: "coingecko", error: String(e3) });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (options.zeroxApiKey) {
|
|
504
|
+
try {
|
|
505
|
+
result.plugins.push(zeroEx({ apiKey: options.zeroxApiKey }));
|
|
506
|
+
result.loadedTier2.push("0x");
|
|
507
|
+
} catch (e3) {
|
|
508
|
+
process.stderr.write(`[web3agent] GOAT plugin 0x failed to load: ${e3}
|
|
509
|
+
`);
|
|
510
|
+
result.failedPlugins.push({ name: "0x", error: String(e3) });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return result;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/goat/provider.ts
|
|
517
|
+
var GoatProvider = class {
|
|
518
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
519
|
+
pluginResult;
|
|
520
|
+
referenceSnapshot;
|
|
521
|
+
generation = 0;
|
|
522
|
+
initConfig;
|
|
523
|
+
walletChangeHandler;
|
|
524
|
+
rebuildPromise = Promise.resolve();
|
|
525
|
+
async initialize(config) {
|
|
526
|
+
this.initConfig = config;
|
|
527
|
+
const walletState = getWalletState();
|
|
528
|
+
const hasWallet = walletState.mode !== "read-only";
|
|
529
|
+
this.pluginResult = loadPlugins({
|
|
530
|
+
hasWallet,
|
|
531
|
+
zeroxApiKey: config.zeroxApiKey,
|
|
532
|
+
coingeckoApiKey: config.coingeckoApiKey,
|
|
533
|
+
rpcUrl: config.rpcUrl
|
|
534
|
+
});
|
|
535
|
+
const defaultChainId = walletState.chainId;
|
|
536
|
+
await this.buildSnapshot(defaultChainId);
|
|
537
|
+
this.referenceSnapshot = this.snapshots.get(defaultChainId);
|
|
538
|
+
this.walletChangeHandler = (state) => {
|
|
539
|
+
this.generation++;
|
|
540
|
+
const gen = this.generation;
|
|
541
|
+
const hasWallet2 = state.mode !== "read-only";
|
|
542
|
+
this.pluginResult = loadPlugins({
|
|
543
|
+
hasWallet: hasWallet2,
|
|
544
|
+
zeroxApiKey: this.initConfig?.zeroxApiKey,
|
|
545
|
+
coingeckoApiKey: this.initConfig?.coingeckoApiKey,
|
|
546
|
+
rpcUrl: this.initConfig?.rpcUrl
|
|
547
|
+
});
|
|
548
|
+
this.rebuildPromise = this.rebuildPromise.catch(() => {
|
|
549
|
+
}).then(() => this.buildSnapshot(state.chainId)).then(() => {
|
|
550
|
+
if (this.generation !== gen) return;
|
|
551
|
+
this.referenceSnapshot = this.snapshots.get(state.chainId);
|
|
552
|
+
process.stderr.write(
|
|
553
|
+
`[goat] Rebuilt snapshot for chain ${state.chainId} after wallet change
|
|
554
|
+
`
|
|
555
|
+
);
|
|
556
|
+
}).catch((e3) => {
|
|
557
|
+
process.stderr.write(`[goat] Failed to rebuild snapshot after wallet change: ${e3}
|
|
558
|
+
`);
|
|
559
|
+
});
|
|
560
|
+
};
|
|
561
|
+
walletEvents.on("wallet-changed", this.walletChangeHandler);
|
|
562
|
+
}
|
|
563
|
+
/** Wait for any in-flight rebuild triggered by a wallet change. */
|
|
564
|
+
async waitForRebuild() {
|
|
565
|
+
await this.rebuildPromise;
|
|
566
|
+
}
|
|
567
|
+
shutdown() {
|
|
568
|
+
if (this.walletChangeHandler) {
|
|
569
|
+
walletEvents.off("wallet-changed", this.walletChangeHandler);
|
|
570
|
+
this.walletChangeHandler = void 0;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
async buildSnapshot(chainId) {
|
|
574
|
+
if (!this.pluginResult) {
|
|
575
|
+
throw new Error("GoatProvider not initialized \u2014 call initialize() first");
|
|
576
|
+
}
|
|
577
|
+
let account;
|
|
578
|
+
const walletState = getWalletState();
|
|
579
|
+
if (walletState.mode !== "read-only") {
|
|
580
|
+
account = getActiveAccount();
|
|
581
|
+
} else {
|
|
582
|
+
const ephemeralKey = generatePrivateKey2();
|
|
583
|
+
account = privateKeyToAccount2(ephemeralKey);
|
|
584
|
+
}
|
|
585
|
+
const walletClient = createWalletClientForChain(account, chainId);
|
|
586
|
+
const tools = await getOnChainTools({
|
|
587
|
+
wallet: viem(walletClient),
|
|
588
|
+
plugins: this.pluginResult.plugins
|
|
589
|
+
// biome-ignore lint/suspicious/noExplicitAny: GOAT SDK generics require flexible typing for cross-plugin wallet compat
|
|
590
|
+
});
|
|
591
|
+
this.snapshots.set(chainId, {
|
|
592
|
+
listOfTools: tools.listOfTools(),
|
|
593
|
+
toolHandler: tools.toolHandler,
|
|
594
|
+
chainId
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
async getOrBuildSnapshot(chainId) {
|
|
598
|
+
const existing = this.snapshots.get(chainId);
|
|
599
|
+
if (existing) return existing;
|
|
600
|
+
try {
|
|
601
|
+
await this.buildSnapshot(chainId);
|
|
602
|
+
return this.snapshots.get(chainId);
|
|
603
|
+
} catch (e3) {
|
|
604
|
+
process.stderr.write(`[web3agent] GOAT snapshot failed for chain ${chainId}: ${e3}
|
|
605
|
+
`);
|
|
606
|
+
return void 0;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
getReferenceSnapshot() {
|
|
610
|
+
return this.referenceSnapshot;
|
|
611
|
+
}
|
|
612
|
+
getAllToolNames() {
|
|
613
|
+
if (this.referenceSnapshot) {
|
|
614
|
+
return this.referenceSnapshot.listOfTools.map((t2) => t2.name);
|
|
615
|
+
}
|
|
616
|
+
return [];
|
|
617
|
+
}
|
|
618
|
+
getLoadedPlugins() {
|
|
619
|
+
if (!this.pluginResult) return [];
|
|
620
|
+
return [
|
|
621
|
+
...this.pluginResult.loadedTier0,
|
|
622
|
+
...this.pluginResult.loadedTier1,
|
|
623
|
+
...this.pluginResult.loadedTier2
|
|
624
|
+
];
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
var goatProvider = new GoatProvider();
|
|
628
|
+
|
|
629
|
+
// src/lifi/config.ts
|
|
630
|
+
import { EVM, createConfig } from "@lifi/sdk";
|
|
631
|
+
var isConfigured = false;
|
|
632
|
+
function initializeLifi(apiKey) {
|
|
633
|
+
if (isConfigured) return;
|
|
634
|
+
createConfig({
|
|
635
|
+
integrator: "web3agent",
|
|
636
|
+
...apiKey ? { apiKey } : {},
|
|
637
|
+
providers: [
|
|
638
|
+
EVM({
|
|
639
|
+
getWalletClient: async () => {
|
|
640
|
+
const account = getActiveAccount();
|
|
641
|
+
const walletState = getWalletState();
|
|
642
|
+
return createWalletClientForChain(account, walletState.chainId);
|
|
643
|
+
},
|
|
644
|
+
switchChain: async (chainId) => {
|
|
645
|
+
const account = getActiveAccount();
|
|
646
|
+
return createWalletClientForChain(account, chainId);
|
|
647
|
+
}
|
|
648
|
+
})
|
|
649
|
+
]
|
|
650
|
+
});
|
|
651
|
+
isConfigured = true;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/tools/lifi/index.ts
|
|
655
|
+
import { convertQuoteToRoute, executeRoute, getChains, getQuote } from "@lifi/sdk";
|
|
656
|
+
|
|
657
|
+
// src/utils/errors.ts
|
|
658
|
+
function formatToolError(code, message, details) {
|
|
659
|
+
return {
|
|
660
|
+
content: [
|
|
661
|
+
{
|
|
662
|
+
type: "text",
|
|
663
|
+
text: JSON.stringify({ error: code, message, details })
|
|
664
|
+
}
|
|
665
|
+
],
|
|
666
|
+
isError: true
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
function formatToolResponse(data) {
|
|
670
|
+
return {
|
|
671
|
+
content: [
|
|
672
|
+
{
|
|
673
|
+
type: "text",
|
|
674
|
+
text: typeof data === "string" ? data : JSON.stringify(data, null, 2)
|
|
675
|
+
}
|
|
676
|
+
],
|
|
677
|
+
isError: false
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/utils/validation.ts
|
|
682
|
+
function validateInput(schema, input) {
|
|
683
|
+
const result = schema.safeParse(input);
|
|
684
|
+
if (result.success) {
|
|
685
|
+
return { success: true, data: result.data };
|
|
686
|
+
}
|
|
687
|
+
const messages = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`);
|
|
688
|
+
return {
|
|
689
|
+
success: false,
|
|
690
|
+
error: formatToolError("INVALID_PARAMS", messages.join("; "))
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// src/wallet/confirmation.ts
|
|
695
|
+
import { randomUUID } from "crypto";
|
|
696
|
+
import { existsSync as existsSync2 } from "fs";
|
|
697
|
+
import { mkdir as mkdir2, open as open2, readFile as readFile2, rename as rename2 } from "fs/promises";
|
|
698
|
+
import { homedir as homedir2 } from "os";
|
|
699
|
+
import { join as join2 } from "path";
|
|
700
|
+
var THIRTY_MINUTES_MS = 30 * 60 * 1e3;
|
|
701
|
+
var executorRegistry = /* @__PURE__ */ new Map();
|
|
702
|
+
function registerExecutor(type, fn) {
|
|
703
|
+
executorRegistry.set(type, fn);
|
|
704
|
+
}
|
|
705
|
+
function getPendingOpsPath() {
|
|
706
|
+
return join2(homedir2(), ".web3agent", "pending-ops.json");
|
|
707
|
+
}
|
|
708
|
+
var ConfirmationQueueManager = class {
|
|
709
|
+
queue = /* @__PURE__ */ new Map();
|
|
710
|
+
enabled;
|
|
711
|
+
constructor(enabled) {
|
|
712
|
+
this.enabled = enabled;
|
|
713
|
+
}
|
|
714
|
+
enqueue(type, description, params, executor, walletAddress) {
|
|
715
|
+
if (!this.enabled) {
|
|
716
|
+
return {
|
|
717
|
+
queued: false,
|
|
718
|
+
id: null,
|
|
719
|
+
summary: `Confirmation bypassed: ${description}`
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
const id = randomUUID();
|
|
723
|
+
const operation = {
|
|
724
|
+
id,
|
|
725
|
+
type,
|
|
726
|
+
description,
|
|
727
|
+
params,
|
|
728
|
+
executor,
|
|
729
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
730
|
+
ttlMs: THIRTY_MINUTES_MS,
|
|
731
|
+
walletAddress
|
|
732
|
+
};
|
|
733
|
+
this.queue.set(id, operation);
|
|
734
|
+
this.persistQueue().catch((e3) => {
|
|
735
|
+
process.stderr.write(`[confirmation] Failed to persist queue: ${e3}
|
|
736
|
+
`);
|
|
737
|
+
});
|
|
738
|
+
return {
|
|
739
|
+
queued: true,
|
|
740
|
+
id,
|
|
741
|
+
summary: `Queued [${type}]: ${description} \u2014 confirm with ID: ${id}`
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
confirm(id) {
|
|
745
|
+
const operation = this.queue.get(id);
|
|
746
|
+
if (!operation) return null;
|
|
747
|
+
const elapsed = Date.now() - operation.createdAt.getTime();
|
|
748
|
+
const stale = elapsed > operation.ttlMs;
|
|
749
|
+
return { operation, stale };
|
|
750
|
+
}
|
|
751
|
+
complete(id) {
|
|
752
|
+
this.queue.delete(id);
|
|
753
|
+
this.persistQueue().catch((e3) => {
|
|
754
|
+
process.stderr.write(`[confirmation] Failed to persist queue: ${e3}
|
|
755
|
+
`);
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
deny(id) {
|
|
759
|
+
const removed = this.queue.delete(id);
|
|
760
|
+
if (removed)
|
|
761
|
+
this.persistQueue().catch((e3) => {
|
|
762
|
+
process.stderr.write(`[confirmation] Failed to persist queue: ${e3}
|
|
763
|
+
`);
|
|
764
|
+
});
|
|
765
|
+
return removed;
|
|
766
|
+
}
|
|
767
|
+
list() {
|
|
768
|
+
return [...this.queue.values()];
|
|
769
|
+
}
|
|
770
|
+
pruneExpired() {
|
|
771
|
+
const now = Date.now();
|
|
772
|
+
let pruned = false;
|
|
773
|
+
for (const [id, op] of this.queue) {
|
|
774
|
+
if (now - op.createdAt.getTime() > op.ttlMs) {
|
|
775
|
+
this.queue.delete(id);
|
|
776
|
+
pruned = true;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (pruned)
|
|
780
|
+
this.persistQueue().catch((e3) => {
|
|
781
|
+
process.stderr.write(`[confirmation] Failed to persist queue: ${e3}
|
|
782
|
+
`);
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
flushAll() {
|
|
786
|
+
const count = this.queue.size;
|
|
787
|
+
this.queue.clear();
|
|
788
|
+
this.persistQueue().catch((e3) => {
|
|
789
|
+
process.stderr.write(`[confirmation] Failed to persist queue: ${e3}
|
|
790
|
+
`);
|
|
791
|
+
});
|
|
792
|
+
return count;
|
|
793
|
+
}
|
|
794
|
+
async persistQueue() {
|
|
795
|
+
const ops = [...this.queue.values()].map((op) => ({
|
|
796
|
+
id: op.id,
|
|
797
|
+
type: op.type,
|
|
798
|
+
description: op.description,
|
|
799
|
+
params: op.params,
|
|
800
|
+
createdAt: op.createdAt.toISOString(),
|
|
801
|
+
ttlMs: op.ttlMs,
|
|
802
|
+
walletAddress: op.walletAddress
|
|
803
|
+
}));
|
|
804
|
+
const filePath = getPendingOpsPath();
|
|
805
|
+
const dir = join2(homedir2(), ".web3agent");
|
|
806
|
+
if (!existsSync2(dir)) {
|
|
807
|
+
await mkdir2(dir, { recursive: true });
|
|
808
|
+
}
|
|
809
|
+
const tmpPath = `${filePath}.tmp`;
|
|
810
|
+
const fd = await open2(tmpPath, "w", 384);
|
|
811
|
+
try {
|
|
812
|
+
await fd.writeFile(JSON.stringify(ops, null, 2));
|
|
813
|
+
await fd.sync();
|
|
814
|
+
} finally {
|
|
815
|
+
await fd.close();
|
|
816
|
+
}
|
|
817
|
+
await rename2(tmpPath, filePath);
|
|
818
|
+
}
|
|
819
|
+
async loadQueue() {
|
|
820
|
+
const filePath = getPendingOpsPath();
|
|
821
|
+
if (!existsSync2(filePath)) return;
|
|
822
|
+
try {
|
|
823
|
+
const raw = await readFile2(filePath, "utf-8");
|
|
824
|
+
const ops = JSON.parse(raw);
|
|
825
|
+
const now = Date.now();
|
|
826
|
+
for (const serialized of ops) {
|
|
827
|
+
const createdAt = new Date(serialized.createdAt);
|
|
828
|
+
const elapsed = now - createdAt.getTime();
|
|
829
|
+
if (elapsed > serialized.ttlMs) continue;
|
|
830
|
+
const executor = executorRegistry.get(serialized.type);
|
|
831
|
+
if (!executor) {
|
|
832
|
+
process.stderr.write(
|
|
833
|
+
`[confirmation] Skipping persisted op ${serialized.id}: no executor for type '${serialized.type}'
|
|
834
|
+
`
|
|
835
|
+
);
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
this.queue.set(serialized.id, {
|
|
839
|
+
id: serialized.id,
|
|
840
|
+
type: serialized.type,
|
|
841
|
+
description: serialized.description,
|
|
842
|
+
params: serialized.params,
|
|
843
|
+
executor,
|
|
844
|
+
createdAt,
|
|
845
|
+
ttlMs: serialized.ttlMs,
|
|
846
|
+
walletAddress: serialized.walletAddress
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
if (this.queue.size > 0) {
|
|
850
|
+
process.stderr.write(
|
|
851
|
+
`[confirmation] Restored ${this.queue.size} pending operation(s) from disk
|
|
852
|
+
`
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
} catch (e3) {
|
|
856
|
+
process.stderr.write(
|
|
857
|
+
`[confirmation] Failed to load persisted queue (starting fresh): ${e3}
|
|
858
|
+
`
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
var confirmationQueue = new ConfirmationQueueManager(true);
|
|
864
|
+
|
|
865
|
+
// src/utils/write.ts
|
|
866
|
+
async function executeWrite(options) {
|
|
867
|
+
const walletState = getWalletState();
|
|
868
|
+
if (walletState.mode === "read-only") {
|
|
869
|
+
return formatToolError(
|
|
870
|
+
"WALLET_READ_ONLY",
|
|
871
|
+
`${options.toolName} requires an active wallet. Use wallet_generate or import a key first.`
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
const { queued, id, summary } = confirmationQueue.enqueue(
|
|
875
|
+
options.toolName,
|
|
876
|
+
options.description,
|
|
877
|
+
options.params,
|
|
878
|
+
options.executor,
|
|
879
|
+
walletState.address
|
|
880
|
+
);
|
|
881
|
+
if (queued) {
|
|
882
|
+
return formatToolResponse({ status: "pending_confirmation", id, summary });
|
|
883
|
+
}
|
|
884
|
+
return options.executor(options.params);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// src/tools/lifi/schemas.ts
|
|
888
|
+
import { z } from "zod";
|
|
889
|
+
var lifiGetQuoteSchema = z.object({
|
|
890
|
+
fromChainId: z.number({ required_error: "fromChainId is required" }),
|
|
891
|
+
toChainId: z.number({ required_error: "toChainId is required" }),
|
|
892
|
+
fromTokenAddress: z.string({ required_error: "fromTokenAddress is required" }),
|
|
893
|
+
toTokenAddress: z.string({ required_error: "toTokenAddress is required" }),
|
|
894
|
+
fromAmount: z.string({ required_error: "fromAmount is required" })
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
// src/tools/lifi/index.ts
|
|
898
|
+
async function lifiGetChains(_params) {
|
|
899
|
+
try {
|
|
900
|
+
const chains = await getChains();
|
|
901
|
+
const summary = chains.map((c4) => ({
|
|
902
|
+
id: c4.id,
|
|
903
|
+
name: c4.name,
|
|
904
|
+
nativeToken: c4.nativeToken?.symbol
|
|
905
|
+
}));
|
|
906
|
+
return formatToolResponse(summary);
|
|
907
|
+
} catch (e3) {
|
|
908
|
+
return formatToolError("LIFI_ERROR", String(e3));
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
async function lifiGetQuote(params) {
|
|
912
|
+
const v = validateInput(lifiGetQuoteSchema, params);
|
|
913
|
+
if (!v.success) return v.error;
|
|
914
|
+
const { fromChainId, toChainId, fromTokenAddress, toTokenAddress, fromAmount } = v.data;
|
|
915
|
+
try {
|
|
916
|
+
const walletState = getWalletState();
|
|
917
|
+
const quote = await getQuote({
|
|
918
|
+
fromChain: fromChainId,
|
|
919
|
+
toChain: toChainId,
|
|
920
|
+
fromToken: fromTokenAddress,
|
|
921
|
+
toToken: toTokenAddress,
|
|
922
|
+
fromAmount,
|
|
923
|
+
fromAddress: walletState.address ?? "0x0000000000000000000000000000000000000000"
|
|
924
|
+
});
|
|
925
|
+
const trimmed = {
|
|
926
|
+
fromChainId: quote.action.fromChainId,
|
|
927
|
+
toChainId: quote.action.toChainId,
|
|
928
|
+
fromToken: quote.action.fromToken?.symbol,
|
|
929
|
+
toToken: quote.action.toToken?.symbol,
|
|
930
|
+
fromAmount: quote.action.fromAmount,
|
|
931
|
+
fromAmountUSD: quote.estimate?.fromAmountUSD,
|
|
932
|
+
toAmount: quote.estimate?.toAmount,
|
|
933
|
+
toAmountUSD: quote.estimate?.toAmountUSD,
|
|
934
|
+
toAmountMin: quote.estimate?.toAmountMin,
|
|
935
|
+
gasCostUSD: quote.estimate?.gasCosts?.[0]?.amountUSD,
|
|
936
|
+
estimatedDurationSeconds: quote.estimate?.executionDuration,
|
|
937
|
+
includedSteps: quote.includedSteps?.map((s3) => ({
|
|
938
|
+
type: s3.type,
|
|
939
|
+
tool: s3.tool
|
|
940
|
+
}))
|
|
941
|
+
};
|
|
942
|
+
return formatToolResponse(trimmed);
|
|
943
|
+
} catch (e3) {
|
|
944
|
+
return formatToolError("LIFI_QUOTE_ERROR", String(e3));
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
async function lifiExecuteBridge(params) {
|
|
948
|
+
const v = validateInput(lifiGetQuoteSchema, params);
|
|
949
|
+
if (!v.success) return v.error;
|
|
950
|
+
const { fromChainId, toChainId, fromTokenAddress, toTokenAddress, fromAmount } = v.data;
|
|
951
|
+
return executeWrite({
|
|
952
|
+
toolName: "lifi_execute_bridge",
|
|
953
|
+
description: `Bridge ${fromAmount} from chain ${fromChainId} to chain ${toChainId}`,
|
|
954
|
+
params: v.data,
|
|
955
|
+
executor: executeBridgeNow
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
async function executeBridgeNow(params) {
|
|
959
|
+
try {
|
|
960
|
+
const { fromChainId, toChainId, fromTokenAddress, toTokenAddress, fromAmount } = params;
|
|
961
|
+
const walletState = getWalletState();
|
|
962
|
+
const quote = await getQuote({
|
|
963
|
+
fromChain: fromChainId,
|
|
964
|
+
toChain: toChainId,
|
|
965
|
+
fromToken: fromTokenAddress,
|
|
966
|
+
toToken: toTokenAddress,
|
|
967
|
+
fromAmount,
|
|
968
|
+
fromAddress: walletState.address ?? "0x0000000000000000000000000000000000000000"
|
|
969
|
+
});
|
|
970
|
+
const route = convertQuoteToRoute(quote);
|
|
971
|
+
await executeRoute(route, {
|
|
972
|
+
updateRouteHook: (updatedRoute) => {
|
|
973
|
+
const step = updatedRoute.steps?.[0];
|
|
974
|
+
if (step?.execution) {
|
|
975
|
+
process.stderr.write(
|
|
976
|
+
`[web3agent] Bridge progress: ${JSON.stringify(step.execution.process)}
|
|
977
|
+
`
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
return formatToolResponse({
|
|
983
|
+
status: "completed",
|
|
984
|
+
message: "Bridge executed successfully"
|
|
985
|
+
});
|
|
986
|
+
} catch (e3) {
|
|
987
|
+
return formatToolError("BRIDGE_ERROR", String(e3));
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
function getLifiToolDefinitions() {
|
|
991
|
+
return [
|
|
992
|
+
{
|
|
993
|
+
name: "lifi_get_chains",
|
|
994
|
+
description: "Get list of chains supported by LI.FI for cross-chain bridging",
|
|
995
|
+
inputSchema: {
|
|
996
|
+
type: "object",
|
|
997
|
+
properties: {},
|
|
998
|
+
required: []
|
|
999
|
+
},
|
|
1000
|
+
handler: lifiGetChains
|
|
1001
|
+
},
|
|
1002
|
+
{
|
|
1003
|
+
name: "lifi_get_quote",
|
|
1004
|
+
description: "Get a cross-chain bridge/swap quote from LI.FI. Supports 20+ EVM chains including Ethereum, BSC, Polygon, Arbitrum, Optimism, Base, Linea, Avalanche, zkSync, Scroll, Gnosis, and more. Requires token addresses \u2014 use resolve_token first to get addresses.",
|
|
1005
|
+
inputSchema: {
|
|
1006
|
+
type: "object",
|
|
1007
|
+
properties: {
|
|
1008
|
+
fromChainId: {
|
|
1009
|
+
type: "number",
|
|
1010
|
+
description: "Source chain ID"
|
|
1011
|
+
},
|
|
1012
|
+
toChainId: {
|
|
1013
|
+
type: "number",
|
|
1014
|
+
description: "Destination chain ID"
|
|
1015
|
+
},
|
|
1016
|
+
fromTokenAddress: {
|
|
1017
|
+
type: "string",
|
|
1018
|
+
description: "Source token address"
|
|
1019
|
+
},
|
|
1020
|
+
toTokenAddress: {
|
|
1021
|
+
type: "string",
|
|
1022
|
+
description: "Destination token address"
|
|
1023
|
+
},
|
|
1024
|
+
fromAmount: {
|
|
1025
|
+
type: "string",
|
|
1026
|
+
description: "Amount in wei"
|
|
1027
|
+
}
|
|
1028
|
+
},
|
|
1029
|
+
required: ["fromChainId", "toChainId", "fromTokenAddress", "toTokenAddress", "fromAmount"]
|
|
1030
|
+
},
|
|
1031
|
+
handler: lifiGetQuote
|
|
1032
|
+
},
|
|
1033
|
+
{
|
|
1034
|
+
name: "lifi_execute_bridge",
|
|
1035
|
+
description: "Execute a cross-chain bridge (write operation, requires wallet and confirmation). Requires token addresses \u2014 use resolve_token first to get addresses.",
|
|
1036
|
+
inputSchema: {
|
|
1037
|
+
type: "object",
|
|
1038
|
+
properties: {
|
|
1039
|
+
fromChainId: { type: "number" },
|
|
1040
|
+
toChainId: { type: "number" },
|
|
1041
|
+
fromTokenAddress: { type: "string" },
|
|
1042
|
+
toTokenAddress: { type: "string" },
|
|
1043
|
+
fromAmount: { type: "string" }
|
|
1044
|
+
},
|
|
1045
|
+
required: ["fromChainId", "toChainId", "fromTokenAddress", "toTokenAddress", "fromAmount"]
|
|
1046
|
+
},
|
|
1047
|
+
handler: lifiExecuteBridge
|
|
1048
|
+
}
|
|
1049
|
+
];
|
|
1050
|
+
}
|
|
1051
|
+
function registerLifiExecutors() {
|
|
1052
|
+
registerExecutor("lifi_execute_bridge", executeBridgeNow);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// src/orbs/chains.ts
|
|
1056
|
+
import { Configs } from "@orbs-network/twap-sdk";
|
|
1057
|
+
var LIQUIDITY_HUB_CHAINS = [
|
|
1058
|
+
137,
|
|
1059
|
+
// Polygon
|
|
1060
|
+
56,
|
|
1061
|
+
// BSC
|
|
1062
|
+
8453,
|
|
1063
|
+
// Base
|
|
1064
|
+
59144,
|
|
1065
|
+
// Linea
|
|
1066
|
+
81457,
|
|
1067
|
+
// Blast
|
|
1068
|
+
42161
|
|
1069
|
+
// Arbitrum
|
|
1070
|
+
];
|
|
1071
|
+
var TWAP_CHAIN_IDS = new Set(Object.values(Configs).map((c4) => c4.chainId));
|
|
1072
|
+
function isLiquidityHubSupported(chainId) {
|
|
1073
|
+
return LIQUIDITY_HUB_CHAINS.includes(chainId);
|
|
1074
|
+
}
|
|
1075
|
+
function isTwapSupported(chainId) {
|
|
1076
|
+
return TWAP_CHAIN_IDS.has(chainId);
|
|
1077
|
+
}
|
|
1078
|
+
function getLiquidityHubError(chainId) {
|
|
1079
|
+
const names = LIQUIDITY_HUB_CHAINS.map(
|
|
1080
|
+
(id) => `${getChainById(id)?.name ?? String(id)} (${id})`
|
|
1081
|
+
).join(", ");
|
|
1082
|
+
return `Orbs Liquidity Hub is not available on chain ${chainId}. Supported: ${names}`;
|
|
1083
|
+
}
|
|
1084
|
+
function getTwapError(chainId) {
|
|
1085
|
+
return `Orbs dTWAP/dLIMIT is not available on chain ${chainId}. Use isTwapSupported() to check availability.`;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// src/orbs/dsltp.ts
|
|
1089
|
+
var DSLTP_AVAILABLE = false;
|
|
1090
|
+
function getDsltpToolDefinitions() {
|
|
1091
|
+
if (!DSLTP_AVAILABLE) return [];
|
|
1092
|
+
return [];
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// src/orbs/liquidity-hub.ts
|
|
1096
|
+
import { constructSDK } from "@orbs-network/liquidity-hub-sdk";
|
|
1097
|
+
import { createPublicClient, maxUint256 } from "viem";
|
|
1098
|
+
if (!("localStorage" in globalThis)) {
|
|
1099
|
+
const noop = () => void 0;
|
|
1100
|
+
Object.defineProperty(globalThis, "localStorage", {
|
|
1101
|
+
value: {
|
|
1102
|
+
getItem: () => null,
|
|
1103
|
+
setItem: noop,
|
|
1104
|
+
removeItem: noop,
|
|
1105
|
+
clear: noop,
|
|
1106
|
+
key: () => null,
|
|
1107
|
+
length: 0
|
|
1108
|
+
},
|
|
1109
|
+
writable: true,
|
|
1110
|
+
configurable: true
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
function normalizeEip712ForSigning(domain, types, primaryType, message) {
|
|
1114
|
+
function normalizeValue(fieldType, value) {
|
|
1115
|
+
if (value == null) return value;
|
|
1116
|
+
if (fieldType.endsWith("[]")) {
|
|
1117
|
+
const elementType = fieldType.slice(0, -2);
|
|
1118
|
+
if (!Array.isArray(value)) return value;
|
|
1119
|
+
return value.map((item) => normalizeValue(elementType, item));
|
|
1120
|
+
}
|
|
1121
|
+
if (types[fieldType]) {
|
|
1122
|
+
return normalizeStruct(fieldType, value);
|
|
1123
|
+
}
|
|
1124
|
+
if (fieldType === "address" && typeof value === "string") {
|
|
1125
|
+
return value.toLowerCase();
|
|
1126
|
+
}
|
|
1127
|
+
if (/^u?int\d*$/.test(fieldType)) {
|
|
1128
|
+
return BigInt(value).toString();
|
|
1129
|
+
}
|
|
1130
|
+
if (fieldType === "bool") {
|
|
1131
|
+
return !!value;
|
|
1132
|
+
}
|
|
1133
|
+
return value;
|
|
1134
|
+
}
|
|
1135
|
+
function normalizeStruct(typeName, obj) {
|
|
1136
|
+
const fields = types[typeName];
|
|
1137
|
+
if (!fields) return obj;
|
|
1138
|
+
const result = {};
|
|
1139
|
+
for (const field of fields) {
|
|
1140
|
+
if (obj[field.name] !== void 0) {
|
|
1141
|
+
result[field.name] = normalizeValue(field.type, obj[field.name]);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return result;
|
|
1145
|
+
}
|
|
1146
|
+
const normalizedDomain = {};
|
|
1147
|
+
if (domain.name != null) normalizedDomain.name = domain.name;
|
|
1148
|
+
if (domain.version != null) normalizedDomain.version = domain.version;
|
|
1149
|
+
if (domain.chainId != null) normalizedDomain.chainId = Number(domain.chainId);
|
|
1150
|
+
if (domain.verifyingContract != null) {
|
|
1151
|
+
normalizedDomain.verifyingContract = domain.verifyingContract.toLowerCase();
|
|
1152
|
+
}
|
|
1153
|
+
if (domain.salt != null) normalizedDomain.salt = domain.salt;
|
|
1154
|
+
return {
|
|
1155
|
+
domain: normalizedDomain,
|
|
1156
|
+
types,
|
|
1157
|
+
primaryType,
|
|
1158
|
+
message: normalizeStruct(primaryType, message)
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
var sdkCache = /* @__PURE__ */ new Map();
|
|
1162
|
+
var DEFAULT_PARTNERS = {
|
|
1163
|
+
56: "thena",
|
|
1164
|
+
137: "quickswap",
|
|
1165
|
+
8453: "intentx",
|
|
1166
|
+
59144: "lynex"
|
|
1167
|
+
};
|
|
1168
|
+
function getPartner(chainId) {
|
|
1169
|
+
return getConfig().orbsPartner || DEFAULT_PARTNERS[chainId] || "widget";
|
|
1170
|
+
}
|
|
1171
|
+
function getSdk(chainId) {
|
|
1172
|
+
let sdk = sdkCache.get(chainId);
|
|
1173
|
+
if (!sdk) {
|
|
1174
|
+
sdk = constructSDK({ partner: getPartner(chainId), chainId });
|
|
1175
|
+
sdkCache.set(chainId, sdk);
|
|
1176
|
+
}
|
|
1177
|
+
return sdk;
|
|
1178
|
+
}
|
|
1179
|
+
var API_URLS = {
|
|
1180
|
+
137: "https://polygon.hub.orbs.network",
|
|
1181
|
+
56: "https://bsc.hub.orbs.network",
|
|
1182
|
+
250: "https://ftm.hub.orbs.network",
|
|
1183
|
+
8453: "https://base.hub.orbs.network",
|
|
1184
|
+
59144: "https://linea.hub.orbs.network",
|
|
1185
|
+
81457: "https://blast.hub.orbs.network",
|
|
1186
|
+
1101: "https://zkevm.hub.orbs.network",
|
|
1187
|
+
146: "https://sonic.hub.orbs.network",
|
|
1188
|
+
42161: "https://arbi.hub.orbs.network"
|
|
1189
|
+
};
|
|
1190
|
+
function getApiUrl(chainId) {
|
|
1191
|
+
return API_URLS[chainId] || "https://hub.orbs.network";
|
|
1192
|
+
}
|
|
1193
|
+
async function submitSwap(params) {
|
|
1194
|
+
const { chainId, quote, signature } = params;
|
|
1195
|
+
const apiUrl = getApiUrl(chainId);
|
|
1196
|
+
process.stderr.write(
|
|
1197
|
+
`[orbs] Submitting swap session=${quote.sessionId} ${quote.inAmount} ${quote.inToken} \u2192 ${quote.outToken}
|
|
1198
|
+
`
|
|
1199
|
+
);
|
|
1200
|
+
const response = await fetch(`${apiUrl}/swap-async?chainId=${chainId}`, {
|
|
1201
|
+
method: "POST",
|
|
1202
|
+
headers: { "Content-Type": "application/json" },
|
|
1203
|
+
body: JSON.stringify({
|
|
1204
|
+
...quote,
|
|
1205
|
+
inToken: quote.inToken,
|
|
1206
|
+
outToken: quote.outToken,
|
|
1207
|
+
inAmount: quote.inAmount,
|
|
1208
|
+
user: quote.user,
|
|
1209
|
+
signature,
|
|
1210
|
+
sessionId: quote.sessionId
|
|
1211
|
+
})
|
|
1212
|
+
});
|
|
1213
|
+
const result = await response.json();
|
|
1214
|
+
process.stderr.write(`[orbs] swap-async response: ${JSON.stringify(result)}
|
|
1215
|
+
`);
|
|
1216
|
+
if (result.error) {
|
|
1217
|
+
return { sessionId: quote.sessionId, status: "failed", error: result.error };
|
|
1218
|
+
}
|
|
1219
|
+
if (result.txHash) {
|
|
1220
|
+
return { sessionId: quote.sessionId, txHash: result.txHash, status: "completed" };
|
|
1221
|
+
}
|
|
1222
|
+
return { sessionId: quote.sessionId, status: "submitted" };
|
|
1223
|
+
}
|
|
1224
|
+
async function pollSwapStatus(params) {
|
|
1225
|
+
const { chainId, sessionId, user, maxAttempts = 15 } = params;
|
|
1226
|
+
const apiUrl = getApiUrl(chainId);
|
|
1227
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
1228
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
1229
|
+
try {
|
|
1230
|
+
const response = await fetch(`${apiUrl}/swap/status/${sessionId}?chainId=${chainId}`, {
|
|
1231
|
+
method: "POST",
|
|
1232
|
+
headers: { "Content-Type": "application/json" },
|
|
1233
|
+
body: JSON.stringify({ user })
|
|
1234
|
+
});
|
|
1235
|
+
const status = await response.json();
|
|
1236
|
+
if (i % 5 === 0 || status.txHash || status.error) {
|
|
1237
|
+
process.stderr.write(`[orbs] Poll ${i + 1}/${maxAttempts}: ${JSON.stringify(status)}
|
|
1238
|
+
`);
|
|
1239
|
+
}
|
|
1240
|
+
if (status.error) {
|
|
1241
|
+
return { sessionId, status: "failed", error: status.error };
|
|
1242
|
+
}
|
|
1243
|
+
if (status.txHash) {
|
|
1244
|
+
process.stderr.write(`[orbs] Swap filled! txHash: ${status.txHash}
|
|
1245
|
+
`);
|
|
1246
|
+
return { sessionId, txHash: status.txHash, status: "completed" };
|
|
1247
|
+
}
|
|
1248
|
+
} catch (e3) {
|
|
1249
|
+
process.stderr.write(`[orbs] Poll ${i + 1} error: ${e3}
|
|
1250
|
+
`);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return { sessionId, status: "submitted" };
|
|
1254
|
+
}
|
|
1255
|
+
var QUOTE_ERRORS = {
|
|
1256
|
+
tns: "Token not supported or swap amount too small (minimum ~$6-10 depending on chain)",
|
|
1257
|
+
ldv: "Low dollar value \u2014 swap amount is below the minimum for this token pair",
|
|
1258
|
+
"no liquidity": "No liquidity available for this token pair on the Liquidity Hub",
|
|
1259
|
+
timeout: "Quote request timed out"
|
|
1260
|
+
};
|
|
1261
|
+
function formatQuoteError(code) {
|
|
1262
|
+
return QUOTE_ERRORS[code] || `Liquidity Hub quote error: ${code}`;
|
|
1263
|
+
}
|
|
1264
|
+
async function getQuote2(chainId, request) {
|
|
1265
|
+
const sdk = getSdk(chainId);
|
|
1266
|
+
const quote = await sdk.getQuote({
|
|
1267
|
+
fromToken: request.fromToken,
|
|
1268
|
+
toToken: request.toToken,
|
|
1269
|
+
inAmount: request.inAmount,
|
|
1270
|
+
slippage: request.slippage ?? 0.5,
|
|
1271
|
+
account: request.account
|
|
1272
|
+
});
|
|
1273
|
+
if (quote.error) {
|
|
1274
|
+
throw new Error(formatQuoteError(quote.error));
|
|
1275
|
+
}
|
|
1276
|
+
return {
|
|
1277
|
+
inToken: quote.inToken,
|
|
1278
|
+
outToken: quote.outToken,
|
|
1279
|
+
inAmount: quote.inAmount,
|
|
1280
|
+
outAmount: quote.outAmount,
|
|
1281
|
+
minAmountOut: quote.minAmountOut,
|
|
1282
|
+
exchange: quote.exchange
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
var PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
|
|
1286
|
+
var NATIVE_TOKENS = /* @__PURE__ */ new Set([
|
|
1287
|
+
"0x0000000000000000000000000000000000000000",
|
|
1288
|
+
"0x0000000000000000000000000000000000001010",
|
|
1289
|
+
"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
|
|
1290
|
+
"0x000000000000000000000000000000000000dead"
|
|
1291
|
+
]);
|
|
1292
|
+
var WRAPPED_NATIVE = {
|
|
1293
|
+
137: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
|
|
1294
|
+
56: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
|
|
1295
|
+
8453: "0x4200000000000000000000000000000000000006",
|
|
1296
|
+
59144: "0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f",
|
|
1297
|
+
81457: "0x4300000000000000000000000000000000000004",
|
|
1298
|
+
42161: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"
|
|
1299
|
+
};
|
|
1300
|
+
var erc20Abi = [
|
|
1301
|
+
{
|
|
1302
|
+
name: "allowance",
|
|
1303
|
+
type: "function",
|
|
1304
|
+
stateMutability: "view",
|
|
1305
|
+
inputs: [
|
|
1306
|
+
{ name: "owner", type: "address" },
|
|
1307
|
+
{ name: "spender", type: "address" }
|
|
1308
|
+
],
|
|
1309
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
name: "approve",
|
|
1313
|
+
type: "function",
|
|
1314
|
+
stateMutability: "nonpayable",
|
|
1315
|
+
inputs: [
|
|
1316
|
+
{ name: "spender", type: "address" },
|
|
1317
|
+
{ name: "amount", type: "uint256" }
|
|
1318
|
+
],
|
|
1319
|
+
outputs: [{ name: "", type: "bool" }]
|
|
1320
|
+
},
|
|
1321
|
+
{
|
|
1322
|
+
name: "deposit",
|
|
1323
|
+
type: "function",
|
|
1324
|
+
stateMutability: "payable",
|
|
1325
|
+
inputs: [],
|
|
1326
|
+
outputs: []
|
|
1327
|
+
}
|
|
1328
|
+
];
|
|
1329
|
+
function isNativeToken(address) {
|
|
1330
|
+
return NATIVE_TOKENS.has(address.toLowerCase());
|
|
1331
|
+
}
|
|
1332
|
+
async function prepareSwap(params) {
|
|
1333
|
+
const { chainId, inAmount, account } = params;
|
|
1334
|
+
const chain = getChainById(chainId);
|
|
1335
|
+
if (!chain) throw new Error(`Unsupported chain: ${chainId}`);
|
|
1336
|
+
const publicClient = createPublicClient({ chain, transport: getTransportForChain(chainId) });
|
|
1337
|
+
let fromToken = params.fromToken;
|
|
1338
|
+
if (isNativeToken(params.fromToken)) {
|
|
1339
|
+
const wrapped = WRAPPED_NATIVE[chainId];
|
|
1340
|
+
if (!wrapped) throw new Error(`No wrapped native token for chain ${chainId}`);
|
|
1341
|
+
const walletClient = createWalletClientForChain(account, chainId);
|
|
1342
|
+
const hash = await walletClient.writeContract({
|
|
1343
|
+
address: wrapped,
|
|
1344
|
+
abi: erc20Abi,
|
|
1345
|
+
functionName: "deposit",
|
|
1346
|
+
value: BigInt(inAmount),
|
|
1347
|
+
chain,
|
|
1348
|
+
account
|
|
1349
|
+
});
|
|
1350
|
+
await publicClient.waitForTransactionReceipt({ hash });
|
|
1351
|
+
process.stderr.write(`[orbs] Wrapped ${inAmount} native \u2192 ${wrapped} (tx: ${hash})
|
|
1352
|
+
`);
|
|
1353
|
+
fromToken = wrapped;
|
|
1354
|
+
}
|
|
1355
|
+
const allowance = await publicClient.readContract({
|
|
1356
|
+
address: fromToken,
|
|
1357
|
+
abi: erc20Abi,
|
|
1358
|
+
functionName: "allowance",
|
|
1359
|
+
args: [account.address, PERMIT2]
|
|
1360
|
+
});
|
|
1361
|
+
if (allowance < BigInt(inAmount)) {
|
|
1362
|
+
const walletClient = createWalletClientForChain(account, chainId);
|
|
1363
|
+
const hash = await walletClient.writeContract({
|
|
1364
|
+
address: fromToken,
|
|
1365
|
+
abi: erc20Abi,
|
|
1366
|
+
functionName: "approve",
|
|
1367
|
+
args: [PERMIT2, maxUint256],
|
|
1368
|
+
chain,
|
|
1369
|
+
account
|
|
1370
|
+
});
|
|
1371
|
+
await publicClient.waitForTransactionReceipt({ hash });
|
|
1372
|
+
process.stderr.write(`[orbs] Approved ${fromToken} \u2192 Permit2 (tx: ${hash})
|
|
1373
|
+
`);
|
|
1374
|
+
}
|
|
1375
|
+
return { fromToken };
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// src/orbs/twap.ts
|
|
1379
|
+
import {
|
|
1380
|
+
Configs as Configs2,
|
|
1381
|
+
buildRePermitOrderData,
|
|
1382
|
+
getAccountOrders,
|
|
1383
|
+
getSrcTokenChunkAmount,
|
|
1384
|
+
getConfig as getTwapConfig,
|
|
1385
|
+
submitOrder
|
|
1386
|
+
} from "@orbs-network/twap-sdk";
|
|
1387
|
+
function getChainConfig(chainId) {
|
|
1388
|
+
const match = Object.values(Configs2).find((c4) => c4.chainId === chainId);
|
|
1389
|
+
if (!match) return void 0;
|
|
1390
|
+
return getTwapConfig(chainId, match.partner);
|
|
1391
|
+
}
|
|
1392
|
+
function prepareTwapOrder(params) {
|
|
1393
|
+
const config = getChainConfig(params.chainId);
|
|
1394
|
+
if (!config) {
|
|
1395
|
+
throw new Error(`No TWAP config available for chain ${params.chainId}`);
|
|
1396
|
+
}
|
|
1397
|
+
const srcAmountPerTrade = getSrcTokenChunkAmount(params.srcAmount, params.chunks);
|
|
1398
|
+
const fillDelayMillis = params.fillDelaySeconds * 1e3;
|
|
1399
|
+
const durationMillis = params.durationSeconds * 1e3;
|
|
1400
|
+
const deadlineMillis = Date.now() + durationMillis;
|
|
1401
|
+
const orderData = buildRePermitOrderData({
|
|
1402
|
+
chainId: params.chainId,
|
|
1403
|
+
srcToken: params.srcToken,
|
|
1404
|
+
dstToken: params.dstToken,
|
|
1405
|
+
srcAmount: params.srcAmount,
|
|
1406
|
+
deadlineMillis,
|
|
1407
|
+
fillDelayMillis,
|
|
1408
|
+
slippage: params.slippage ?? 0.5,
|
|
1409
|
+
account: params.account,
|
|
1410
|
+
srcAmountPerTrade,
|
|
1411
|
+
dstMinAmountPerTrade: params.dstMinAmountPerTrade,
|
|
1412
|
+
triggerAmountPerTrade: params.triggerAmountPerTrade,
|
|
1413
|
+
config
|
|
1414
|
+
});
|
|
1415
|
+
return {
|
|
1416
|
+
domain: orderData.domain,
|
|
1417
|
+
order: orderData.order,
|
|
1418
|
+
types: orderData.types,
|
|
1419
|
+
primaryType: orderData.primaryType
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
async function submitSignedOrder(order, signature) {
|
|
1423
|
+
return submitOrder(order, signature);
|
|
1424
|
+
}
|
|
1425
|
+
async function listOrders(chainId, account, options) {
|
|
1426
|
+
return getAccountOrders({
|
|
1427
|
+
chainId,
|
|
1428
|
+
account,
|
|
1429
|
+
limit: options?.limit ?? 50,
|
|
1430
|
+
page: options?.page ?? 0
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// src/utils/signature.ts
|
|
1435
|
+
function splitSignature(signature) {
|
|
1436
|
+
if (!signature.startsWith("0x")) {
|
|
1437
|
+
throw new Error("Invalid signature: must start with 0x prefix");
|
|
1438
|
+
}
|
|
1439
|
+
if (signature.length !== 132) {
|
|
1440
|
+
throw new Error(
|
|
1441
|
+
`Invalid signature: expected 132 characters (0x + 130 hex), got ${signature.length}`
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
if (!/^0x[0-9a-fA-F]{130}$/.test(signature)) {
|
|
1445
|
+
throw new Error("Invalid signature: contains non-hex characters");
|
|
1446
|
+
}
|
|
1447
|
+
return {
|
|
1448
|
+
r: `0x${signature.slice(2, 66)}`,
|
|
1449
|
+
s: `0x${signature.slice(66, 130)}`,
|
|
1450
|
+
v: `0x${signature.slice(130, 132)}`
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// src/utils/tool-helpers.ts
|
|
1455
|
+
function resolveChainId(params) {
|
|
1456
|
+
return typeof params.chainId === "number" ? params.chainId : getConfig().chainId;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// src/tools/orbs/schemas.ts
|
|
1460
|
+
import { z as z2 } from "zod";
|
|
1461
|
+
var orbsGetQuoteSchema = z2.object({
|
|
1462
|
+
chainId: z2.number({ required_error: "chainId is required" }),
|
|
1463
|
+
fromToken: z2.string({ required_error: "fromToken is required" }),
|
|
1464
|
+
toToken: z2.string({ required_error: "toToken is required" }),
|
|
1465
|
+
inAmount: z2.string({ required_error: "inAmount is required" }),
|
|
1466
|
+
slippage: z2.number().optional()
|
|
1467
|
+
});
|
|
1468
|
+
var orbsSwapSchema = orbsGetQuoteSchema;
|
|
1469
|
+
var orbsPlaceTwapSchema = z2.object({
|
|
1470
|
+
chainId: z2.number({ required_error: "chainId is required" }),
|
|
1471
|
+
srcToken: z2.string({ required_error: "srcToken is required" }),
|
|
1472
|
+
dstToken: z2.string({ required_error: "dstToken is required" }),
|
|
1473
|
+
srcAmount: z2.string({ required_error: "srcAmount is required" }),
|
|
1474
|
+
chunks: z2.number({ required_error: "chunks is required" }),
|
|
1475
|
+
fillDelay: z2.number({ required_error: "fillDelay is required" })
|
|
1476
|
+
});
|
|
1477
|
+
var orbsPlaceLimitSchema = z2.object({
|
|
1478
|
+
chainId: z2.number({ required_error: "chainId is required" }),
|
|
1479
|
+
srcToken: z2.string({ required_error: "srcToken is required" }),
|
|
1480
|
+
dstToken: z2.string({ required_error: "dstToken is required" }),
|
|
1481
|
+
srcAmount: z2.string({ required_error: "srcAmount is required" }),
|
|
1482
|
+
dstMinAmount: z2.string({ required_error: "dstMinAmount is required" }),
|
|
1483
|
+
expiry: z2.number().optional()
|
|
1484
|
+
});
|
|
1485
|
+
var orbsSwapStatusSchema = z2.object({
|
|
1486
|
+
chainId: z2.number({ required_error: "chainId is required" }),
|
|
1487
|
+
sessionId: z2.string({ required_error: "sessionId is required" }),
|
|
1488
|
+
user: z2.string({ required_error: "user is required" }),
|
|
1489
|
+
maxAttempts: z2.number().optional()
|
|
1490
|
+
});
|
|
1491
|
+
var orbsListOrdersSchema = z2.object({
|
|
1492
|
+
chainId: z2.number({ required_error: "chainId is required" })
|
|
1493
|
+
});
|
|
1494
|
+
|
|
1495
|
+
// src/tools/orbs/index.ts
|
|
1496
|
+
async function orbsGetQuote(params) {
|
|
1497
|
+
const v = validateInput(orbsGetQuoteSchema, params);
|
|
1498
|
+
if (!v.success) return v.error;
|
|
1499
|
+
const { chainId, fromToken, toToken, inAmount, slippage } = v.data;
|
|
1500
|
+
if (!isLiquidityHubSupported(chainId)) {
|
|
1501
|
+
return formatToolError("CHAIN_NOT_SUPPORTED", getLiquidityHubError(chainId));
|
|
1502
|
+
}
|
|
1503
|
+
try {
|
|
1504
|
+
const result = await getQuote2(chainId, {
|
|
1505
|
+
fromToken,
|
|
1506
|
+
toToken,
|
|
1507
|
+
inAmount,
|
|
1508
|
+
slippage: slippage ?? 0.5
|
|
1509
|
+
});
|
|
1510
|
+
return formatToolResponse(result);
|
|
1511
|
+
} catch (e3) {
|
|
1512
|
+
return formatToolError("ORBS_QUOTE_ERROR", String(e3));
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
async function executeOrbsSwapNow(params) {
|
|
1516
|
+
const chainId = resolveChainId(params);
|
|
1517
|
+
try {
|
|
1518
|
+
const account = getActiveAccount();
|
|
1519
|
+
const { fromToken } = await prepareSwap({
|
|
1520
|
+
chainId,
|
|
1521
|
+
fromToken: params.fromToken,
|
|
1522
|
+
inAmount: params.inAmount,
|
|
1523
|
+
account
|
|
1524
|
+
});
|
|
1525
|
+
const sdk = getSdk(chainId);
|
|
1526
|
+
const slippage = params.slippage ?? 0.5;
|
|
1527
|
+
const toToken = params.toToken;
|
|
1528
|
+
const inAmount = params.inAmount;
|
|
1529
|
+
const quote = await sdk.getQuote({
|
|
1530
|
+
fromToken,
|
|
1531
|
+
toToken,
|
|
1532
|
+
inAmount,
|
|
1533
|
+
slippage,
|
|
1534
|
+
dexMinAmountOut: "-1",
|
|
1535
|
+
account: account.address
|
|
1536
|
+
});
|
|
1537
|
+
if (quote.error) {
|
|
1538
|
+
return formatToolError("ORBS_QUOTE_ERROR", quote.error);
|
|
1539
|
+
}
|
|
1540
|
+
if (quote.inToken && quote.inToken.toLowerCase() !== fromToken.toLowerCase()) {
|
|
1541
|
+
process.stderr.write(
|
|
1542
|
+
`[orbs] Warning: quote.inToken (${quote.inToken}) does not match fromToken (${fromToken})
|
|
1543
|
+
`
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
if (!account.signTypedData) {
|
|
1547
|
+
return formatToolError("WALLET_ERROR", "Active account does not support EIP-712 signing");
|
|
1548
|
+
}
|
|
1549
|
+
const rawPrimaryType = quote.eip712?.primaryType ?? "PermitWitnessTransferFrom";
|
|
1550
|
+
const rawMessage = quote.eip712?.message ?? quote.permitData;
|
|
1551
|
+
const {
|
|
1552
|
+
domain: eip712Domain,
|
|
1553
|
+
types: eip712Types,
|
|
1554
|
+
primaryType: eip712PrimaryType,
|
|
1555
|
+
message: eip712Message
|
|
1556
|
+
} = normalizeEip712ForSigning(
|
|
1557
|
+
quote.eip712?.domain,
|
|
1558
|
+
quote.eip712?.types,
|
|
1559
|
+
rawPrimaryType,
|
|
1560
|
+
rawMessage
|
|
1561
|
+
);
|
|
1562
|
+
process.stderr.write(
|
|
1563
|
+
`[orbs] EIP-712 primaryType: ${eip712PrimaryType}, types keys: ${Object.keys(eip712Types).join(", ")}
|
|
1564
|
+
`
|
|
1565
|
+
);
|
|
1566
|
+
const signature = await account.signTypedData({
|
|
1567
|
+
domain: eip712Domain,
|
|
1568
|
+
types: eip712Types,
|
|
1569
|
+
primaryType: eip712PrimaryType,
|
|
1570
|
+
message: eip712Message
|
|
1571
|
+
});
|
|
1572
|
+
process.stderr.write("[orbs] Attempting swap via SDK swap() method...\n");
|
|
1573
|
+
let txHash;
|
|
1574
|
+
try {
|
|
1575
|
+
txHash = await sdk.swap(quote, signature);
|
|
1576
|
+
process.stderr.write(`[orbs] SDK swap() returned txHash: ${txHash}
|
|
1577
|
+
`);
|
|
1578
|
+
} catch (sdkSwapErr) {
|
|
1579
|
+
process.stderr.write(`[orbs] SDK swap() error: ${sdkSwapErr}
|
|
1580
|
+
`);
|
|
1581
|
+
}
|
|
1582
|
+
if (txHash) {
|
|
1583
|
+
return formatToolResponse({
|
|
1584
|
+
txHash,
|
|
1585
|
+
status: "completed",
|
|
1586
|
+
quote: { outAmount: quote.outAmount, minAmountOut: quote.minAmountOut }
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
process.stderr.write(
|
|
1590
|
+
"[orbs] SDK swap() did not return txHash, falling back to direct API...\n"
|
|
1591
|
+
);
|
|
1592
|
+
const submission = await submitSwap({ chainId, quote, signature });
|
|
1593
|
+
if (submission.status === "failed") {
|
|
1594
|
+
return formatToolError("ORBS_SWAP_ERROR", submission.error ?? "Swap submission failed");
|
|
1595
|
+
}
|
|
1596
|
+
if (submission.status === "completed") {
|
|
1597
|
+
return formatToolResponse({
|
|
1598
|
+
txHash: submission.txHash,
|
|
1599
|
+
status: "completed",
|
|
1600
|
+
quote: { outAmount: quote.outAmount, minAmountOut: quote.minAmountOut }
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
const result = await pollSwapStatus({
|
|
1604
|
+
chainId,
|
|
1605
|
+
sessionId: submission.sessionId,
|
|
1606
|
+
user: quote.user,
|
|
1607
|
+
maxAttempts: 15
|
|
1608
|
+
});
|
|
1609
|
+
if (result.status === "completed") {
|
|
1610
|
+
return formatToolResponse({
|
|
1611
|
+
txHash: result.txHash,
|
|
1612
|
+
status: "completed",
|
|
1613
|
+
quote: { outAmount: quote.outAmount, minAmountOut: quote.minAmountOut }
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
return formatToolResponse({
|
|
1617
|
+
status: "pending",
|
|
1618
|
+
sessionId: submission.sessionId,
|
|
1619
|
+
chainId,
|
|
1620
|
+
user: quote.user,
|
|
1621
|
+
message: "Swap submitted but not yet filled. Use orbs_swap_status to check.",
|
|
1622
|
+
quote: { outAmount: quote.outAmount, minAmountOut: quote.minAmountOut }
|
|
1623
|
+
});
|
|
1624
|
+
} catch (e3) {
|
|
1625
|
+
return formatToolError("ORBS_SWAP_ERROR", String(e3));
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
async function orbsSwapStatus(params) {
|
|
1629
|
+
const v = validateInput(orbsSwapStatusSchema, params);
|
|
1630
|
+
if (!v.success) return v.error;
|
|
1631
|
+
const { chainId, sessionId, user, maxAttempts } = v.data;
|
|
1632
|
+
try {
|
|
1633
|
+
const result = await pollSwapStatus({
|
|
1634
|
+
chainId,
|
|
1635
|
+
sessionId,
|
|
1636
|
+
user,
|
|
1637
|
+
maxAttempts: maxAttempts ?? 15
|
|
1638
|
+
});
|
|
1639
|
+
return formatToolResponse(result);
|
|
1640
|
+
} catch (e3) {
|
|
1641
|
+
return formatToolError("ORBS_STATUS_ERROR", String(e3));
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
async function orbsSwap(params) {
|
|
1645
|
+
const v = validateInput(orbsSwapSchema, params);
|
|
1646
|
+
if (!v.success) return v.error;
|
|
1647
|
+
const { chainId, fromToken, toToken, inAmount } = v.data;
|
|
1648
|
+
if (!isLiquidityHubSupported(chainId)) {
|
|
1649
|
+
return formatToolError("CHAIN_NOT_SUPPORTED", getLiquidityHubError(chainId));
|
|
1650
|
+
}
|
|
1651
|
+
return executeWrite({
|
|
1652
|
+
toolName: "orbs_swap",
|
|
1653
|
+
description: `Orbs Liquidity Hub swap: ${inAmount} of ${fromToken} \u2192 ${toToken} on chain ${chainId}`,
|
|
1654
|
+
params: { ...v.data },
|
|
1655
|
+
executor: executeOrbsSwapNow
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
async function executeOrbsTwapNow(params) {
|
|
1659
|
+
const chainId = resolveChainId(params);
|
|
1660
|
+
const chunks = Number(params.chunks ?? 5);
|
|
1661
|
+
const fillDelay = Number(params.fillDelay ?? 300);
|
|
1662
|
+
try {
|
|
1663
|
+
const account = getActiveAccount();
|
|
1664
|
+
const durationSeconds = chunks * fillDelay * 2;
|
|
1665
|
+
const prepared = prepareTwapOrder({
|
|
1666
|
+
chainId,
|
|
1667
|
+
srcToken: params.srcToken,
|
|
1668
|
+
dstToken: params.dstToken,
|
|
1669
|
+
srcAmount: params.srcAmount,
|
|
1670
|
+
chunks,
|
|
1671
|
+
fillDelaySeconds: fillDelay,
|
|
1672
|
+
durationSeconds,
|
|
1673
|
+
account: account.address
|
|
1674
|
+
});
|
|
1675
|
+
if (!account.signTypedData) {
|
|
1676
|
+
return formatToolError("WALLET_ERROR", "Active account does not support EIP-712 signing");
|
|
1677
|
+
}
|
|
1678
|
+
const signature = await account.signTypedData({
|
|
1679
|
+
domain: prepared.domain,
|
|
1680
|
+
types: prepared.types,
|
|
1681
|
+
primaryType: prepared.primaryType,
|
|
1682
|
+
message: prepared.order
|
|
1683
|
+
});
|
|
1684
|
+
const { v, r, s: s3 } = splitSignature(signature);
|
|
1685
|
+
const order = await submitSignedOrder(prepared.order, { v, r, s: s3 });
|
|
1686
|
+
return formatToolResponse({
|
|
1687
|
+
orderId: order.id,
|
|
1688
|
+
status: order.status,
|
|
1689
|
+
txHash: order.txHash
|
|
1690
|
+
});
|
|
1691
|
+
} catch (e3) {
|
|
1692
|
+
return formatToolError("ORBS_TWAP_ERROR", String(e3));
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
async function orbsPlaceTwap(params) {
|
|
1696
|
+
const v = validateInput(orbsPlaceTwapSchema, params);
|
|
1697
|
+
if (!v.success) return v.error;
|
|
1698
|
+
const { chainId, srcToken, dstToken, srcAmount, chunks, fillDelay } = v.data;
|
|
1699
|
+
if (!isTwapSupported(chainId)) {
|
|
1700
|
+
return formatToolError("CHAIN_NOT_SUPPORTED", getTwapError(chainId));
|
|
1701
|
+
}
|
|
1702
|
+
return executeWrite({
|
|
1703
|
+
toolName: "orbs_place_twap",
|
|
1704
|
+
description: `dTWAP order: ${srcAmount} of ${srcToken} \u2192 ${dstToken}, ${chunks} chunks, ${fillDelay}s delay on chain ${chainId}`,
|
|
1705
|
+
params: { ...v.data },
|
|
1706
|
+
executor: executeOrbsTwapNow
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
async function executeOrbsLimitNow(params) {
|
|
1710
|
+
const chainId = resolveChainId(params);
|
|
1711
|
+
try {
|
|
1712
|
+
const account = getActiveAccount();
|
|
1713
|
+
const expirySeconds = Number(params.expiry ?? 86400);
|
|
1714
|
+
const prepared = prepareTwapOrder({
|
|
1715
|
+
chainId,
|
|
1716
|
+
srcToken: params.srcToken,
|
|
1717
|
+
dstToken: params.dstToken,
|
|
1718
|
+
srcAmount: params.srcAmount,
|
|
1719
|
+
chunks: 1,
|
|
1720
|
+
fillDelaySeconds: 0,
|
|
1721
|
+
durationSeconds: expirySeconds,
|
|
1722
|
+
account: account.address,
|
|
1723
|
+
dstMinAmountPerTrade: params.dstMinAmount
|
|
1724
|
+
});
|
|
1725
|
+
if (!account.signTypedData) {
|
|
1726
|
+
return formatToolError("WALLET_ERROR", "Active account does not support EIP-712 signing");
|
|
1727
|
+
}
|
|
1728
|
+
const signature = await account.signTypedData({
|
|
1729
|
+
domain: prepared.domain,
|
|
1730
|
+
types: prepared.types,
|
|
1731
|
+
primaryType: prepared.primaryType,
|
|
1732
|
+
message: prepared.order
|
|
1733
|
+
});
|
|
1734
|
+
const { v, r, s: s3 } = splitSignature(signature);
|
|
1735
|
+
const order = await submitSignedOrder(prepared.order, { v, r, s: s3 });
|
|
1736
|
+
return formatToolResponse({
|
|
1737
|
+
orderId: order.id,
|
|
1738
|
+
status: order.status,
|
|
1739
|
+
txHash: order.txHash
|
|
1740
|
+
});
|
|
1741
|
+
} catch (e3) {
|
|
1742
|
+
return formatToolError("ORBS_LIMIT_ERROR", String(e3));
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
async function orbsPlaceLimit(params) {
|
|
1746
|
+
const v = validateInput(orbsPlaceLimitSchema, params);
|
|
1747
|
+
if (!v.success) return v.error;
|
|
1748
|
+
const { chainId, srcToken, dstToken, srcAmount, dstMinAmount } = v.data;
|
|
1749
|
+
if (!isTwapSupported(chainId)) {
|
|
1750
|
+
return formatToolError("CHAIN_NOT_SUPPORTED", getTwapError(chainId));
|
|
1751
|
+
}
|
|
1752
|
+
return executeWrite({
|
|
1753
|
+
toolName: "orbs_place_limit",
|
|
1754
|
+
description: `dLIMIT order: ${srcAmount} of ${srcToken} \u2192 ${dstToken}, min output ${dstMinAmount} on chain ${chainId}`,
|
|
1755
|
+
params: { ...v.data },
|
|
1756
|
+
executor: executeOrbsLimitNow
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
async function orbsListOrders(params) {
|
|
1760
|
+
const v = validateInput(orbsListOrdersSchema, params);
|
|
1761
|
+
if (!v.success) return v.error;
|
|
1762
|
+
const { chainId } = v.data;
|
|
1763
|
+
if (!isTwapSupported(chainId)) {
|
|
1764
|
+
return formatToolError("CHAIN_NOT_SUPPORTED", getTwapError(chainId));
|
|
1765
|
+
}
|
|
1766
|
+
const walletState = getWalletState();
|
|
1767
|
+
if (!walletState.address) {
|
|
1768
|
+
return formatToolError(
|
|
1769
|
+
"WALLET_READ_ONLY",
|
|
1770
|
+
"orbs_list_orders requires an active wallet with an address."
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
try {
|
|
1774
|
+
const orders = await listOrders(chainId, walletState.address);
|
|
1775
|
+
return formatToolResponse({
|
|
1776
|
+
count: orders.length,
|
|
1777
|
+
orders: orders.map((o2) => ({
|
|
1778
|
+
id: o2.id,
|
|
1779
|
+
type: o2.type,
|
|
1780
|
+
status: o2.status,
|
|
1781
|
+
srcToken: o2.srcTokenAddress,
|
|
1782
|
+
dstToken: o2.dstTokenAddress,
|
|
1783
|
+
srcAmount: o2.srcAmount,
|
|
1784
|
+
progress: o2.progress,
|
|
1785
|
+
createdAt: o2.createdAt
|
|
1786
|
+
}))
|
|
1787
|
+
});
|
|
1788
|
+
} catch (e3) {
|
|
1789
|
+
return formatToolError("ORBS_LIST_ERROR", String(e3));
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
function getOrbsToolDefinitions() {
|
|
1793
|
+
const tools = [
|
|
1794
|
+
{
|
|
1795
|
+
name: "orbs_get_quote",
|
|
1796
|
+
description: "Get a quote from Orbs Liquidity Hub for same-chain aggregated swap. Requires token addresses \u2014 use resolve_token first to get addresses.",
|
|
1797
|
+
inputSchema: {
|
|
1798
|
+
type: "object",
|
|
1799
|
+
properties: {
|
|
1800
|
+
chainId: {
|
|
1801
|
+
type: "number",
|
|
1802
|
+
description: "Chain ID (supported: 137, 56, 8453, 59144, 81457, 42161)"
|
|
1803
|
+
},
|
|
1804
|
+
fromToken: { type: "string", description: "Source token address" },
|
|
1805
|
+
toToken: {
|
|
1806
|
+
type: "string",
|
|
1807
|
+
description: "Destination token address"
|
|
1808
|
+
},
|
|
1809
|
+
inAmount: {
|
|
1810
|
+
type: "string",
|
|
1811
|
+
description: "Input amount in wei"
|
|
1812
|
+
},
|
|
1813
|
+
slippage: {
|
|
1814
|
+
type: "number",
|
|
1815
|
+
description: "Slippage tolerance (0.5 = 0.5%)"
|
|
1816
|
+
}
|
|
1817
|
+
},
|
|
1818
|
+
required: ["chainId", "fromToken", "toToken", "inAmount"]
|
|
1819
|
+
},
|
|
1820
|
+
handler: orbsGetQuote
|
|
1821
|
+
},
|
|
1822
|
+
{
|
|
1823
|
+
name: "orbs_swap",
|
|
1824
|
+
description: "Execute a same-chain swap via Orbs Liquidity Hub (write, confirmation-gated). Supported chains: 137 (Polygon), 56 (BSC), 8453 (Base), 59144 (Linea), 81457 (Blast), 42161 (Arbitrum). Requires token addresses \u2014 use resolve_token first.",
|
|
1825
|
+
inputSchema: {
|
|
1826
|
+
type: "object",
|
|
1827
|
+
properties: {
|
|
1828
|
+
chainId: { type: "number" },
|
|
1829
|
+
fromToken: { type: "string" },
|
|
1830
|
+
toToken: { type: "string" },
|
|
1831
|
+
inAmount: { type: "string" },
|
|
1832
|
+
slippage: { type: "number" }
|
|
1833
|
+
},
|
|
1834
|
+
required: ["chainId", "fromToken", "toToken", "inAmount"]
|
|
1835
|
+
},
|
|
1836
|
+
handler: orbsSwap
|
|
1837
|
+
},
|
|
1838
|
+
{
|
|
1839
|
+
name: "orbs_place_twap",
|
|
1840
|
+
description: "Place a dTWAP (time-weighted average price) order (write, confirmation-gated)",
|
|
1841
|
+
inputSchema: {
|
|
1842
|
+
type: "object",
|
|
1843
|
+
properties: {
|
|
1844
|
+
chainId: { type: "number" },
|
|
1845
|
+
srcToken: { type: "string" },
|
|
1846
|
+
dstToken: { type: "string" },
|
|
1847
|
+
srcAmount: {
|
|
1848
|
+
type: "string",
|
|
1849
|
+
description: "Total amount to swap in wei"
|
|
1850
|
+
},
|
|
1851
|
+
chunks: {
|
|
1852
|
+
type: "number",
|
|
1853
|
+
description: "Number of equal chunks"
|
|
1854
|
+
},
|
|
1855
|
+
fillDelay: {
|
|
1856
|
+
type: "number",
|
|
1857
|
+
description: "Delay between fills in seconds"
|
|
1858
|
+
}
|
|
1859
|
+
},
|
|
1860
|
+
required: ["chainId", "srcToken", "dstToken", "srcAmount", "chunks", "fillDelay"]
|
|
1861
|
+
},
|
|
1862
|
+
handler: orbsPlaceTwap
|
|
1863
|
+
},
|
|
1864
|
+
{
|
|
1865
|
+
name: "orbs_place_limit",
|
|
1866
|
+
description: "Place a dLIMIT order (write, confirmation-gated)",
|
|
1867
|
+
inputSchema: {
|
|
1868
|
+
type: "object",
|
|
1869
|
+
properties: {
|
|
1870
|
+
chainId: { type: "number" },
|
|
1871
|
+
srcToken: { type: "string" },
|
|
1872
|
+
dstToken: { type: "string" },
|
|
1873
|
+
srcAmount: { type: "string" },
|
|
1874
|
+
dstMinAmount: {
|
|
1875
|
+
type: "string",
|
|
1876
|
+
description: "Minimum output amount (the limit price)"
|
|
1877
|
+
},
|
|
1878
|
+
expiry: {
|
|
1879
|
+
type: "number",
|
|
1880
|
+
description: "Order expiry in seconds from now"
|
|
1881
|
+
}
|
|
1882
|
+
},
|
|
1883
|
+
required: ["chainId", "srcToken", "dstToken", "srcAmount", "dstMinAmount"]
|
|
1884
|
+
},
|
|
1885
|
+
handler: orbsPlaceLimit
|
|
1886
|
+
},
|
|
1887
|
+
{
|
|
1888
|
+
name: "orbs_swap_status",
|
|
1889
|
+
description: "Check the status of a pending Orbs Liquidity Hub swap",
|
|
1890
|
+
inputSchema: {
|
|
1891
|
+
type: "object",
|
|
1892
|
+
properties: {
|
|
1893
|
+
chainId: { type: "number", description: "Chain ID" },
|
|
1894
|
+
sessionId: {
|
|
1895
|
+
type: "string",
|
|
1896
|
+
description: "Session ID from orbs_swap response"
|
|
1897
|
+
},
|
|
1898
|
+
user: {
|
|
1899
|
+
type: "string",
|
|
1900
|
+
description: "User wallet address"
|
|
1901
|
+
},
|
|
1902
|
+
maxAttempts: {
|
|
1903
|
+
type: "number",
|
|
1904
|
+
description: "Max poll attempts (default 15, 2s each = 30s)"
|
|
1905
|
+
}
|
|
1906
|
+
},
|
|
1907
|
+
required: ["chainId", "sessionId", "user"]
|
|
1908
|
+
},
|
|
1909
|
+
handler: orbsSwapStatus
|
|
1910
|
+
},
|
|
1911
|
+
{
|
|
1912
|
+
name: "orbs_list_orders",
|
|
1913
|
+
description: "List open TWAP/dLIMIT orders for active wallet",
|
|
1914
|
+
inputSchema: {
|
|
1915
|
+
type: "object",
|
|
1916
|
+
properties: {
|
|
1917
|
+
chainId: { type: "number" }
|
|
1918
|
+
},
|
|
1919
|
+
required: ["chainId"]
|
|
1920
|
+
},
|
|
1921
|
+
handler: orbsListOrders
|
|
1922
|
+
},
|
|
1923
|
+
...getDsltpToolDefinitions()
|
|
1924
|
+
];
|
|
1925
|
+
return tools;
|
|
1926
|
+
}
|
|
1927
|
+
function registerOrbsExecutors() {
|
|
1928
|
+
registerExecutor("orbs_swap", executeOrbsSwapNow);
|
|
1929
|
+
registerExecutor("orbs_place_twap", executeOrbsTwapNow);
|
|
1930
|
+
registerExecutor("orbs_place_limit", executeOrbsLimitNow);
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// src/goat/dispatch.ts
|
|
1934
|
+
var RESTRICTED_PLUGIN_CHAINS = {
|
|
1935
|
+
uniswap: [1, 137, 43114, 8453, 10, 42161, 42220],
|
|
1936
|
+
balancer: [34443, 8453, 137, 100, 42161, 43114, 10]
|
|
1937
|
+
};
|
|
1938
|
+
function findRestrictedPlugin(toolName) {
|
|
1939
|
+
const lowerName = toolName.toLowerCase();
|
|
1940
|
+
for (const pluginKey of Object.keys(RESTRICTED_PLUGIN_CHAINS)) {
|
|
1941
|
+
if (lowerName.startsWith(pluginKey.toLowerCase())) {
|
|
1942
|
+
return pluginKey;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
return void 0;
|
|
1946
|
+
}
|
|
1947
|
+
async function dispatchGoatTool(toolName, params) {
|
|
1948
|
+
const config = getConfig();
|
|
1949
|
+
const chainId = typeof params.chainId === "number" ? params.chainId : config.chainId;
|
|
1950
|
+
if (!isSupported(chainId)) {
|
|
1951
|
+
return formatToolError("UNSUPPORTED_CHAIN", `Chain ${chainId} is not a known EVM chain`);
|
|
1952
|
+
}
|
|
1953
|
+
const pluginKey = findRestrictedPlugin(toolName);
|
|
1954
|
+
if (pluginKey) {
|
|
1955
|
+
const availableChains = RESTRICTED_PLUGIN_CHAINS[pluginKey];
|
|
1956
|
+
if (!availableChains.includes(chainId)) {
|
|
1957
|
+
return formatToolError(
|
|
1958
|
+
"TOOL_UNAVAILABLE_ON_CHAIN",
|
|
1959
|
+
`${toolName} is not available on chain ${chainId}. Available on chains: ${availableChains.join(", ")}`,
|
|
1960
|
+
{ availableChainIds: availableChains }
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
const snapshot = await goatProvider.getOrBuildSnapshot(chainId);
|
|
1965
|
+
if (!snapshot) {
|
|
1966
|
+
return formatToolError(
|
|
1967
|
+
"CHAIN_INIT_FAILED",
|
|
1968
|
+
`Failed to initialize GOAT tools for chain ${chainId}`
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1971
|
+
const { chainId: _ignored, ...goatParams } = params;
|
|
1972
|
+
try {
|
|
1973
|
+
const result = await snapshot.toolHandler(toolName, goatParams);
|
|
1974
|
+
return result;
|
|
1975
|
+
} catch (e3) {
|
|
1976
|
+
return formatToolError("GOAT_TOOL_ERROR", String(e3));
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
// src/tools/utility/index.ts
|
|
1981
|
+
var _health = null;
|
|
1982
|
+
var _totalToolCount = 0;
|
|
1983
|
+
function setHealthStatus(health, totalToolCount) {
|
|
1984
|
+
_health = health;
|
|
1985
|
+
_totalToolCount = totalToolCount;
|
|
1986
|
+
}
|
|
1987
|
+
async function serverStatus() {
|
|
1988
|
+
try {
|
|
1989
|
+
const wallet = getWalletState();
|
|
1990
|
+
const backends = _health ? {
|
|
1991
|
+
blockscout: _health.blockscout.status,
|
|
1992
|
+
etherscan: _health.etherscan.status,
|
|
1993
|
+
evm: _health.evm.status,
|
|
1994
|
+
goat: _health.goat.status,
|
|
1995
|
+
lifi: _health.lifi.status,
|
|
1996
|
+
orbs: _health.orbs.status
|
|
1997
|
+
} : {
|
|
1998
|
+
blockscout: "not_initialized",
|
|
1999
|
+
etherscan: "not_initialized",
|
|
2000
|
+
evm: "not_initialized",
|
|
2001
|
+
goat: "not_initialized",
|
|
2002
|
+
lifi: "not_initialized",
|
|
2003
|
+
orbs: "not_initialized"
|
|
2004
|
+
};
|
|
2005
|
+
return formatToolResponse({
|
|
2006
|
+
walletMode: wallet.mode,
|
|
2007
|
+
activeChainId: wallet.chainId,
|
|
2008
|
+
confirmWrites: confirmationQueue.enabled,
|
|
2009
|
+
backends,
|
|
2010
|
+
toolCount: _totalToolCount
|
|
2011
|
+
});
|
|
2012
|
+
} catch (err) {
|
|
2013
|
+
return formatToolError("STATUS_FAILED", err instanceof Error ? err.message : "Unknown error");
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
var INTEGRATION_CHAINS = /* @__PURE__ */ new Set([
|
|
2017
|
+
...LIQUIDITY_HUB_CHAINS,
|
|
2018
|
+
...Object.values(RESTRICTED_PLUGIN_CHAINS).flat()
|
|
2019
|
+
]);
|
|
2020
|
+
async function listSupportedChains() {
|
|
2021
|
+
try {
|
|
2022
|
+
const chains = [...INTEGRATION_CHAINS].map((id) => {
|
|
2023
|
+
const chain = getChainById(id);
|
|
2024
|
+
if (!chain) return null;
|
|
2025
|
+
return { id: chain.id, name: chain.name, nativeCurrency: chain.nativeCurrency };
|
|
2026
|
+
}).filter((c4) => c4 !== null);
|
|
2027
|
+
return formatToolResponse({
|
|
2028
|
+
note: "Any EVM chain supported by viem works for basic operations (ERC-20, ERC-721, ENS). Chains listed below have enhanced integration (DEX swaps, bridging, Orbs).",
|
|
2029
|
+
chains
|
|
2030
|
+
});
|
|
2031
|
+
} catch (e3) {
|
|
2032
|
+
return formatToolError("CHAINS_UNAVAILABLE", String(e3));
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// src/tools/wallet/index.ts
|
|
2037
|
+
import {
|
|
2038
|
+
english,
|
|
2039
|
+
generateMnemonic,
|
|
2040
|
+
generatePrivateKey as generatePrivateKey3,
|
|
2041
|
+
mnemonicToAccount as mnemonicToAccount2,
|
|
2042
|
+
privateKeyToAccount as privateKeyToAccount3
|
|
2043
|
+
} from "viem/accounts";
|
|
2044
|
+
|
|
2045
|
+
// src/tools/wallet/schemas.ts
|
|
2046
|
+
import { z as z3 } from "zod";
|
|
2047
|
+
var walletFromMnemonicSchema = z3.object({
|
|
2048
|
+
mnemonic: z3.string({ required_error: "mnemonic is required" }).min(1, "mnemonic must not be empty"),
|
|
2049
|
+
accountIndex: z3.number().optional(),
|
|
2050
|
+
addressIndex: z3.number().optional()
|
|
2051
|
+
});
|
|
2052
|
+
var walletDeriveAddressesSchema = z3.object({
|
|
2053
|
+
mnemonic: z3.string({ required_error: "mnemonic is required" }).min(1, "mnemonic must not be empty"),
|
|
2054
|
+
count: z3.number().min(1).max(20).optional()
|
|
2055
|
+
});
|
|
2056
|
+
var walletActivateSchema = z3.object({
|
|
2057
|
+
privateKey: z3.string().optional(),
|
|
2058
|
+
mnemonic: z3.string().optional(),
|
|
2059
|
+
accountIndex: z3.number().optional(),
|
|
2060
|
+
addressIndex: z3.number().optional()
|
|
2061
|
+
}).refine((data) => data.privateKey || data.mnemonic, {
|
|
2062
|
+
message: "Either privateKey or mnemonic must be provided"
|
|
2063
|
+
});
|
|
2064
|
+
var walletSetConfirmationSchema = z3.object({
|
|
2065
|
+
enabled: z3.boolean({ required_error: "enabled is required" })
|
|
2066
|
+
});
|
|
2067
|
+
var transactionConfirmSchema = z3.object({
|
|
2068
|
+
id: z3.string({ required_error: "id is required" }).min(1, "id must not be empty")
|
|
2069
|
+
});
|
|
2070
|
+
var transactionDenySchema = transactionConfirmSchema;
|
|
2071
|
+
|
|
2072
|
+
// src/tools/wallet/index.ts
|
|
2073
|
+
async function walletGenerate() {
|
|
2074
|
+
try {
|
|
2075
|
+
const key = generatePrivateKey3();
|
|
2076
|
+
const account = privateKeyToAccount3(key);
|
|
2077
|
+
return formatToolResponse({
|
|
2078
|
+
address: account.address,
|
|
2079
|
+
privateKey: key,
|
|
2080
|
+
warning: "Private key returned once. Never stored. Save it now."
|
|
2081
|
+
});
|
|
2082
|
+
} catch (err) {
|
|
2083
|
+
return formatToolError(
|
|
2084
|
+
"WALLET_GENERATE_FAILED",
|
|
2085
|
+
err instanceof Error ? err.message : "Unknown error"
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
async function walletGenerateMnemonic() {
|
|
2090
|
+
try {
|
|
2091
|
+
const mnemonic = generateMnemonic(english);
|
|
2092
|
+
const account = mnemonicToAccount2(mnemonic);
|
|
2093
|
+
return formatToolResponse({
|
|
2094
|
+
mnemonic,
|
|
2095
|
+
firstAddress: account.address,
|
|
2096
|
+
derivationPath: "m/44'/60'/0'/0/0",
|
|
2097
|
+
warning: "Mnemonic returned once. Never stored. Save it now."
|
|
2098
|
+
});
|
|
2099
|
+
} catch (err) {
|
|
2100
|
+
return formatToolError(
|
|
2101
|
+
"MNEMONIC_GENERATE_FAILED",
|
|
2102
|
+
err instanceof Error ? err.message : "Unknown error"
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
async function walletFromMnemonic(params) {
|
|
2107
|
+
try {
|
|
2108
|
+
const v = validateInput(walletFromMnemonicSchema, params);
|
|
2109
|
+
if (!v.success) return v.error;
|
|
2110
|
+
const { mnemonic, accountIndex = 0, addressIndex = 0 } = v.data;
|
|
2111
|
+
const account = mnemonicToAccount2(mnemonic, {
|
|
2112
|
+
accountIndex,
|
|
2113
|
+
addressIndex
|
|
2114
|
+
});
|
|
2115
|
+
return formatToolResponse({
|
|
2116
|
+
address: account.address,
|
|
2117
|
+
derivationPath: `m/44'/60'/${accountIndex}'/0/${addressIndex}`
|
|
2118
|
+
});
|
|
2119
|
+
} catch (err) {
|
|
2120
|
+
return formatToolError(
|
|
2121
|
+
"MNEMONIC_RESOLVE_FAILED",
|
|
2122
|
+
err instanceof Error ? err.message : "Unknown error"
|
|
2123
|
+
);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
async function walletDeriveAddresses(params) {
|
|
2127
|
+
try {
|
|
2128
|
+
const v = validateInput(walletDeriveAddressesSchema, params);
|
|
2129
|
+
if (!v.success) return v.error;
|
|
2130
|
+
const { mnemonic, count = 5 } = v.data;
|
|
2131
|
+
const addresses = Array.from({ length: count }, (_, i) => {
|
|
2132
|
+
const account = mnemonicToAccount2(mnemonic, { addressIndex: i });
|
|
2133
|
+
return {
|
|
2134
|
+
index: i,
|
|
2135
|
+
address: account.address,
|
|
2136
|
+
derivationPath: `m/44'/60'/0'/0/${i}`
|
|
2137
|
+
};
|
|
2138
|
+
});
|
|
2139
|
+
return formatToolResponse(addresses);
|
|
2140
|
+
} catch (err) {
|
|
2141
|
+
return formatToolError("DERIVE_FAILED", err instanceof Error ? err.message : "Unknown error");
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
async function walletGetActive() {
|
|
2145
|
+
try {
|
|
2146
|
+
const state = getWalletState();
|
|
2147
|
+
return formatToolResponse({
|
|
2148
|
+
address: state.address ?? getActiveAccount().address,
|
|
2149
|
+
chainId: state.chainId,
|
|
2150
|
+
mode: state.mode
|
|
2151
|
+
});
|
|
2152
|
+
} catch (err) {
|
|
2153
|
+
return formatToolError(
|
|
2154
|
+
"WALLET_STATE_FAILED",
|
|
2155
|
+
err instanceof Error ? err.message : "Unknown error"
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
async function walletActivate(params) {
|
|
2160
|
+
try {
|
|
2161
|
+
const v = validateInput(walletActivateSchema, params);
|
|
2162
|
+
if (!v.success) return v.error;
|
|
2163
|
+
const { privateKey, mnemonic, accountIndex, addressIndex } = v.data;
|
|
2164
|
+
const state = await activateWallet({
|
|
2165
|
+
privateKey,
|
|
2166
|
+
mnemonic,
|
|
2167
|
+
accountIndex,
|
|
2168
|
+
addressIndex
|
|
2169
|
+
});
|
|
2170
|
+
return formatToolResponse({
|
|
2171
|
+
address: state.address,
|
|
2172
|
+
chainId: state.chainId,
|
|
2173
|
+
mode: state.mode
|
|
2174
|
+
});
|
|
2175
|
+
} catch (err) {
|
|
2176
|
+
return formatToolError(
|
|
2177
|
+
"WALLET_ACTIVATE_FAILED",
|
|
2178
|
+
err instanceof Error ? err.message : "Unknown error"
|
|
2179
|
+
);
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
async function walletDeactivate() {
|
|
2183
|
+
try {
|
|
2184
|
+
await deactivateWallet();
|
|
2185
|
+
const state = getWalletState();
|
|
2186
|
+
return formatToolResponse({
|
|
2187
|
+
mode: state.mode,
|
|
2188
|
+
message: "Wallet deactivated. Reverted to read-only ephemeral wallet."
|
|
2189
|
+
});
|
|
2190
|
+
} catch (err) {
|
|
2191
|
+
return formatToolError(
|
|
2192
|
+
"WALLET_DEACTIVATE_FAILED",
|
|
2193
|
+
err instanceof Error ? err.message : "Unknown error"
|
|
2194
|
+
);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
async function walletSetConfirmation(params) {
|
|
2198
|
+
try {
|
|
2199
|
+
const v = validateInput(walletSetConfirmationSchema, params);
|
|
2200
|
+
if (!v.success) return v.error;
|
|
2201
|
+
const { enabled } = v.data;
|
|
2202
|
+
confirmationQueue.enabled = enabled;
|
|
2203
|
+
return formatToolResponse({
|
|
2204
|
+
confirmationRequired: confirmationQueue.enabled,
|
|
2205
|
+
message: enabled ? "Write confirmation enabled. Transactions will require explicit confirmation." : "Write confirmation disabled. Transactions will execute immediately."
|
|
2206
|
+
});
|
|
2207
|
+
} catch (err) {
|
|
2208
|
+
return formatToolError(
|
|
2209
|
+
"SET_CONFIRMATION_FAILED",
|
|
2210
|
+
err instanceof Error ? err.message : "Unknown error"
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
async function transactionConfirm(params) {
|
|
2215
|
+
try {
|
|
2216
|
+
const walletState = getWalletState();
|
|
2217
|
+
if (walletState.mode === "read-only") {
|
|
2218
|
+
return formatToolError(
|
|
2219
|
+
"WALLET_READ_ONLY",
|
|
2220
|
+
"transaction_confirm requires an active wallet. Activate a wallet first."
|
|
2221
|
+
);
|
|
2222
|
+
}
|
|
2223
|
+
const v = validateInput(transactionConfirmSchema, params);
|
|
2224
|
+
if (!v.success) return v.error;
|
|
2225
|
+
const { id } = v.data;
|
|
2226
|
+
const result = confirmationQueue.confirm(id);
|
|
2227
|
+
if (!result) {
|
|
2228
|
+
return formatToolError("NOT_FOUND", `No pending operation with ID: ${id}`);
|
|
2229
|
+
}
|
|
2230
|
+
if (result.stale) {
|
|
2231
|
+
confirmationQueue.complete(id);
|
|
2232
|
+
return formatToolError(
|
|
2233
|
+
"OPERATION_EXPIRED",
|
|
2234
|
+
`Operation ${id} was confirmed after TTL expiry and will not be executed.`
|
|
2235
|
+
);
|
|
2236
|
+
}
|
|
2237
|
+
if (result.operation.walletAddress && walletState.address && result.operation.walletAddress.toLowerCase() !== walletState.address.toLowerCase()) {
|
|
2238
|
+
return formatToolError(
|
|
2239
|
+
"WALLET_MISMATCH",
|
|
2240
|
+
`Operation ${id} was queued for wallet ${result.operation.walletAddress} but active wallet is ${walletState.address}. Deny this operation and re-submit.`
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
const execResult = await result.operation.executor(result.operation.params);
|
|
2244
|
+
confirmationQueue.complete(id);
|
|
2245
|
+
return execResult;
|
|
2246
|
+
} catch (err) {
|
|
2247
|
+
return formatToolError("CONFIRM_FAILED", err instanceof Error ? err.message : "Unknown error");
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
async function transactionDeny(params) {
|
|
2251
|
+
try {
|
|
2252
|
+
const v = validateInput(transactionDenySchema, params);
|
|
2253
|
+
if (!v.success) return v.error;
|
|
2254
|
+
const { id } = v.data;
|
|
2255
|
+
const removed = confirmationQueue.deny(id);
|
|
2256
|
+
if (!removed) {
|
|
2257
|
+
return formatToolError("NOT_FOUND", `No pending operation with ID: ${id}`);
|
|
2258
|
+
}
|
|
2259
|
+
return formatToolResponse({
|
|
2260
|
+
denied: true,
|
|
2261
|
+
id,
|
|
2262
|
+
message: "Operation denied and removed from queue."
|
|
2263
|
+
});
|
|
2264
|
+
} catch (err) {
|
|
2265
|
+
return formatToolError("DENY_FAILED", err instanceof Error ? err.message : "Unknown error");
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
async function transactionList() {
|
|
2269
|
+
try {
|
|
2270
|
+
confirmationQueue.pruneExpired();
|
|
2271
|
+
const operations = confirmationQueue.list();
|
|
2272
|
+
return formatToolResponse({
|
|
2273
|
+
count: operations.length,
|
|
2274
|
+
operations: operations.map((op) => ({
|
|
2275
|
+
id: op.id,
|
|
2276
|
+
type: op.type,
|
|
2277
|
+
description: op.description,
|
|
2278
|
+
createdAt: op.createdAt.toISOString(),
|
|
2279
|
+
expiresIn: Math.max(0, op.ttlMs - (Date.now() - op.createdAt.getTime()))
|
|
2280
|
+
}))
|
|
2281
|
+
});
|
|
2282
|
+
} catch (err) {
|
|
2283
|
+
return formatToolError("LIST_FAILED", err instanceof Error ? err.message : "Unknown error");
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
// src/tools/register.ts
|
|
2288
|
+
function getWalletToolDefinitions() {
|
|
2289
|
+
return [
|
|
2290
|
+
{
|
|
2291
|
+
name: "wallet_generate",
|
|
2292
|
+
description: "Generate a new random Ethereum wallet. Returns address and private key once \u2014 never stored.",
|
|
2293
|
+
inputSchema: { type: "object", properties: {} },
|
|
2294
|
+
handler: () => walletGenerate()
|
|
2295
|
+
},
|
|
2296
|
+
{
|
|
2297
|
+
name: "wallet_generate_mnemonic",
|
|
2298
|
+
description: "Generate a new BIP-39 mnemonic phrase with its first derived address.",
|
|
2299
|
+
inputSchema: { type: "object", properties: {} },
|
|
2300
|
+
handler: () => walletGenerateMnemonic()
|
|
2301
|
+
},
|
|
2302
|
+
{
|
|
2303
|
+
name: "wallet_from_mnemonic",
|
|
2304
|
+
description: "Derive an address from a BIP-39 mnemonic at optional account/address index. Does NOT return private key.",
|
|
2305
|
+
inputSchema: {
|
|
2306
|
+
type: "object",
|
|
2307
|
+
properties: {
|
|
2308
|
+
mnemonic: { type: "string", description: "BIP-39 mnemonic phrase" },
|
|
2309
|
+
accountIndex: {
|
|
2310
|
+
type: "number",
|
|
2311
|
+
description: "BIP-44 account index (default 0)"
|
|
2312
|
+
},
|
|
2313
|
+
addressIndex: {
|
|
2314
|
+
type: "number",
|
|
2315
|
+
description: "BIP-44 address index (default 0)"
|
|
2316
|
+
}
|
|
2317
|
+
},
|
|
2318
|
+
required: ["mnemonic"]
|
|
2319
|
+
},
|
|
2320
|
+
handler: (params) => walletFromMnemonic(params)
|
|
2321
|
+
},
|
|
2322
|
+
{
|
|
2323
|
+
name: "wallet_derive_addresses",
|
|
2324
|
+
description: "Derive multiple addresses from a mnemonic (1-20). Returns index, address, and derivation path.",
|
|
2325
|
+
inputSchema: {
|
|
2326
|
+
type: "object",
|
|
2327
|
+
properties: {
|
|
2328
|
+
mnemonic: { type: "string", description: "BIP-39 mnemonic phrase" },
|
|
2329
|
+
count: {
|
|
2330
|
+
type: "number",
|
|
2331
|
+
description: "Number of addresses to derive (1-20, default 5)"
|
|
2332
|
+
}
|
|
2333
|
+
},
|
|
2334
|
+
required: ["mnemonic"]
|
|
2335
|
+
},
|
|
2336
|
+
handler: (params) => walletDeriveAddresses(params)
|
|
2337
|
+
},
|
|
2338
|
+
{
|
|
2339
|
+
name: "wallet_get_active",
|
|
2340
|
+
description: "Get the currently active wallet address, chain ID, and mode (private-key, mnemonic, or read-only).",
|
|
2341
|
+
inputSchema: { type: "object", properties: {} },
|
|
2342
|
+
handler: () => walletGetActive()
|
|
2343
|
+
},
|
|
2344
|
+
{
|
|
2345
|
+
name: "wallet_activate",
|
|
2346
|
+
description: "Activate a wallet from a private key or mnemonic. Persists to disk (mode 0600) and emits wallet-changed.",
|
|
2347
|
+
inputSchema: {
|
|
2348
|
+
type: "object",
|
|
2349
|
+
properties: {
|
|
2350
|
+
privateKey: {
|
|
2351
|
+
type: "string",
|
|
2352
|
+
description: "Hex-encoded private key (0x-prefixed)"
|
|
2353
|
+
},
|
|
2354
|
+
mnemonic: {
|
|
2355
|
+
type: "string",
|
|
2356
|
+
description: "BIP-39 mnemonic phrase"
|
|
2357
|
+
},
|
|
2358
|
+
accountIndex: {
|
|
2359
|
+
type: "number",
|
|
2360
|
+
description: "BIP-44 account index (default 0, mnemonic only)"
|
|
2361
|
+
},
|
|
2362
|
+
addressIndex: {
|
|
2363
|
+
type: "number",
|
|
2364
|
+
description: "BIP-44 address index (default 0, mnemonic only)"
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
},
|
|
2368
|
+
handler: (params) => walletActivate(params)
|
|
2369
|
+
},
|
|
2370
|
+
{
|
|
2371
|
+
name: "wallet_deactivate",
|
|
2372
|
+
description: "Deactivate the current wallet, delete persisted key file, and revert to read-only ephemeral mode.",
|
|
2373
|
+
inputSchema: { type: "object", properties: {} },
|
|
2374
|
+
handler: () => walletDeactivate()
|
|
2375
|
+
},
|
|
2376
|
+
{
|
|
2377
|
+
name: "wallet_set_confirmation",
|
|
2378
|
+
description: "Toggle write confirmation at runtime. When enabled, write operations are queued and require explicit confirmation.",
|
|
2379
|
+
inputSchema: {
|
|
2380
|
+
type: "object",
|
|
2381
|
+
properties: {
|
|
2382
|
+
enabled: {
|
|
2383
|
+
type: "boolean",
|
|
2384
|
+
description: "true to require confirmation for writes, false to execute immediately"
|
|
2385
|
+
}
|
|
2386
|
+
},
|
|
2387
|
+
required: ["enabled"]
|
|
2388
|
+
},
|
|
2389
|
+
handler: (params) => walletSetConfirmation(params)
|
|
2390
|
+
}
|
|
2391
|
+
];
|
|
2392
|
+
}
|
|
2393
|
+
function getTransactionToolDefinitions() {
|
|
2394
|
+
return [
|
|
2395
|
+
{
|
|
2396
|
+
name: "transaction_confirm",
|
|
2397
|
+
description: "Confirm a pending operation by ID. Returns the operation details so the caller can execute it.",
|
|
2398
|
+
inputSchema: {
|
|
2399
|
+
type: "object",
|
|
2400
|
+
properties: {
|
|
2401
|
+
id: {
|
|
2402
|
+
type: "string",
|
|
2403
|
+
description: "UUID of the pending operation to confirm"
|
|
2404
|
+
}
|
|
2405
|
+
},
|
|
2406
|
+
required: ["id"]
|
|
2407
|
+
},
|
|
2408
|
+
handler: (params) => transactionConfirm(params)
|
|
2409
|
+
},
|
|
2410
|
+
{
|
|
2411
|
+
name: "transaction_deny",
|
|
2412
|
+
description: "Deny and remove a pending operation by ID without executing it.",
|
|
2413
|
+
inputSchema: {
|
|
2414
|
+
type: "object",
|
|
2415
|
+
properties: {
|
|
2416
|
+
id: {
|
|
2417
|
+
type: "string",
|
|
2418
|
+
description: "UUID of the pending operation to deny"
|
|
2419
|
+
}
|
|
2420
|
+
},
|
|
2421
|
+
required: ["id"]
|
|
2422
|
+
},
|
|
2423
|
+
handler: (params) => transactionDeny(params)
|
|
2424
|
+
},
|
|
2425
|
+
{
|
|
2426
|
+
name: "transaction_list",
|
|
2427
|
+
description: "List all pending operations awaiting confirmation. Automatically prunes expired entries.",
|
|
2428
|
+
inputSchema: { type: "object", properties: {} },
|
|
2429
|
+
handler: () => transactionList()
|
|
2430
|
+
}
|
|
2431
|
+
];
|
|
2432
|
+
}
|
|
2433
|
+
function getUtilityToolDefinitions() {
|
|
2434
|
+
return [
|
|
2435
|
+
{
|
|
2436
|
+
name: "server_status",
|
|
2437
|
+
description: "Get current server status including wallet mode, active chain, confirmation setting, and backend health.",
|
|
2438
|
+
inputSchema: { type: "object", properties: {} },
|
|
2439
|
+
handler: () => serverStatus()
|
|
2440
|
+
},
|
|
2441
|
+
{
|
|
2442
|
+
name: "list_supported_chains",
|
|
2443
|
+
description: "List all supported EVM chains with their chain IDs, names, and native currencies.",
|
|
2444
|
+
inputSchema: { type: "object", properties: {} },
|
|
2445
|
+
handler: () => listSupportedChains()
|
|
2446
|
+
}
|
|
2447
|
+
];
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// src/tokens/registry.ts
|
|
2451
|
+
var WELL_KNOWN_TOKENS = {
|
|
2452
|
+
// ── Ethereum Mainnet (1) ──────────────────────────────────────────
|
|
2453
|
+
1: {
|
|
2454
|
+
USDT: {
|
|
2455
|
+
address: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
|
2456
|
+
decimals: 6,
|
|
2457
|
+
name: "Tether USD",
|
|
2458
|
+
symbol: "USDT"
|
|
2459
|
+
},
|
|
2460
|
+
USDC: {
|
|
2461
|
+
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
2462
|
+
decimals: 6,
|
|
2463
|
+
name: "USD Coin",
|
|
2464
|
+
symbol: "USDC"
|
|
2465
|
+
},
|
|
2466
|
+
WETH: {
|
|
2467
|
+
address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
|
2468
|
+
decimals: 18,
|
|
2469
|
+
name: "Wrapped Ether",
|
|
2470
|
+
symbol: "WETH"
|
|
2471
|
+
},
|
|
2472
|
+
DAI: {
|
|
2473
|
+
address: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
|
2474
|
+
decimals: 18,
|
|
2475
|
+
name: "Dai Stablecoin",
|
|
2476
|
+
symbol: "DAI"
|
|
2477
|
+
},
|
|
2478
|
+
WBTC: {
|
|
2479
|
+
address: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
|
|
2480
|
+
decimals: 8,
|
|
2481
|
+
name: "Wrapped BTC",
|
|
2482
|
+
symbol: "WBTC"
|
|
2483
|
+
},
|
|
2484
|
+
LINK: {
|
|
2485
|
+
address: "0x514910771AF9Ca656af840dff83E8264EcF986CA",
|
|
2486
|
+
decimals: 18,
|
|
2487
|
+
name: "Chainlink",
|
|
2488
|
+
symbol: "LINK"
|
|
2489
|
+
},
|
|
2490
|
+
UNI: {
|
|
2491
|
+
address: "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984",
|
|
2492
|
+
decimals: 18,
|
|
2493
|
+
name: "Uniswap",
|
|
2494
|
+
symbol: "UNI"
|
|
2495
|
+
},
|
|
2496
|
+
AAVE: {
|
|
2497
|
+
address: "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9",
|
|
2498
|
+
decimals: 18,
|
|
2499
|
+
name: "Aave",
|
|
2500
|
+
symbol: "AAVE"
|
|
2501
|
+
}
|
|
2502
|
+
},
|
|
2503
|
+
// ── BNB Smart Chain (56) ──────────────────────────────────────────
|
|
2504
|
+
56: {
|
|
2505
|
+
USDT: {
|
|
2506
|
+
address: "0x55d398326f99059fF775485246999027B3197955",
|
|
2507
|
+
decimals: 18,
|
|
2508
|
+
name: "Binance-Peg BSC-USD",
|
|
2509
|
+
symbol: "USDT"
|
|
2510
|
+
},
|
|
2511
|
+
USDC: {
|
|
2512
|
+
address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
2513
|
+
decimals: 18,
|
|
2514
|
+
name: "Binance-Peg USD Coin",
|
|
2515
|
+
symbol: "USDC"
|
|
2516
|
+
},
|
|
2517
|
+
WBNB: {
|
|
2518
|
+
address: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
|
|
2519
|
+
decimals: 18,
|
|
2520
|
+
name: "Wrapped BNB",
|
|
2521
|
+
symbol: "WBNB"
|
|
2522
|
+
},
|
|
2523
|
+
WETH: {
|
|
2524
|
+
address: "0x2170Ed0880ac9A755fd29B2688956BD959F933F8",
|
|
2525
|
+
decimals: 18,
|
|
2526
|
+
name: "Binance-Peg Ethereum",
|
|
2527
|
+
symbol: "WETH"
|
|
2528
|
+
},
|
|
2529
|
+
DAI: {
|
|
2530
|
+
address: "0x1AF3F329e8BE154074D8769D1FFa4eE058B1DBc3",
|
|
2531
|
+
decimals: 18,
|
|
2532
|
+
name: "Binance-Peg Dai",
|
|
2533
|
+
symbol: "DAI"
|
|
2534
|
+
},
|
|
2535
|
+
BTCB: {
|
|
2536
|
+
address: "0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c",
|
|
2537
|
+
decimals: 18,
|
|
2538
|
+
name: "Binance-Peg BTCB",
|
|
2539
|
+
symbol: "BTCB"
|
|
2540
|
+
},
|
|
2541
|
+
CAKE: {
|
|
2542
|
+
address: "0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82",
|
|
2543
|
+
decimals: 18,
|
|
2544
|
+
name: "PancakeSwap",
|
|
2545
|
+
symbol: "CAKE"
|
|
2546
|
+
}
|
|
2547
|
+
},
|
|
2548
|
+
// ── Polygon (137) ─────────────────────────────────────────────────
|
|
2549
|
+
137: {
|
|
2550
|
+
USDT: {
|
|
2551
|
+
address: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
|
|
2552
|
+
decimals: 6,
|
|
2553
|
+
name: "Tether USD",
|
|
2554
|
+
symbol: "USDT"
|
|
2555
|
+
},
|
|
2556
|
+
USDC: {
|
|
2557
|
+
address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
2558
|
+
decimals: 6,
|
|
2559
|
+
name: "USD Coin",
|
|
2560
|
+
symbol: "USDC"
|
|
2561
|
+
},
|
|
2562
|
+
"USDC.E": {
|
|
2563
|
+
address: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
|
|
2564
|
+
decimals: 6,
|
|
2565
|
+
name: "Bridged USDC",
|
|
2566
|
+
symbol: "USDC.e"
|
|
2567
|
+
},
|
|
2568
|
+
WETH: {
|
|
2569
|
+
address: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619",
|
|
2570
|
+
decimals: 18,
|
|
2571
|
+
name: "Wrapped Ether",
|
|
2572
|
+
symbol: "WETH"
|
|
2573
|
+
},
|
|
2574
|
+
WMATIC: {
|
|
2575
|
+
address: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
|
|
2576
|
+
decimals: 18,
|
|
2577
|
+
name: "Wrapped Matic",
|
|
2578
|
+
symbol: "WMATIC"
|
|
2579
|
+
},
|
|
2580
|
+
WPOL: {
|
|
2581
|
+
address: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
|
|
2582
|
+
decimals: 18,
|
|
2583
|
+
name: "Wrapped POL",
|
|
2584
|
+
symbol: "WPOL"
|
|
2585
|
+
},
|
|
2586
|
+
DAI: {
|
|
2587
|
+
address: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063",
|
|
2588
|
+
decimals: 18,
|
|
2589
|
+
name: "Dai Stablecoin",
|
|
2590
|
+
symbol: "DAI"
|
|
2591
|
+
},
|
|
2592
|
+
WBTC: {
|
|
2593
|
+
address: "0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6",
|
|
2594
|
+
decimals: 8,
|
|
2595
|
+
name: "Wrapped BTC",
|
|
2596
|
+
symbol: "WBTC"
|
|
2597
|
+
},
|
|
2598
|
+
LINK: {
|
|
2599
|
+
address: "0x53E0bca35eC356BD5ddDFebbD1Fc0fD03FaBad39",
|
|
2600
|
+
decimals: 18,
|
|
2601
|
+
name: "Chainlink",
|
|
2602
|
+
symbol: "LINK"
|
|
2603
|
+
},
|
|
2604
|
+
AAVE: {
|
|
2605
|
+
address: "0xD6DF932A45C0f255f85145f286eA0b292B21C90B",
|
|
2606
|
+
decimals: 18,
|
|
2607
|
+
name: "Aave",
|
|
2608
|
+
symbol: "AAVE"
|
|
2609
|
+
}
|
|
2610
|
+
},
|
|
2611
|
+
// ── Arbitrum One (42161) ──────────────────────────────────────────
|
|
2612
|
+
42161: {
|
|
2613
|
+
USDT: {
|
|
2614
|
+
address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
|
|
2615
|
+
decimals: 6,
|
|
2616
|
+
name: "Tether USD",
|
|
2617
|
+
symbol: "USDT"
|
|
2618
|
+
},
|
|
2619
|
+
USDC: {
|
|
2620
|
+
address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
|
2621
|
+
decimals: 6,
|
|
2622
|
+
name: "USD Coin",
|
|
2623
|
+
symbol: "USDC"
|
|
2624
|
+
},
|
|
2625
|
+
"USDC.E": {
|
|
2626
|
+
address: "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8",
|
|
2627
|
+
decimals: 6,
|
|
2628
|
+
name: "Bridged USDC",
|
|
2629
|
+
symbol: "USDC.e"
|
|
2630
|
+
},
|
|
2631
|
+
WETH: {
|
|
2632
|
+
address: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
|
|
2633
|
+
decimals: 18,
|
|
2634
|
+
name: "Wrapped Ether",
|
|
2635
|
+
symbol: "WETH"
|
|
2636
|
+
},
|
|
2637
|
+
DAI: {
|
|
2638
|
+
address: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1",
|
|
2639
|
+
decimals: 18,
|
|
2640
|
+
name: "Dai Stablecoin",
|
|
2641
|
+
symbol: "DAI"
|
|
2642
|
+
},
|
|
2643
|
+
WBTC: {
|
|
2644
|
+
address: "0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f",
|
|
2645
|
+
decimals: 8,
|
|
2646
|
+
name: "Wrapped BTC",
|
|
2647
|
+
symbol: "WBTC"
|
|
2648
|
+
},
|
|
2649
|
+
ARB: {
|
|
2650
|
+
address: "0x912CE59144191C1204E64559FE8253a0e49E6548",
|
|
2651
|
+
decimals: 18,
|
|
2652
|
+
name: "Arbitrum",
|
|
2653
|
+
symbol: "ARB"
|
|
2654
|
+
},
|
|
2655
|
+
LINK: {
|
|
2656
|
+
address: "0xf97f4df75117a78c1A5a0DBb814Af92458539FB4",
|
|
2657
|
+
decimals: 18,
|
|
2658
|
+
name: "Chainlink",
|
|
2659
|
+
symbol: "LINK"
|
|
2660
|
+
}
|
|
2661
|
+
},
|
|
2662
|
+
// ── Optimism (10) ─────────────────────────────────────────────────
|
|
2663
|
+
10: {
|
|
2664
|
+
USDT: {
|
|
2665
|
+
address: "0x94b008aA00579c1307B0EF2c499aD98a8ce58e58",
|
|
2666
|
+
decimals: 6,
|
|
2667
|
+
name: "Tether USD",
|
|
2668
|
+
symbol: "USDT"
|
|
2669
|
+
},
|
|
2670
|
+
USDC: {
|
|
2671
|
+
address: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
|
|
2672
|
+
decimals: 6,
|
|
2673
|
+
name: "USD Coin",
|
|
2674
|
+
symbol: "USDC"
|
|
2675
|
+
},
|
|
2676
|
+
"USDC.E": {
|
|
2677
|
+
address: "0x7F5c764cBc14f9669B88837ca1490cCa17c31607",
|
|
2678
|
+
decimals: 6,
|
|
2679
|
+
name: "Bridged USDC",
|
|
2680
|
+
symbol: "USDC.e"
|
|
2681
|
+
},
|
|
2682
|
+
WETH: {
|
|
2683
|
+
address: "0x4200000000000000000000000000000000000006",
|
|
2684
|
+
decimals: 18,
|
|
2685
|
+
name: "Wrapped Ether",
|
|
2686
|
+
symbol: "WETH"
|
|
2687
|
+
},
|
|
2688
|
+
DAI: {
|
|
2689
|
+
address: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1",
|
|
2690
|
+
decimals: 18,
|
|
2691
|
+
name: "Dai Stablecoin",
|
|
2692
|
+
symbol: "DAI"
|
|
2693
|
+
},
|
|
2694
|
+
WBTC: {
|
|
2695
|
+
address: "0x68f180fcCe6836688e9084f035309E29Bf0A2095",
|
|
2696
|
+
decimals: 8,
|
|
2697
|
+
name: "Wrapped BTC",
|
|
2698
|
+
symbol: "WBTC"
|
|
2699
|
+
},
|
|
2700
|
+
OP: {
|
|
2701
|
+
address: "0x4200000000000000000000000000000000000042",
|
|
2702
|
+
decimals: 18,
|
|
2703
|
+
name: "Optimism",
|
|
2704
|
+
symbol: "OP"
|
|
2705
|
+
},
|
|
2706
|
+
LINK: {
|
|
2707
|
+
address: "0x350a791Bfc2C21F9Ed5d10980Dad2e2638ffa7f6",
|
|
2708
|
+
decimals: 18,
|
|
2709
|
+
name: "Chainlink",
|
|
2710
|
+
symbol: "LINK"
|
|
2711
|
+
}
|
|
2712
|
+
},
|
|
2713
|
+
// ── Base (8453) ───────────────────────────────────────────────────
|
|
2714
|
+
8453: {
|
|
2715
|
+
USDC: {
|
|
2716
|
+
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
2717
|
+
decimals: 6,
|
|
2718
|
+
name: "USD Coin",
|
|
2719
|
+
symbol: "USDC"
|
|
2720
|
+
},
|
|
2721
|
+
USDT: {
|
|
2722
|
+
address: "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2",
|
|
2723
|
+
decimals: 6,
|
|
2724
|
+
name: "Tether USD",
|
|
2725
|
+
symbol: "USDT"
|
|
2726
|
+
},
|
|
2727
|
+
WETH: {
|
|
2728
|
+
address: "0x4200000000000000000000000000000000000006",
|
|
2729
|
+
decimals: 18,
|
|
2730
|
+
name: "Wrapped Ether",
|
|
2731
|
+
symbol: "WETH"
|
|
2732
|
+
},
|
|
2733
|
+
DAI: {
|
|
2734
|
+
address: "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
|
|
2735
|
+
decimals: 18,
|
|
2736
|
+
name: "Dai Stablecoin",
|
|
2737
|
+
symbol: "DAI"
|
|
2738
|
+
},
|
|
2739
|
+
CBBTC: {
|
|
2740
|
+
address: "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
|
|
2741
|
+
decimals: 8,
|
|
2742
|
+
name: "Coinbase Wrapped BTC",
|
|
2743
|
+
symbol: "cbBTC"
|
|
2744
|
+
},
|
|
2745
|
+
AERO: {
|
|
2746
|
+
address: "0x940181a94A35A4569E4529A3CDfB74e38FD98631",
|
|
2747
|
+
decimals: 18,
|
|
2748
|
+
name: "Aerodrome",
|
|
2749
|
+
symbol: "AERO"
|
|
2750
|
+
}
|
|
2751
|
+
},
|
|
2752
|
+
// ── Linea (59144) ─────────────────────────────────────────────────
|
|
2753
|
+
59144: {
|
|
2754
|
+
WETH: {
|
|
2755
|
+
address: "0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f",
|
|
2756
|
+
decimals: 18,
|
|
2757
|
+
name: "Wrapped Ether",
|
|
2758
|
+
symbol: "WETH"
|
|
2759
|
+
},
|
|
2760
|
+
USDC: {
|
|
2761
|
+
address: "0x176211869cA2b568f2A7D4EE941E073a821EE1ff",
|
|
2762
|
+
decimals: 6,
|
|
2763
|
+
name: "USDC",
|
|
2764
|
+
symbol: "USDC"
|
|
2765
|
+
},
|
|
2766
|
+
USDT: {
|
|
2767
|
+
address: "0xA219439258ca9da29E9Cc4cE5596924745e12B93",
|
|
2768
|
+
decimals: 6,
|
|
2769
|
+
name: "Tether USD",
|
|
2770
|
+
symbol: "USDT"
|
|
2771
|
+
},
|
|
2772
|
+
DAI: {
|
|
2773
|
+
address: "0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5",
|
|
2774
|
+
decimals: 18,
|
|
2775
|
+
name: "Dai Stablecoin",
|
|
2776
|
+
symbol: "DAI"
|
|
2777
|
+
},
|
|
2778
|
+
WBTC: {
|
|
2779
|
+
address: "0x3aAB2285ddcDdaD8edf438C1bAB47e1a9D05a9b4",
|
|
2780
|
+
decimals: 8,
|
|
2781
|
+
name: "Wrapped BTC",
|
|
2782
|
+
symbol: "WBTC"
|
|
2783
|
+
}
|
|
2784
|
+
},
|
|
2785
|
+
// ── Avalanche C-Chain (43114) ─────────────────────────────────────
|
|
2786
|
+
43114: {
|
|
2787
|
+
USDT: {
|
|
2788
|
+
address: "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7",
|
|
2789
|
+
decimals: 6,
|
|
2790
|
+
name: "Tether USD",
|
|
2791
|
+
symbol: "USDT"
|
|
2792
|
+
},
|
|
2793
|
+
USDC: {
|
|
2794
|
+
address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
|
|
2795
|
+
decimals: 6,
|
|
2796
|
+
name: "USD Coin",
|
|
2797
|
+
symbol: "USDC"
|
|
2798
|
+
},
|
|
2799
|
+
WAVAX: {
|
|
2800
|
+
address: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7",
|
|
2801
|
+
decimals: 18,
|
|
2802
|
+
name: "Wrapped AVAX",
|
|
2803
|
+
symbol: "WAVAX"
|
|
2804
|
+
},
|
|
2805
|
+
"WETH.E": {
|
|
2806
|
+
address: "0x49D5c2BdFfac6CE2BFdB6640F4F80f226bc10bAB",
|
|
2807
|
+
decimals: 18,
|
|
2808
|
+
name: "Wrapped Ether",
|
|
2809
|
+
symbol: "WETH.e"
|
|
2810
|
+
},
|
|
2811
|
+
WETH: {
|
|
2812
|
+
address: "0x49D5c2BdFfac6CE2BFdB6640F4F80f226bc10bAB",
|
|
2813
|
+
decimals: 18,
|
|
2814
|
+
name: "Wrapped Ether",
|
|
2815
|
+
symbol: "WETH"
|
|
2816
|
+
},
|
|
2817
|
+
DAI: {
|
|
2818
|
+
address: "0xd586E7F844cEa2F87f50152665BCbc2C279D8d70",
|
|
2819
|
+
decimals: 18,
|
|
2820
|
+
name: "Dai Stablecoin",
|
|
2821
|
+
symbol: "DAI"
|
|
2822
|
+
},
|
|
2823
|
+
"WBTC.E": {
|
|
2824
|
+
address: "0x50b7545627a5162F82A992c33b87aDc75187B218",
|
|
2825
|
+
decimals: 8,
|
|
2826
|
+
name: "Wrapped BTC",
|
|
2827
|
+
symbol: "WBTC.e"
|
|
2828
|
+
},
|
|
2829
|
+
WBTC: {
|
|
2830
|
+
address: "0x50b7545627a5162F82A992c33b87aDc75187B218",
|
|
2831
|
+
decimals: 8,
|
|
2832
|
+
name: "Wrapped BTC",
|
|
2833
|
+
symbol: "WBTC"
|
|
2834
|
+
}
|
|
2835
|
+
},
|
|
2836
|
+
// ── Blast (81457) ─────────────────────────────────────────────────
|
|
2837
|
+
81457: {
|
|
2838
|
+
WETH: {
|
|
2839
|
+
address: "0x4300000000000000000000000000000000000004",
|
|
2840
|
+
decimals: 18,
|
|
2841
|
+
name: "Wrapped Ether",
|
|
2842
|
+
symbol: "WETH"
|
|
2843
|
+
},
|
|
2844
|
+
USDB: {
|
|
2845
|
+
address: "0x4300000000000000000000000000000000000003",
|
|
2846
|
+
decimals: 18,
|
|
2847
|
+
name: "USDB",
|
|
2848
|
+
symbol: "USDB"
|
|
2849
|
+
},
|
|
2850
|
+
BLAST: {
|
|
2851
|
+
address: "0xb1a5700fA2358173Fe465e6eA4Ff52E36e88E2ad",
|
|
2852
|
+
decimals: 18,
|
|
2853
|
+
name: "Blast",
|
|
2854
|
+
symbol: "BLAST"
|
|
2855
|
+
}
|
|
2856
|
+
},
|
|
2857
|
+
// ── zkSync Era (324) ──────────────────────────────────────────────
|
|
2858
|
+
324: {
|
|
2859
|
+
USDT: {
|
|
2860
|
+
address: "0x493257fD37EDB34451f62EDf8D2a0C418852bA4C",
|
|
2861
|
+
decimals: 6,
|
|
2862
|
+
name: "Tether USD",
|
|
2863
|
+
symbol: "USDT"
|
|
2864
|
+
},
|
|
2865
|
+
USDC: {
|
|
2866
|
+
address: "0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4",
|
|
2867
|
+
decimals: 6,
|
|
2868
|
+
name: "USD Coin",
|
|
2869
|
+
symbol: "USDC"
|
|
2870
|
+
},
|
|
2871
|
+
WETH: {
|
|
2872
|
+
address: "0x5AEa5775959fBC2557Cc8789bC1bf90A239D9a91",
|
|
2873
|
+
decimals: 18,
|
|
2874
|
+
name: "Wrapped Ether",
|
|
2875
|
+
symbol: "WETH"
|
|
2876
|
+
},
|
|
2877
|
+
WBTC: {
|
|
2878
|
+
address: "0xBBeB516fb02a01611cBBE0453Fe3c580D7281011",
|
|
2879
|
+
decimals: 8,
|
|
2880
|
+
name: "Wrapped BTC",
|
|
2881
|
+
symbol: "WBTC"
|
|
2882
|
+
}
|
|
2883
|
+
},
|
|
2884
|
+
// ── Scroll (534352) ───────────────────────────────────────────────
|
|
2885
|
+
534352: {
|
|
2886
|
+
USDT: {
|
|
2887
|
+
address: "0xf55BEC9cafDbE8730f096Aa55dad6D22d44099Df",
|
|
2888
|
+
decimals: 6,
|
|
2889
|
+
name: "Tether USD",
|
|
2890
|
+
symbol: "USDT"
|
|
2891
|
+
},
|
|
2892
|
+
USDC: {
|
|
2893
|
+
address: "0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4",
|
|
2894
|
+
decimals: 6,
|
|
2895
|
+
name: "USD Coin",
|
|
2896
|
+
symbol: "USDC"
|
|
2897
|
+
},
|
|
2898
|
+
WETH: {
|
|
2899
|
+
address: "0x5300000000000000000000000000000000000004",
|
|
2900
|
+
decimals: 18,
|
|
2901
|
+
name: "Wrapped Ether",
|
|
2902
|
+
symbol: "WETH"
|
|
2903
|
+
},
|
|
2904
|
+
DAI: {
|
|
2905
|
+
address: "0xcA77eB3fEFe3725Dc33bccB54eDEFc3D9f764f97",
|
|
2906
|
+
decimals: 18,
|
|
2907
|
+
name: "Dai Stablecoin",
|
|
2908
|
+
symbol: "DAI"
|
|
2909
|
+
},
|
|
2910
|
+
WBTC: {
|
|
2911
|
+
address: "0x3C1BCa5a656e69edCD0D4E36BEbb3FcDAcA60Cf1",
|
|
2912
|
+
decimals: 8,
|
|
2913
|
+
name: "Wrapped BTC",
|
|
2914
|
+
symbol: "WBTC"
|
|
2915
|
+
}
|
|
2916
|
+
},
|
|
2917
|
+
// ── Gnosis (100) ──────────────────────────────────────────────────
|
|
2918
|
+
100: {
|
|
2919
|
+
USDT: {
|
|
2920
|
+
address: "0x4ECaBa5870353805a9F068101A40E0f32ed605C6",
|
|
2921
|
+
decimals: 6,
|
|
2922
|
+
name: "Tether USD",
|
|
2923
|
+
symbol: "USDT"
|
|
2924
|
+
},
|
|
2925
|
+
USDC: {
|
|
2926
|
+
address: "0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83",
|
|
2927
|
+
decimals: 6,
|
|
2928
|
+
name: "USD Coin",
|
|
2929
|
+
symbol: "USDC"
|
|
2930
|
+
},
|
|
2931
|
+
WETH: {
|
|
2932
|
+
address: "0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1",
|
|
2933
|
+
decimals: 18,
|
|
2934
|
+
name: "Wrapped Ether",
|
|
2935
|
+
symbol: "WETH"
|
|
2936
|
+
},
|
|
2937
|
+
WXDAI: {
|
|
2938
|
+
address: "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d",
|
|
2939
|
+
decimals: 18,
|
|
2940
|
+
name: "Wrapped xDAI",
|
|
2941
|
+
symbol: "WXDAI"
|
|
2942
|
+
},
|
|
2943
|
+
GNO: {
|
|
2944
|
+
address: "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb",
|
|
2945
|
+
decimals: 18,
|
|
2946
|
+
name: "Gnosis",
|
|
2947
|
+
symbol: "GNO"
|
|
2948
|
+
}
|
|
2949
|
+
},
|
|
2950
|
+
// ── Celo (42220) ──────────────────────────────────────────────────
|
|
2951
|
+
42220: {
|
|
2952
|
+
USDT: {
|
|
2953
|
+
address: "0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e",
|
|
2954
|
+
decimals: 6,
|
|
2955
|
+
name: "Tether USD",
|
|
2956
|
+
symbol: "USDT"
|
|
2957
|
+
},
|
|
2958
|
+
USDC: {
|
|
2959
|
+
address: "0xcebA9300f2b948710d2653dD7B07f33A8B32118C",
|
|
2960
|
+
decimals: 6,
|
|
2961
|
+
name: "USD Coin",
|
|
2962
|
+
symbol: "USDC"
|
|
2963
|
+
},
|
|
2964
|
+
CELO: {
|
|
2965
|
+
address: "0x471EcE3750Da237f93B8E339c536989b8978a438",
|
|
2966
|
+
decimals: 18,
|
|
2967
|
+
name: "Celo",
|
|
2968
|
+
symbol: "CELO"
|
|
2969
|
+
}
|
|
2970
|
+
},
|
|
2971
|
+
// ── Mantle (5000) ─────────────────────────────────────────────────
|
|
2972
|
+
5e3: {
|
|
2973
|
+
USDT: {
|
|
2974
|
+
address: "0x201EBa5CC46D216Ce6DC03F6a759e8E766e956aE",
|
|
2975
|
+
decimals: 6,
|
|
2976
|
+
name: "Tether USD",
|
|
2977
|
+
symbol: "USDT"
|
|
2978
|
+
},
|
|
2979
|
+
USDC: {
|
|
2980
|
+
address: "0x09Bc4E0D864854c6aFB6eB9A9cdF58aC190D0dF9",
|
|
2981
|
+
decimals: 6,
|
|
2982
|
+
name: "USD Coin",
|
|
2983
|
+
symbol: "USDC"
|
|
2984
|
+
},
|
|
2985
|
+
WMNT: {
|
|
2986
|
+
address: "0x78c1b0C915c4FAA5FffA6CAbf0219DA63d7f4cb8",
|
|
2987
|
+
decimals: 18,
|
|
2988
|
+
name: "Wrapped Mantle",
|
|
2989
|
+
symbol: "WMNT"
|
|
2990
|
+
},
|
|
2991
|
+
WETH: {
|
|
2992
|
+
address: "0xdEAddEaDdeadDEadDEADDEAddEADDEAddead1111",
|
|
2993
|
+
decimals: 18,
|
|
2994
|
+
name: "Wrapped Ether",
|
|
2995
|
+
symbol: "WETH"
|
|
2996
|
+
}
|
|
2997
|
+
},
|
|
2998
|
+
// ── Mode (34443) ──────────────────────────────────────────────────
|
|
2999
|
+
34443: {
|
|
3000
|
+
USDT: {
|
|
3001
|
+
address: "0xf0F161fDA2712DB8b566946122a5af183995e2eD",
|
|
3002
|
+
decimals: 6,
|
|
3003
|
+
name: "Tether USD",
|
|
3004
|
+
symbol: "USDT"
|
|
3005
|
+
},
|
|
3006
|
+
USDC: {
|
|
3007
|
+
address: "0xd988097fb8612cc24eeC14542bC03424c656005f",
|
|
3008
|
+
decimals: 6,
|
|
3009
|
+
name: "USD Coin",
|
|
3010
|
+
symbol: "USDC"
|
|
3011
|
+
},
|
|
3012
|
+
WETH: {
|
|
3013
|
+
address: "0x4200000000000000000000000000000000000006",
|
|
3014
|
+
decimals: 18,
|
|
3015
|
+
name: "Wrapped Ether",
|
|
3016
|
+
symbol: "WETH"
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
};
|
|
3020
|
+
function lookupToken(symbol, chainId) {
|
|
3021
|
+
return WELL_KNOWN_TOKENS[chainId]?.[symbol.toUpperCase()];
|
|
3022
|
+
}
|
|
3023
|
+
function getChainTokens(chainId) {
|
|
3024
|
+
return WELL_KNOWN_TOKENS[chainId];
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
// src/tokens/resolver.ts
|
|
3028
|
+
var NATIVE_ALIASES = {
|
|
3029
|
+
1: { ETH: "WETH" },
|
|
3030
|
+
56: { BNB: "WBNB" },
|
|
3031
|
+
137: { MATIC: "WMATIC", POL: "WMATIC" },
|
|
3032
|
+
42161: { ETH: "WETH" },
|
|
3033
|
+
10: { ETH: "WETH" },
|
|
3034
|
+
8453: { ETH: "WETH" },
|
|
3035
|
+
59144: { ETH: "WETH" },
|
|
3036
|
+
43114: { AVAX: "WAVAX" },
|
|
3037
|
+
81457: { ETH: "WETH" },
|
|
3038
|
+
324: { ETH: "WETH" },
|
|
3039
|
+
534352: { ETH: "WETH" },
|
|
3040
|
+
100: { XDAI: "WXDAI" },
|
|
3041
|
+
5e3: { MNT: "WMNT" },
|
|
3042
|
+
34443: { ETH: "WETH" }
|
|
3043
|
+
};
|
|
3044
|
+
async function resolveToken(symbol, chainId) {
|
|
3045
|
+
const entry = lookupToken(symbol, chainId);
|
|
3046
|
+
if (entry) {
|
|
3047
|
+
return { ...entry, chainId, source: "registry" };
|
|
3048
|
+
}
|
|
3049
|
+
const upperSymbol = symbol.toUpperCase();
|
|
3050
|
+
const chainAliases = NATIVE_ALIASES[chainId];
|
|
3051
|
+
if (chainAliases) {
|
|
3052
|
+
const wrappedSymbol = chainAliases[upperSymbol];
|
|
3053
|
+
if (wrappedSymbol) {
|
|
3054
|
+
const wrappedEntry = lookupToken(wrappedSymbol, chainId);
|
|
3055
|
+
if (wrappedEntry) {
|
|
3056
|
+
return {
|
|
3057
|
+
...wrappedEntry,
|
|
3058
|
+
chainId,
|
|
3059
|
+
source: "registry",
|
|
3060
|
+
note: `${upperSymbol} is a native token; resolved to its wrapped equivalent ${wrappedSymbol}.`
|
|
3061
|
+
};
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
return resolveViaDexScreener(symbol, chainId);
|
|
3066
|
+
}
|
|
3067
|
+
function listTokens(chainId) {
|
|
3068
|
+
const tokens = getChainTokens(chainId);
|
|
3069
|
+
if (!tokens) return [];
|
|
3070
|
+
return Object.values(tokens);
|
|
3071
|
+
}
|
|
3072
|
+
var DEXSCREENER_CHAIN_SLUGS = {
|
|
3073
|
+
1: "ethereum",
|
|
3074
|
+
56: "bsc",
|
|
3075
|
+
137: "polygon",
|
|
3076
|
+
42161: "arbitrum",
|
|
3077
|
+
10: "optimism",
|
|
3078
|
+
8453: "base",
|
|
3079
|
+
59144: "linea",
|
|
3080
|
+
43114: "avalanche",
|
|
3081
|
+
81457: "blast",
|
|
3082
|
+
324: "zksync",
|
|
3083
|
+
534352: "scroll",
|
|
3084
|
+
100: "gnosis",
|
|
3085
|
+
42220: "celo",
|
|
3086
|
+
5e3: "mantle",
|
|
3087
|
+
34443: "mode"
|
|
3088
|
+
};
|
|
3089
|
+
async function resolveViaDexScreener(symbol, chainId) {
|
|
3090
|
+
const chainSlug = DEXSCREENER_CHAIN_SLUGS[chainId];
|
|
3091
|
+
if (!chainSlug) return null;
|
|
3092
|
+
try {
|
|
3093
|
+
const response = await fetch(
|
|
3094
|
+
`https://api.dexscreener.com/latest/dex/search?q=${encodeURIComponent(symbol)}`
|
|
3095
|
+
);
|
|
3096
|
+
if (!response.ok) return null;
|
|
3097
|
+
const data = await response.json();
|
|
3098
|
+
if (!data.pairs?.length) return null;
|
|
3099
|
+
const upperSymbol = symbol.toUpperCase();
|
|
3100
|
+
const candidates = data.pairs.filter((p2) => p2.chainId === chainSlug).flatMap((p2) => {
|
|
3101
|
+
const matches = [];
|
|
3102
|
+
if (p2.baseToken.symbol.toUpperCase() === upperSymbol) {
|
|
3103
|
+
matches.push({ ...p2.baseToken, liquidity: p2.liquidity?.usd ?? 0 });
|
|
3104
|
+
}
|
|
3105
|
+
if (p2.quoteToken.symbol.toUpperCase() === upperSymbol) {
|
|
3106
|
+
matches.push({ ...p2.quoteToken, liquidity: p2.liquidity?.usd ?? 0 });
|
|
3107
|
+
}
|
|
3108
|
+
return matches;
|
|
3109
|
+
});
|
|
3110
|
+
if (!candidates.length) return null;
|
|
3111
|
+
candidates.sort((a2, b) => b.liquidity - a2.liquidity);
|
|
3112
|
+
const best = candidates[0];
|
|
3113
|
+
const chain = getChainById(chainId);
|
|
3114
|
+
const decimals = await fetchDecimals(best.address, chainId, chain);
|
|
3115
|
+
if (decimals === null) {
|
|
3116
|
+
process.stderr.write(
|
|
3117
|
+
`[tokens] Could not fetch decimals for ${best.symbol} (${best.address}) on chain ${chainId} \u2014 refusing to resolve
|
|
3118
|
+
`
|
|
3119
|
+
);
|
|
3120
|
+
return null;
|
|
3121
|
+
}
|
|
3122
|
+
return {
|
|
3123
|
+
address: best.address,
|
|
3124
|
+
decimals,
|
|
3125
|
+
name: best.name,
|
|
3126
|
+
symbol: best.symbol,
|
|
3127
|
+
chainId,
|
|
3128
|
+
source: "dexscreener"
|
|
3129
|
+
};
|
|
3130
|
+
} catch (e3) {
|
|
3131
|
+
process.stderr.write(`[tokens] DexScreener fallback failed: ${e3}
|
|
3132
|
+
`);
|
|
3133
|
+
return null;
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
async function fetchDecimals(tokenAddress, _chainId, chain) {
|
|
3137
|
+
const rpcUrl = chain?.rpcUrls?.default?.http?.[0];
|
|
3138
|
+
if (!rpcUrl) return null;
|
|
3139
|
+
try {
|
|
3140
|
+
const response = await fetch(rpcUrl, {
|
|
3141
|
+
method: "POST",
|
|
3142
|
+
headers: { "Content-Type": "application/json" },
|
|
3143
|
+
body: JSON.stringify({
|
|
3144
|
+
jsonrpc: "2.0",
|
|
3145
|
+
id: 1,
|
|
3146
|
+
method: "eth_call",
|
|
3147
|
+
params: [{ to: tokenAddress, data: "0x313ce567" }, "latest"]
|
|
3148
|
+
})
|
|
3149
|
+
});
|
|
3150
|
+
if (!response.ok) return null;
|
|
3151
|
+
const result = await response.json();
|
|
3152
|
+
if (result.result && result.result !== "0x") {
|
|
3153
|
+
const decimals = Number.parseInt(result.result, 16);
|
|
3154
|
+
if (decimals < 0 || decimals > 77 || !Number.isFinite(decimals)) {
|
|
3155
|
+
return null;
|
|
3156
|
+
}
|
|
3157
|
+
return decimals;
|
|
3158
|
+
}
|
|
3159
|
+
} catch {
|
|
3160
|
+
}
|
|
3161
|
+
return null;
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
// src/tools/tokens/schemas.ts
|
|
3165
|
+
import { z as z4 } from "zod";
|
|
3166
|
+
var resolveTokenSchema = z4.object({
|
|
3167
|
+
symbol: z4.string({ required_error: "symbol is required" }),
|
|
3168
|
+
chainId: z4.number({ required_error: "chainId is required" })
|
|
3169
|
+
});
|
|
3170
|
+
var listChainTokensSchema = z4.object({
|
|
3171
|
+
chainId: z4.number({ required_error: "chainId is required" })
|
|
3172
|
+
});
|
|
3173
|
+
|
|
3174
|
+
// src/tools/tokens/index.ts
|
|
3175
|
+
async function handleResolveToken(params) {
|
|
3176
|
+
const v = validateInput(resolveTokenSchema, params);
|
|
3177
|
+
if (!v.success) return v.error;
|
|
3178
|
+
const { symbol, chainId } = v.data;
|
|
3179
|
+
const chain = getChainById(chainId);
|
|
3180
|
+
if (!chain) {
|
|
3181
|
+
return formatToolError("UNKNOWN_CHAIN", `Chain ${chainId} is not a known EVM chain`);
|
|
3182
|
+
}
|
|
3183
|
+
const result = await resolveToken(symbol, chainId);
|
|
3184
|
+
if (!result) {
|
|
3185
|
+
const available = listTokens(chainId);
|
|
3186
|
+
const knownSymbols = available.map((t2) => t2.symbol).join(", ");
|
|
3187
|
+
return formatToolError(
|
|
3188
|
+
"TOKEN_NOT_FOUND",
|
|
3189
|
+
`Token '${symbol}' not found on ${chain.name} (${chainId}). Known tokens: ${knownSymbols || "none"}`
|
|
3190
|
+
);
|
|
3191
|
+
}
|
|
3192
|
+
return formatToolResponse(result);
|
|
3193
|
+
}
|
|
3194
|
+
function handleListTokens(params) {
|
|
3195
|
+
const v = validateInput(listChainTokensSchema, params);
|
|
3196
|
+
if (!v.success) return v.error;
|
|
3197
|
+
const { chainId } = v.data;
|
|
3198
|
+
const chain = getChainById(chainId);
|
|
3199
|
+
if (!chain) {
|
|
3200
|
+
return formatToolError("UNKNOWN_CHAIN", `Chain ${chainId} is not a known EVM chain`);
|
|
3201
|
+
}
|
|
3202
|
+
const tokens = listTokens(chainId);
|
|
3203
|
+
return formatToolResponse({
|
|
3204
|
+
chainId,
|
|
3205
|
+
chainName: chain.name,
|
|
3206
|
+
tokens
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3209
|
+
function getTokenToolDefinitions() {
|
|
3210
|
+
return [
|
|
3211
|
+
{
|
|
3212
|
+
name: "resolve_token",
|
|
3213
|
+
description: "Resolve a token symbol (e.g. 'USDT', 'WBNB') to its contract address and decimals on a specific chain. Uses a built-in registry of verified canonical addresses for major tokens with DexScreener fallback. ALWAYS call this first when you need a token address \u2014 do NOT use blockscout_lookup_token_by_symbol or get_token_info_by_symbol.",
|
|
3214
|
+
inputSchema: {
|
|
3215
|
+
type: "object",
|
|
3216
|
+
properties: {
|
|
3217
|
+
symbol: {
|
|
3218
|
+
type: "string",
|
|
3219
|
+
description: "Token symbol (e.g. 'USDT', 'USDC', 'WETH', 'WBNB', 'DAI')"
|
|
3220
|
+
},
|
|
3221
|
+
chainId: {
|
|
3222
|
+
type: "number",
|
|
3223
|
+
description: "EVM chain ID (e.g. 1=Ethereum, 56=BSC, 137=Polygon, 8453=Base, 59144=Linea, 42161=Arbitrum, 10=Optimism)"
|
|
3224
|
+
}
|
|
3225
|
+
},
|
|
3226
|
+
required: ["symbol", "chainId"]
|
|
3227
|
+
},
|
|
3228
|
+
handler: (params) => handleResolveToken(params)
|
|
3229
|
+
},
|
|
3230
|
+
{
|
|
3231
|
+
name: "list_chain_tokens",
|
|
3232
|
+
description: "List all well-known tokens available in the built-in registry for a specific chain. Returns symbol, address, decimals, and name for each token.",
|
|
3233
|
+
inputSchema: {
|
|
3234
|
+
type: "object",
|
|
3235
|
+
properties: {
|
|
3236
|
+
chainId: {
|
|
3237
|
+
type: "number",
|
|
3238
|
+
description: "EVM chain ID"
|
|
3239
|
+
}
|
|
3240
|
+
},
|
|
3241
|
+
required: ["chainId"]
|
|
3242
|
+
},
|
|
3243
|
+
handler: (params) => Promise.resolve(handleListTokens(params))
|
|
3244
|
+
}
|
|
3245
|
+
];
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3248
|
+
// src/upstream/remote-mcp-adapter.ts
|
|
3249
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3250
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
3251
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3252
|
+
var RemoteMcpAdapter = class {
|
|
3253
|
+
name;
|
|
3254
|
+
client;
|
|
3255
|
+
tools = [];
|
|
3256
|
+
routeMap = /* @__PURE__ */ new Map();
|
|
3257
|
+
health;
|
|
3258
|
+
prefix;
|
|
3259
|
+
url;
|
|
3260
|
+
constructor(config) {
|
|
3261
|
+
this.name = config.name;
|
|
3262
|
+
this.prefix = config.prefix;
|
|
3263
|
+
this.url = config.url;
|
|
3264
|
+
this.client = new Client({ name: "web3agent", version: VERSION });
|
|
3265
|
+
this.health = {
|
|
3266
|
+
name: config.name,
|
|
3267
|
+
status: config.initialStatus ?? "unavailable",
|
|
3268
|
+
message: config.initialMessage ?? "Not initialized"
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
shouldSkipInit() {
|
|
3272
|
+
return false;
|
|
3273
|
+
}
|
|
3274
|
+
getTransportOptions() {
|
|
3275
|
+
return void 0;
|
|
3276
|
+
}
|
|
3277
|
+
async postConnect() {
|
|
3278
|
+
}
|
|
3279
|
+
filterTools(tools) {
|
|
3280
|
+
return tools;
|
|
3281
|
+
}
|
|
3282
|
+
transformDescription(description) {
|
|
3283
|
+
return description;
|
|
3284
|
+
}
|
|
3285
|
+
async initialize() {
|
|
3286
|
+
if (this.shouldSkipInit()) return;
|
|
3287
|
+
const endpoint = new URL(this.url);
|
|
3288
|
+
const requestInit = this.getTransportOptions();
|
|
3289
|
+
let connected = false;
|
|
3290
|
+
for (const TransportClass of [StreamableHTTPClientTransport, SSEClientTransport]) {
|
|
3291
|
+
try {
|
|
3292
|
+
const transport = requestInit ? new TransportClass(endpoint, { requestInit }) : new TransportClass(endpoint);
|
|
3293
|
+
await this.client.connect(transport);
|
|
3294
|
+
connected = true;
|
|
3295
|
+
break;
|
|
3296
|
+
} catch (e3) {
|
|
3297
|
+
process.stderr.write(`[${this.name}] ${TransportClass.name} transport failed: ${e3}
|
|
3298
|
+
`);
|
|
3299
|
+
try {
|
|
3300
|
+
await this.client.close();
|
|
3301
|
+
} catch {
|
|
3302
|
+
}
|
|
3303
|
+
this.client = new Client({ name: "web3agent", version: VERSION });
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
if (!connected) {
|
|
3307
|
+
this.health = {
|
|
3308
|
+
name: this.name,
|
|
3309
|
+
status: "degraded",
|
|
3310
|
+
message: `Failed to connect to ${this.url} via StreamableHTTP and SSE`
|
|
3311
|
+
};
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
3314
|
+
try {
|
|
3315
|
+
await this.postConnect();
|
|
3316
|
+
const { tools: rawTools } = await this.client.listTools();
|
|
3317
|
+
const filtered = this.filterTools(rawTools);
|
|
3318
|
+
this.tools = filtered.map((t2) => {
|
|
3319
|
+
const prefixedName = `${this.prefix}_${t2.name}`;
|
|
3320
|
+
this.routeMap.set(prefixedName, t2.name);
|
|
3321
|
+
return {
|
|
3322
|
+
...t2,
|
|
3323
|
+
name: prefixedName,
|
|
3324
|
+
description: this.transformDescription(t2.description ?? ""),
|
|
3325
|
+
upstreamName: t2.name,
|
|
3326
|
+
prefix: this.prefix
|
|
3327
|
+
};
|
|
3328
|
+
});
|
|
3329
|
+
this.health = {
|
|
3330
|
+
name: this.name,
|
|
3331
|
+
status: "ok",
|
|
3332
|
+
message: `Connected with ${this.tools.length} tools`,
|
|
3333
|
+
toolCount: this.tools.length
|
|
3334
|
+
};
|
|
3335
|
+
} catch (err) {
|
|
3336
|
+
const msg = err instanceof Error ? err.message : "Unknown post-connect error";
|
|
3337
|
+
this.health = {
|
|
3338
|
+
name: this.name,
|
|
3339
|
+
status: "degraded",
|
|
3340
|
+
message: `Connected but post-connect/listTools failed: ${msg}`
|
|
3341
|
+
};
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
getTools() {
|
|
3345
|
+
return this.tools;
|
|
3346
|
+
}
|
|
3347
|
+
async callTool(name, args) {
|
|
3348
|
+
const originalName = this.routeMap.get(name);
|
|
3349
|
+
if (!originalName) {
|
|
3350
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
3351
|
+
}
|
|
3352
|
+
return this.client.callTool({ name: originalName, arguments: args });
|
|
3353
|
+
}
|
|
3354
|
+
getHealth() {
|
|
3355
|
+
return this.health;
|
|
3356
|
+
}
|
|
3357
|
+
async shutdown() {
|
|
3358
|
+
try {
|
|
3359
|
+
await this.client.close();
|
|
3360
|
+
} catch (e3) {
|
|
3361
|
+
process.stderr.write(`[${this.name}] Failed to close client during shutdown: ${e3}
|
|
3362
|
+
`);
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
};
|
|
3366
|
+
|
|
3367
|
+
// src/upstream/blockscout/adapter.ts
|
|
3368
|
+
var BOOTSTRAP_TOOL = "__unlock_blockchain_analysis__";
|
|
3369
|
+
var CHAIN_SUPPORT_NOTE = " NOTE: Blockscout hosted instances do NOT support all chains. Supported: Ethereum (1), Polygon (137), Arbitrum (42161), Optimism (10), Base (8453), Gnosis (100), Scroll (534352), zkSync Era (324). NOT supported: BSC (56), Linea (59144), Avalanche (43114), Blast (81457), Mantle (5000), Mode (34443). For token lookups, prefer resolve_token tool instead.";
|
|
3370
|
+
var BlockscoutAdapter = class extends RemoteMcpAdapter {
|
|
3371
|
+
constructor(url = BLOCKSCOUT_DEFAULT_URL) {
|
|
3372
|
+
super({ name: "blockscout", prefix: "blockscout", url });
|
|
3373
|
+
}
|
|
3374
|
+
async postConnect() {
|
|
3375
|
+
await this.client.callTool({ name: BOOTSTRAP_TOOL, arguments: {} });
|
|
3376
|
+
}
|
|
3377
|
+
filterTools(tools) {
|
|
3378
|
+
return tools.filter((t2) => t2.name !== BOOTSTRAP_TOOL);
|
|
3379
|
+
}
|
|
3380
|
+
transformDescription(desc) {
|
|
3381
|
+
return desc + CHAIN_SUPPORT_NOTE;
|
|
3382
|
+
}
|
|
3383
|
+
};
|
|
3384
|
+
|
|
3385
|
+
// src/upstream/etherscan/adapter.ts
|
|
3386
|
+
var EtherscanAdapter = class extends RemoteMcpAdapter {
|
|
3387
|
+
apiKey;
|
|
3388
|
+
constructor(url = ETHERSCAN_DEFAULT_URL, apiKey) {
|
|
3389
|
+
super({
|
|
3390
|
+
name: "etherscan",
|
|
3391
|
+
prefix: "etherscan",
|
|
3392
|
+
url,
|
|
3393
|
+
initialStatus: apiKey ? "unavailable" : "not_configured",
|
|
3394
|
+
initialMessage: apiKey ? "Not initialized" : "No API key provided (ETHERSCAN_API_KEY)"
|
|
3395
|
+
});
|
|
3396
|
+
this.apiKey = apiKey;
|
|
3397
|
+
}
|
|
3398
|
+
shouldSkipInit() {
|
|
3399
|
+
return !this.apiKey;
|
|
3400
|
+
}
|
|
3401
|
+
getTransportOptions() {
|
|
3402
|
+
if (!this.apiKey) return void 0;
|
|
3403
|
+
return { headers: { Authorization: `Bearer ${this.apiKey}` } };
|
|
3404
|
+
}
|
|
3405
|
+
};
|
|
3406
|
+
|
|
3407
|
+
// src/upstream/evm/adapter.ts
|
|
3408
|
+
import { Client as Client2 } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3409
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3410
|
+
var PREFIX = "evm";
|
|
3411
|
+
async function buildEvmEnv() {
|
|
3412
|
+
const env = {
|
|
3413
|
+
PATH: process.env.PATH ?? "",
|
|
3414
|
+
HOME: process.env.HOME ?? "",
|
|
3415
|
+
TMPDIR: process.env.TMPDIR ?? "/tmp"
|
|
3416
|
+
};
|
|
3417
|
+
if (process.env.TERM) env.TERM = process.env.TERM;
|
|
3418
|
+
if (process.env.NODE_ENV) env.NODE_ENV = process.env.NODE_ENV;
|
|
3419
|
+
const walletKey = await getPersistedKeyForSubprocess();
|
|
3420
|
+
if (walletKey) env.EVM_PRIVATE_KEY = walletKey;
|
|
3421
|
+
if (process.env.WALLET_ACCOUNT_INDEX) env.EVM_ACCOUNT_INDEX = process.env.WALLET_ACCOUNT_INDEX;
|
|
3422
|
+
if (process.env.ETHERSCAN_API_KEY) env.ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY;
|
|
3423
|
+
return env;
|
|
3424
|
+
}
|
|
3425
|
+
var EvmAdapter = class {
|
|
3426
|
+
name = "evm";
|
|
3427
|
+
client;
|
|
3428
|
+
transport;
|
|
3429
|
+
tools = [];
|
|
3430
|
+
routeMap = /* @__PURE__ */ new Map();
|
|
3431
|
+
health;
|
|
3432
|
+
startedAt;
|
|
3433
|
+
restartCount = 0;
|
|
3434
|
+
restarting = false;
|
|
3435
|
+
walletChangeHandler;
|
|
3436
|
+
constructor() {
|
|
3437
|
+
this.health = {
|
|
3438
|
+
name: "evm",
|
|
3439
|
+
status: "unavailable",
|
|
3440
|
+
message: "Not initialized"
|
|
3441
|
+
};
|
|
3442
|
+
}
|
|
3443
|
+
async initialize() {
|
|
3444
|
+
await this.start();
|
|
3445
|
+
this.walletChangeHandler = async (_state) => {
|
|
3446
|
+
try {
|
|
3447
|
+
await this.restart();
|
|
3448
|
+
} catch (e3) {
|
|
3449
|
+
process.stderr.write(`[evm] Restart after wallet change failed: ${e3}
|
|
3450
|
+
`);
|
|
3451
|
+
}
|
|
3452
|
+
};
|
|
3453
|
+
walletEvents.on("wallet-changed", this.walletChangeHandler);
|
|
3454
|
+
}
|
|
3455
|
+
async start() {
|
|
3456
|
+
try {
|
|
3457
|
+
const whitelistedEnv = await buildEvmEnv();
|
|
3458
|
+
this.transport = new StdioClientTransport({
|
|
3459
|
+
command: "npx",
|
|
3460
|
+
args: ["-y", "@mcpdotdirect/evm-mcp-server"],
|
|
3461
|
+
env: whitelistedEnv
|
|
3462
|
+
});
|
|
3463
|
+
this.client = new Client2({
|
|
3464
|
+
name: "web3agent-evm",
|
|
3465
|
+
version: VERSION
|
|
3466
|
+
});
|
|
3467
|
+
await this.client.connect(this.transport);
|
|
3468
|
+
this.startedAt = /* @__PURE__ */ new Date();
|
|
3469
|
+
const result = await this.client.listTools();
|
|
3470
|
+
this.tools = [];
|
|
3471
|
+
this.routeMap.clear();
|
|
3472
|
+
for (const tool of result.tools) {
|
|
3473
|
+
const prefixed = `${PREFIX}_${tool.name}`;
|
|
3474
|
+
this.routeMap.set(prefixed, tool.name);
|
|
3475
|
+
this.tools.push({
|
|
3476
|
+
...tool,
|
|
3477
|
+
name: prefixed,
|
|
3478
|
+
upstreamName: tool.name,
|
|
3479
|
+
prefix: PREFIX
|
|
3480
|
+
});
|
|
3481
|
+
}
|
|
3482
|
+
const pid = this.transport.process?.pid ?? void 0;
|
|
3483
|
+
this.health = {
|
|
3484
|
+
name: "evm",
|
|
3485
|
+
status: "ok",
|
|
3486
|
+
message: `Running with ${this.tools.length} tools`,
|
|
3487
|
+
toolCount: this.tools.length,
|
|
3488
|
+
restartCount: this.restartCount,
|
|
3489
|
+
pid,
|
|
3490
|
+
uptimeMs: 0
|
|
3491
|
+
};
|
|
3492
|
+
process.stderr.write(
|
|
3493
|
+
`[evm] Started with ${this.tools.length} tools (pid=${pid ?? "unknown"})
|
|
3494
|
+
`
|
|
3495
|
+
);
|
|
3496
|
+
} catch (err) {
|
|
3497
|
+
const message = err instanceof Error ? err.message : "Unknown startup error";
|
|
3498
|
+
process.stderr.write(`[evm] Failed to start: ${message}
|
|
3499
|
+
`);
|
|
3500
|
+
this.tools = [];
|
|
3501
|
+
this.routeMap.clear();
|
|
3502
|
+
this.health = {
|
|
3503
|
+
name: "evm",
|
|
3504
|
+
status: "degraded",
|
|
3505
|
+
message: `Startup failed: ${message}`,
|
|
3506
|
+
toolCount: 0,
|
|
3507
|
+
restartCount: this.restartCount
|
|
3508
|
+
};
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
async restart() {
|
|
3512
|
+
if (this.restarting) return;
|
|
3513
|
+
this.restarting = true;
|
|
3514
|
+
try {
|
|
3515
|
+
process.stderr.write("[evm] Restarting due to wallet change...\n");
|
|
3516
|
+
try {
|
|
3517
|
+
await this.client?.close();
|
|
3518
|
+
} catch (e3) {
|
|
3519
|
+
process.stderr.write(`[evm] Failed to close client during restart: ${e3}
|
|
3520
|
+
`);
|
|
3521
|
+
}
|
|
3522
|
+
this.killSubprocess();
|
|
3523
|
+
this.restartCount++;
|
|
3524
|
+
await this.start();
|
|
3525
|
+
this.health.lastRestartAt = /* @__PURE__ */ new Date();
|
|
3526
|
+
this.health.restartCount = this.restartCount;
|
|
3527
|
+
} finally {
|
|
3528
|
+
this.restarting = false;
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
killSubprocess() {
|
|
3532
|
+
try {
|
|
3533
|
+
const proc = this.transport?.process;
|
|
3534
|
+
if (proc?.kill) {
|
|
3535
|
+
proc.kill();
|
|
3536
|
+
}
|
|
3537
|
+
} catch (e3) {
|
|
3538
|
+
process.stderr.write(`[evm] Failed to kill subprocess: ${e3}
|
|
3539
|
+
`);
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
getTools() {
|
|
3543
|
+
return this.tools;
|
|
3544
|
+
}
|
|
3545
|
+
async callTool(name, args) {
|
|
3546
|
+
const upstreamName = this.routeMap.get(name);
|
|
3547
|
+
if (!upstreamName) {
|
|
3548
|
+
throw new Error(`Unknown EVM tool: ${name}`);
|
|
3549
|
+
}
|
|
3550
|
+
if (this.restarting) {
|
|
3551
|
+
throw new Error("EVM adapter is restarting due to wallet change, please retry");
|
|
3552
|
+
}
|
|
3553
|
+
if (this.health.status !== "ok") {
|
|
3554
|
+
throw new Error(`EVM adapter is ${this.health.status}: ${this.health.message}`);
|
|
3555
|
+
}
|
|
3556
|
+
const result = await this.client.callTool({
|
|
3557
|
+
name: upstreamName,
|
|
3558
|
+
arguments: args
|
|
3559
|
+
});
|
|
3560
|
+
return result;
|
|
3561
|
+
}
|
|
3562
|
+
getHealth() {
|
|
3563
|
+
if (this.startedAt && this.health.status === "ok") {
|
|
3564
|
+
this.health.uptimeMs = Date.now() - this.startedAt.getTime();
|
|
3565
|
+
}
|
|
3566
|
+
return this.health;
|
|
3567
|
+
}
|
|
3568
|
+
async shutdown() {
|
|
3569
|
+
if (this.walletChangeHandler) {
|
|
3570
|
+
walletEvents.off("wallet-changed", this.walletChangeHandler);
|
|
3571
|
+
this.walletChangeHandler = void 0;
|
|
3572
|
+
}
|
|
3573
|
+
try {
|
|
3574
|
+
await this.client?.close();
|
|
3575
|
+
} catch (e3) {
|
|
3576
|
+
process.stderr.write(`[evm] Failed to close client during shutdown: ${e3}
|
|
3577
|
+
`);
|
|
3578
|
+
}
|
|
3579
|
+
this.killSubprocess();
|
|
3580
|
+
this.health = {
|
|
3581
|
+
name: "evm",
|
|
3582
|
+
status: "unavailable",
|
|
3583
|
+
message: "Shut down",
|
|
3584
|
+
toolCount: 0
|
|
3585
|
+
};
|
|
3586
|
+
process.stderr.write("[evm] Shut down\n");
|
|
3587
|
+
}
|
|
3588
|
+
};
|
|
3589
|
+
|
|
3590
|
+
// src/runtime/server.ts
|
|
3591
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3592
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3593
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
3594
|
+
function normalizeInputSchema(schema) {
|
|
3595
|
+
const candidate = schema;
|
|
3596
|
+
return {
|
|
3597
|
+
...candidate,
|
|
3598
|
+
type: "object"
|
|
3599
|
+
};
|
|
3600
|
+
}
|
|
3601
|
+
var ProxyServer = class {
|
|
3602
|
+
constructor(blockscoutAdapter, etherscanAdapter, evmAdapter, goatProvider2) {
|
|
3603
|
+
this.blockscoutAdapter = blockscoutAdapter;
|
|
3604
|
+
this.etherscanAdapter = etherscanAdapter;
|
|
3605
|
+
this.evmAdapter = evmAdapter;
|
|
3606
|
+
this.goatProvider = goatProvider2;
|
|
3607
|
+
this.server = new Server(
|
|
3608
|
+
{ name: "web3agent", version: VERSION },
|
|
3609
|
+
{ capabilities: { tools: {} } }
|
|
3610
|
+
);
|
|
3611
|
+
this.frameworkTools = [
|
|
3612
|
+
...getWalletToolDefinitions(),
|
|
3613
|
+
...getTransactionToolDefinitions(),
|
|
3614
|
+
...getUtilityToolDefinitions()
|
|
3615
|
+
];
|
|
3616
|
+
this.lifiTools = getLifiToolDefinitions();
|
|
3617
|
+
this.orbsTools = getOrbsToolDefinitions();
|
|
3618
|
+
this.tokenTools = getTokenToolDefinitions();
|
|
3619
|
+
this.goatToolNames = new Set(this.goatProvider.getAllToolNames());
|
|
3620
|
+
this.rebuildDispatchMap();
|
|
3621
|
+
this.registerHandlers();
|
|
3622
|
+
this.registerWalletChangeNotification();
|
|
3623
|
+
}
|
|
3624
|
+
server;
|
|
3625
|
+
frameworkTools;
|
|
3626
|
+
lifiTools;
|
|
3627
|
+
orbsTools;
|
|
3628
|
+
tokenTools;
|
|
3629
|
+
goatToolNames;
|
|
3630
|
+
toolDispatch = /* @__PURE__ */ new Map();
|
|
3631
|
+
walletChangeHandler;
|
|
3632
|
+
rebuildDispatchMap() {
|
|
3633
|
+
this.toolDispatch.clear();
|
|
3634
|
+
for (const tool of this.blockscoutAdapter.getTools()) {
|
|
3635
|
+
this.toolDispatch.set(
|
|
3636
|
+
tool.name,
|
|
3637
|
+
(args) => this.blockscoutAdapter.callTool(tool.name, args)
|
|
3638
|
+
);
|
|
3639
|
+
}
|
|
3640
|
+
for (const tool of this.etherscanAdapter.getTools()) {
|
|
3641
|
+
this.toolDispatch.set(
|
|
3642
|
+
tool.name,
|
|
3643
|
+
(args) => this.etherscanAdapter.callTool(tool.name, args)
|
|
3644
|
+
);
|
|
3645
|
+
}
|
|
3646
|
+
for (const tool of this.evmAdapter.getTools()) {
|
|
3647
|
+
this.toolDispatch.set(
|
|
3648
|
+
tool.name,
|
|
3649
|
+
(args) => this.evmAdapter.callTool(tool.name, args)
|
|
3650
|
+
);
|
|
3651
|
+
}
|
|
3652
|
+
for (const tool of this.tokenTools) {
|
|
3653
|
+
this.toolDispatch.set(tool.name, (args) => tool.handler(args));
|
|
3654
|
+
}
|
|
3655
|
+
for (const name of this.goatToolNames) {
|
|
3656
|
+
this.toolDispatch.set(name, (args) => dispatchGoatTool(name, args));
|
|
3657
|
+
}
|
|
3658
|
+
for (const tool of this.lifiTools) {
|
|
3659
|
+
this.toolDispatch.set(tool.name, (args) => tool.handler(args));
|
|
3660
|
+
}
|
|
3661
|
+
for (const tool of this.orbsTools) {
|
|
3662
|
+
this.toolDispatch.set(tool.name, (args) => tool.handler(args));
|
|
3663
|
+
}
|
|
3664
|
+
for (const tool of this.frameworkTools) {
|
|
3665
|
+
this.toolDispatch.set(tool.name, (args) => tool.handler(args));
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
registerHandlers() {
|
|
3669
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
3670
|
+
tools: this.getAggregatedTools()
|
|
3671
|
+
}));
|
|
3672
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
3673
|
+
const name = request.params.name;
|
|
3674
|
+
const args = request.params.arguments ?? {};
|
|
3675
|
+
const handler = this.toolDispatch.get(name);
|
|
3676
|
+
if (handler) {
|
|
3677
|
+
return handler(args);
|
|
3678
|
+
}
|
|
3679
|
+
return formatToolError("UNKNOWN_TOOL", `Unknown tool: ${name}`);
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3682
|
+
getGoatTools() {
|
|
3683
|
+
const snapshot = this.goatProvider.getReferenceSnapshot();
|
|
3684
|
+
if (!snapshot) return [];
|
|
3685
|
+
return snapshot.listOfTools.map((tool) => {
|
|
3686
|
+
const schema = normalizeInputSchema(tool.inputSchema);
|
|
3687
|
+
const properties = schema.properties ?? {};
|
|
3688
|
+
properties.chainId = {
|
|
3689
|
+
type: "number",
|
|
3690
|
+
description: "Optional EVM chain ID to run this tool on (e.g. 1 for Ethereum, 8453 for Base, 9745 for Plasma). Defaults to the active wallet chain."
|
|
3691
|
+
};
|
|
3692
|
+
let description = tool.description;
|
|
3693
|
+
const lowerName = tool.name.toLowerCase();
|
|
3694
|
+
for (const [plugin, chains] of Object.entries(RESTRICTED_PLUGIN_CHAINS)) {
|
|
3695
|
+
if (lowerName.startsWith(plugin)) {
|
|
3696
|
+
description += ` Only available on chains: ${chains.join(", ")}.`;
|
|
3697
|
+
break;
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
return {
|
|
3701
|
+
name: tool.name,
|
|
3702
|
+
description,
|
|
3703
|
+
inputSchema: { ...schema, properties }
|
|
3704
|
+
};
|
|
3705
|
+
});
|
|
3706
|
+
}
|
|
3707
|
+
getAggregatedTools() {
|
|
3708
|
+
return [
|
|
3709
|
+
...this.frameworkTools.map((tool) => ({
|
|
3710
|
+
name: tool.name,
|
|
3711
|
+
description: tool.description,
|
|
3712
|
+
inputSchema: normalizeInputSchema(tool.inputSchema)
|
|
3713
|
+
})),
|
|
3714
|
+
...this.getGoatTools(),
|
|
3715
|
+
...this.blockscoutAdapter.getTools(),
|
|
3716
|
+
...this.etherscanAdapter.getTools(),
|
|
3717
|
+
...this.evmAdapter.getTools(),
|
|
3718
|
+
...this.lifiTools.map((tool) => ({
|
|
3719
|
+
name: tool.name,
|
|
3720
|
+
description: tool.description,
|
|
3721
|
+
inputSchema: normalizeInputSchema(tool.inputSchema)
|
|
3722
|
+
})),
|
|
3723
|
+
...this.orbsTools.map((tool) => ({
|
|
3724
|
+
name: tool.name,
|
|
3725
|
+
description: tool.description,
|
|
3726
|
+
inputSchema: normalizeInputSchema(tool.inputSchema)
|
|
3727
|
+
})),
|
|
3728
|
+
...this.tokenTools.map((tool) => ({
|
|
3729
|
+
name: tool.name,
|
|
3730
|
+
description: tool.description,
|
|
3731
|
+
inputSchema: normalizeInputSchema(tool.inputSchema)
|
|
3732
|
+
}))
|
|
3733
|
+
];
|
|
3734
|
+
}
|
|
3735
|
+
registerWalletChangeNotification() {
|
|
3736
|
+
this.walletChangeHandler = () => {
|
|
3737
|
+
this.goatProvider.waitForRebuild().then(() => {
|
|
3738
|
+
this.goatToolNames = new Set(this.goatProvider.getAllToolNames());
|
|
3739
|
+
this.rebuildDispatchMap();
|
|
3740
|
+
return this.server.notification({ method: "notifications/tools/list_changed" });
|
|
3741
|
+
}).catch((e3) => {
|
|
3742
|
+
process.stderr.write(`[web3agent] Failed to update tools after wallet change: ${e3}
|
|
3743
|
+
`);
|
|
3744
|
+
});
|
|
3745
|
+
};
|
|
3746
|
+
walletEvents.on("wallet-changed", this.walletChangeHandler);
|
|
3747
|
+
}
|
|
3748
|
+
async shutdown() {
|
|
3749
|
+
if (this.walletChangeHandler) {
|
|
3750
|
+
walletEvents.off("wallet-changed", this.walletChangeHandler);
|
|
3751
|
+
this.walletChangeHandler = void 0;
|
|
3752
|
+
}
|
|
3753
|
+
this.goatProvider.shutdown();
|
|
3754
|
+
await this.goatProvider.waitForRebuild().catch(() => {
|
|
3755
|
+
});
|
|
3756
|
+
await Promise.allSettled([
|
|
3757
|
+
this.blockscoutAdapter.shutdown(),
|
|
3758
|
+
this.etherscanAdapter.shutdown(),
|
|
3759
|
+
this.evmAdapter.shutdown()
|
|
3760
|
+
]);
|
|
3761
|
+
await this.server.close().catch((e3) => {
|
|
3762
|
+
process.stderr.write(`[web3agent] Failed to close MCP server: ${e3}
|
|
3763
|
+
`);
|
|
3764
|
+
});
|
|
3765
|
+
}
|
|
3766
|
+
async connect(transport) {
|
|
3767
|
+
await this.server.connect(transport);
|
|
3768
|
+
}
|
|
3769
|
+
async start() {
|
|
3770
|
+
const transport = new StdioServerTransport();
|
|
3771
|
+
await this.connect(transport);
|
|
3772
|
+
}
|
|
3773
|
+
};
|
|
3774
|
+
|
|
3775
|
+
// src/runtime/startup.ts
|
|
3776
|
+
async function startServer() {
|
|
3777
|
+
let config;
|
|
3778
|
+
try {
|
|
3779
|
+
config = parseEnv(process.env);
|
|
3780
|
+
setConfig(config);
|
|
3781
|
+
} catch (error) {
|
|
3782
|
+
if (error instanceof ValidationError) {
|
|
3783
|
+
process.stderr.write(`[web3agent] Invalid config ${error.field}: ${error.message}
|
|
3784
|
+
`);
|
|
3785
|
+
process.exit(1);
|
|
3786
|
+
}
|
|
3787
|
+
throw error;
|
|
3788
|
+
}
|
|
3789
|
+
confirmationQueue.enabled = config.confirmWrites;
|
|
3790
|
+
await initializeWallet({
|
|
3791
|
+
chainId: config.chainId,
|
|
3792
|
+
accountIndex: config.walletAccountIndex,
|
|
3793
|
+
addressIndex: config.walletAddressIndex,
|
|
3794
|
+
privateKey: config.privateKey,
|
|
3795
|
+
mnemonic: config.mnemonic
|
|
3796
|
+
});
|
|
3797
|
+
walletEvents.on("wallet-changed", () => {
|
|
3798
|
+
const flushed = confirmationQueue.flushAll();
|
|
3799
|
+
if (flushed > 0) {
|
|
3800
|
+
process.stderr.write(
|
|
3801
|
+
`[web3agent] Wallet changed \u2014 flushed ${flushed} pending operation(s) from confirmation queue
|
|
3802
|
+
`
|
|
3803
|
+
);
|
|
3804
|
+
}
|
|
3805
|
+
});
|
|
3806
|
+
registerOrbsExecutors();
|
|
3807
|
+
registerLifiExecutors();
|
|
3808
|
+
await confirmationQueue.loadQueue();
|
|
3809
|
+
const blockscoutAdapter = new BlockscoutAdapter(config.blockscoutMcpUrl);
|
|
3810
|
+
const etherscanAdapter = new EtherscanAdapter(config.etherscanMcpUrl, config.etherscanApiKey);
|
|
3811
|
+
const evmAdapter = new EvmAdapter();
|
|
3812
|
+
const health = createDefaultHealthStatus();
|
|
3813
|
+
try {
|
|
3814
|
+
await blockscoutAdapter.initialize();
|
|
3815
|
+
} catch (error) {
|
|
3816
|
+
const message = error instanceof Error ? error.message : "Unknown initialization error";
|
|
3817
|
+
health.blockscout.status = "degraded";
|
|
3818
|
+
health.blockscout.message = message;
|
|
3819
|
+
process.stderr.write(`[web3agent] Blockscout degraded: ${message}
|
|
3820
|
+
`);
|
|
3821
|
+
}
|
|
3822
|
+
try {
|
|
3823
|
+
await etherscanAdapter.initialize();
|
|
3824
|
+
} catch (error) {
|
|
3825
|
+
const message = error instanceof Error ? error.message : "Unknown initialization error";
|
|
3826
|
+
health.etherscan.status = "degraded";
|
|
3827
|
+
health.etherscan.message = message;
|
|
3828
|
+
process.stderr.write(`[web3agent] Etherscan degraded: ${message}
|
|
3829
|
+
`);
|
|
3830
|
+
}
|
|
3831
|
+
try {
|
|
3832
|
+
await evmAdapter.initialize();
|
|
3833
|
+
} catch (error) {
|
|
3834
|
+
const message = error instanceof Error ? error.message : "Unknown initialization error";
|
|
3835
|
+
health.evm.status = "degraded";
|
|
3836
|
+
health.evm.message = message;
|
|
3837
|
+
process.stderr.write(`[web3agent] EVM degraded: ${message}
|
|
3838
|
+
`);
|
|
3839
|
+
}
|
|
3840
|
+
await goatProvider.initialize({
|
|
3841
|
+
zeroxApiKey: config.zeroxApiKey,
|
|
3842
|
+
coingeckoApiKey: config.coingeckoApiKey,
|
|
3843
|
+
rpcUrl: config.rpcUrl
|
|
3844
|
+
});
|
|
3845
|
+
initializeLifi(config.lifiApiKey);
|
|
3846
|
+
const server = new ProxyServer(blockscoutAdapter, etherscanAdapter, evmAdapter, goatProvider);
|
|
3847
|
+
const walletMode = getWalletState().mode;
|
|
3848
|
+
const goatToolCount = goatProvider.getAllToolNames().length;
|
|
3849
|
+
const blockscoutToolCount = blockscoutAdapter.getTools().length;
|
|
3850
|
+
const etherscanToolCount = etherscanAdapter.getTools().length;
|
|
3851
|
+
const evmToolCount = evmAdapter.getTools().length;
|
|
3852
|
+
const lifiToolCount = getLifiToolDefinitions().length;
|
|
3853
|
+
const orbsToolCount = getOrbsToolDefinitions().length;
|
|
3854
|
+
const tokenToolCount = getTokenToolDefinitions().length;
|
|
3855
|
+
const frameworkToolCount = getWalletToolDefinitions().length + getTransactionToolDefinitions().length + getUtilityToolDefinitions().length;
|
|
3856
|
+
health.blockscout = blockscoutAdapter.getHealth();
|
|
3857
|
+
health.etherscan = etherscanAdapter.getHealth();
|
|
3858
|
+
health.evm = evmAdapter.getHealth();
|
|
3859
|
+
health.goat = {
|
|
3860
|
+
name: "goat",
|
|
3861
|
+
status: "ok",
|
|
3862
|
+
toolCount: goatToolCount,
|
|
3863
|
+
message: `Loaded ${goatToolCount} tools`
|
|
3864
|
+
};
|
|
3865
|
+
health.lifi = {
|
|
3866
|
+
name: "lifi",
|
|
3867
|
+
status: "ok",
|
|
3868
|
+
toolCount: lifiToolCount,
|
|
3869
|
+
message: `Loaded ${lifiToolCount} tools`
|
|
3870
|
+
};
|
|
3871
|
+
health.orbs = {
|
|
3872
|
+
name: "orbs",
|
|
3873
|
+
status: "ok",
|
|
3874
|
+
toolCount: orbsToolCount,
|
|
3875
|
+
message: `Loaded ${orbsToolCount} tools`
|
|
3876
|
+
};
|
|
3877
|
+
const totalToolCount = frameworkToolCount + goatToolCount + blockscoutToolCount + etherscanToolCount + evmToolCount + lifiToolCount + orbsToolCount + tokenToolCount;
|
|
3878
|
+
setHealthStatus(health, totalToolCount);
|
|
3879
|
+
const degradedServices = [];
|
|
3880
|
+
if (health.blockscout.status !== "ok") degradedServices.push("blockscout");
|
|
3881
|
+
if (health.etherscan.status !== "ok" && health.etherscan.status !== "not_configured")
|
|
3882
|
+
degradedServices.push("etherscan");
|
|
3883
|
+
if (health.evm.status !== "ok") degradedServices.push("evm");
|
|
3884
|
+
const report = createStartupReport({
|
|
3885
|
+
health,
|
|
3886
|
+
activeChainId: config.chainId,
|
|
3887
|
+
walletMode,
|
|
3888
|
+
confirmWrites: config.confirmWrites,
|
|
3889
|
+
degradedServices,
|
|
3890
|
+
totalToolCount
|
|
3891
|
+
});
|
|
3892
|
+
process.stderr.write(`${formatHealthSummary(report)}
|
|
3893
|
+
`);
|
|
3894
|
+
process.stderr.write(
|
|
3895
|
+
`[web3agent] Tool counts => framework:${frameworkToolCount}, goat:${goatToolCount}, blockscout:${blockscoutToolCount}, etherscan:${etherscanToolCount}, evm:${evmToolCount}, lifi:${lifiToolCount}, orbs:${orbsToolCount}, tokens:${tokenToolCount}
|
|
3896
|
+
`
|
|
3897
|
+
);
|
|
3898
|
+
let shuttingDown = false;
|
|
3899
|
+
const gracefulShutdown = async () => {
|
|
3900
|
+
if (shuttingDown) return;
|
|
3901
|
+
shuttingDown = true;
|
|
3902
|
+
process.stderr.write("[web3agent] Shutting down...\n");
|
|
3903
|
+
await server.shutdown();
|
|
3904
|
+
process.exit(0);
|
|
3905
|
+
};
|
|
3906
|
+
process.on("SIGTERM", gracefulShutdown);
|
|
3907
|
+
process.on("SIGINT", gracefulShutdown);
|
|
3908
|
+
await server.start();
|
|
3909
|
+
}
|
|
3910
|
+
export {
|
|
3911
|
+
startServer
|
|
3912
|
+
};
|