Skip to content

Commit 4dd5a05

Browse files
committed
add tool to get contract info from etherscan
1 parent 0d6e1d5 commit 4dd5a05

File tree

5 files changed

+226
-49
lines changed

5 files changed

+226
-49
lines changed

README.md

+22
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,28 @@ Example query to Claude:
352352

353353
> "Show me the most recent transactions for address 0xc5102fE9359FD9a28f877a67E36B0F050d81a3CC."
354354
355+
### etherscan_contract_info
356+
357+
Gets detailed information about a smart contract using Etherscan API.
358+
359+
Parameters:
360+
361+
- `address`: The contract address to get information for
362+
- `chainId`: The chain ID (defaults to chain the wallet is connected to)
363+
364+
The tool returns the following information:
365+
- Contract name
366+
- Contract address
367+
- ABI
368+
- Contract creator address
369+
- Transaction hash where the contract was created
370+
- Creation timestamp
371+
- Current ETH balance of the contract
372+
373+
Example query to Claude:
374+
375+
> "Show me information about the contract at 0xc5102fE9359FD9a28f877a67E36B0F050d81a3CC."
376+
355377
## Security Considerations
356378

357379
- The configuration file contains sensitive information (API keys and seed phrases). Ensure it's properly secured and not shared.

src/tools/etherscan/handlers.ts

+152-41
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,100 @@
11
import type { PublicActions, WalletClient } from 'viem';
2+
import { formatGwei, formatUnits, isAddress } from 'viem';
3+
import { getBalance, getCode } from 'viem/actions';
24
import { base } from 'viem/chains';
3-
import { formatUnits, formatGwei } from 'viem';
45
import type { z } from 'zod';
5-
import type { GetAddressTransactionsSchema } from './schemas.js';
6+
import type {
7+
GetAddressTransactionsSchema,
8+
GetContractInfoSchema,
9+
} from './schemas.js';
610

711
// Etherscan API endpoint for all supported chains
812
const ETHERSCAN_API_URL = 'https://api.etherscan.io/v2/api';
913

1014
// Helper function to handle Etherscan API requests using V2 API
1115
async function makeEtherscanRequest(
1216
params: Record<string, string>,
13-
): Promise<any> {
17+
): Promise<Record<string, unknown>> {
1418
// Add API key if available
1519
const apiKey = process.env.ETHERSCAN_API_KEY;
1620
if (apiKey) {
1721
params.apikey = apiKey;
1822
} else {
1923
throw new Error('ETHERSCAN_API_KEY is not set');
2024
}
21-
25+
2226
// Build query string
2327
const queryParams = new URLSearchParams();
2428
Object.entries(params).forEach(([key, value]) => {
2529
queryParams.append(key, value);
2630
});
2731

2832
try {
29-
const response = await fetch(`${ETHERSCAN_API_URL}?${queryParams.toString()}`);
30-
33+
const response = await fetch(
34+
`${ETHERSCAN_API_URL}?${queryParams.toString()}`,
35+
);
36+
3137
if (!response.ok) {
3238
throw new Error(`HTTP error! Status: ${response.status}`);
3339
}
34-
40+
3541
const data = await response.json();
36-
42+
3743
// Handle Etherscan API errors
3844
if (data.status === '0' && data.message === 'NOTOK') {
3945
throw new Error(`Etherscan API error: ${data.result}`);
4046
}
41-
47+
4248
return data;
4349
} catch (error) {
44-
throw new Error(`Failed to fetch from Etherscan API: ${error instanceof Error ? error.message : String(error)}`);
50+
throw new Error(
51+
`Failed to fetch from Etherscan API: ${error instanceof Error ? error.message : String(error)}`,
52+
);
4553
}
4654
}
4755

4856
export async function getAddressTransactionsHandler(
4957
wallet: WalletClient & PublicActions,
5058
args: z.infer<typeof GetAddressTransactionsSchema>,
51-
): Promise<any> {
59+
): Promise<string> {
5260
// Get chain ID from args or wallet
5361
const chainId = args.chainId ?? wallet.chain?.id ?? base.id;
54-
62+
63+
// Validate address
64+
if (!isAddress(args.address, { strict: false })) {
65+
throw new Error(`Invalid address: ${args.address}`);
66+
}
67+
5568
// Request parameters for normal transactions
5669
const txParams: Record<string, string> = {
5770
chainid: chainId.toString(),
5871
module: 'account',
5972
action: 'txlist',
6073
address: args.address,
6174
startblock: (args.startblock ?? 0).toString(),
62-
endblock: (args.endblock ?? "latest").toString(),
75+
endblock: (args.endblock ?? 'latest').toString(),
6376
page: (args.page ?? 1).toString(),
6477
offset: (args.offset ?? 5).toString(),
6578
sort: args.sort ?? 'desc',
6679
};
67-
80+
6881
// API call to get 'normal' transaction data
6982
const txData = await makeEtherscanRequest(txParams);
70-
83+
7184
// Get ERC20 token transfers data within block range and map to transaction hash
72-
const tokenTransfersByHash: Record<string, any[]> = {};
73-
if (txData.status === '1' && Array.isArray(txData.result) && txData.result.length > 0) {
74-
85+
const tokenTransfersByHash: Record<
86+
string,
87+
Array<Record<string, string>>
88+
> = {};
89+
if (
90+
txData.status === '1' &&
91+
Array.isArray(txData.result) &&
92+
txData.result.length > 0
93+
) {
7594
// Find min and max block numbers based on sort order
76-
const blockNumbers = txData.result.map((tx: any) => parseInt(tx.blockNumber));
95+
const blockNumbers = txData.result.map((tx: Record<string, string>) =>
96+
parseInt(tx.blockNumber),
97+
);
7798

7899
let minBlock: number;
79100
let maxBlock: number;
@@ -84,57 +105,64 @@ export async function getAddressTransactionsHandler(
84105
minBlock = blockNumbers[blockNumbers.length - 1];
85106
maxBlock = blockNumbers[0];
86107
}
87-
108+
88109
// Request parameters for ERC20 token transfers
89110
const tokenTxParams: Record<string, string> = {
90111
chainid: chainId.toString(),
91112
module: 'account',
92113
action: 'tokentx',
93114
address: args.address,
94-
startblock: (minBlock-1).toString(),
95-
endblock: (maxBlock+1).toString(),
115+
startblock: (minBlock - 1).toString(),
116+
endblock: (maxBlock + 1).toString(),
96117
page: '1',
97-
offset: '100',
118+
offset: '100',
98119
sort: args.sort ?? 'desc',
99120
};
100-
121+
101122
// API call to get ERC20 token transfer data
102123
const tokenTxData = await makeEtherscanRequest(tokenTxParams);
103-
124+
104125
if (tokenTxData.status === '1' && Array.isArray(tokenTxData.result)) {
105-
106126
// Map token transfers that match transaction hashes
107-
const txHashes = new Set(txData.result.map((tx: any) => tx.hash));
127+
const txHashes = new Set(
128+
txData.result.map((tx: Record<string, string>) => tx.hash),
129+
);
108130

109-
tokenTxData.result.forEach((tokenTx: any) => {
131+
tokenTxData.result.forEach((tokenTx: Record<string, string>) => {
110132
if (txHashes.has(tokenTx.hash)) {
111133
if (!tokenTransfersByHash[tokenTx.hash]) {
112134
tokenTransfersByHash[tokenTx.hash] = [];
113135
}
114-
136+
115137
tokenTransfersByHash[tokenTx.hash].push({
116138
from: tokenTx.from,
117139
contractAddress: tokenTx.contractAddress,
118140
to: tokenTx.to,
119-
value: formatUnits(BigInt(tokenTx.value), tokenTx.tokenDecimal) + ' ' + tokenTx.tokenSymbol,
141+
value:
142+
formatUnits(
143+
BigInt(tokenTx.value),
144+
parseInt(tokenTx.tokenDecimal),
145+
) +
146+
' ' +
147+
tokenTx.tokenSymbol,
120148
tokenName: tokenTx.tokenName,
121149
});
122150
}
123151
});
124152
}
125153
}
126-
154+
127155
// Format the transaction data
128156
if (txData.status === '1' && Array.isArray(txData.result)) {
129-
const filteredResults = txData.result.map((tx: any) => {
157+
const filteredResults = txData.result.map((tx: Record<string, string>) => {
130158
// Convert Unix timestamp to human-readable date
131159
const date = new Date(parseInt(tx.timeStamp) * 1000);
132160
const formattedDate = date.toISOString();
133-
134-
// Calculate paid fee in ETH
161+
162+
// Calculate paid fee in ETH
135163
const feeWei = BigInt(tx.gasUsed) * BigInt(tx.gasPrice);
136164
const feeInEth = formatUnits(feeWei, 18);
137-
165+
138166
const result = {
139167
timeStamp: formattedDate + ' UTC',
140168
hash: tx.hash,
@@ -150,15 +178,98 @@ export async function getAddressTransactionsHandler(
150178
feeInEth: feeInEth + ' ETH',
151179
methodId: tx.methodId,
152180
functionName: tx.functionName,
153-
tokenTransfers: tokenTransfersByHash[tx.hash] || []
181+
tokenTransfers: tokenTransfersByHash[tx.hash] || [],
154182
};
155-
183+
156184
return result;
157185
});
158-
186+
159187
// Add debug information to the response
160-
return filteredResults;
188+
return JSON.stringify(filteredResults);
189+
}
190+
191+
return JSON.stringify(txData);
192+
}
193+
194+
export async function getContractInfoHandler(
195+
wallet: WalletClient & PublicActions,
196+
args: z.infer<typeof GetContractInfoSchema>,
197+
): Promise<string> {
198+
// Get chain ID from args or wallet
199+
const chainId = args.chainId ?? wallet.chain?.id ?? base.id;
200+
201+
// Validate address
202+
if (!isAddress(args.address, { strict: false })) {
203+
throw new Error(`Invalid address: ${args.address}`);
204+
}
205+
206+
// Check if address is a contract
207+
const code = await getCode(wallet, { address: args.address });
208+
if (code === '0x') {
209+
throw new Error(`Address is not a contract: ${args.address}`);
210+
}
211+
212+
// Get ETH balance of contract
213+
const ethBalance = await getBalance(wallet, { address: args.address });
214+
215+
// Request parameters for contract source code
216+
const sourceCodeParams: Record<string, string> = {
217+
chainid: chainId.toString(),
218+
module: 'contract',
219+
action: 'getsourcecode',
220+
address: args.address,
221+
};
222+
223+
// API call to get contract source code data
224+
const sourceCodeData = await makeEtherscanRequest(sourceCodeParams);
225+
226+
// Request parameters for contract creation info
227+
const creationParams: Record<string, string> = {
228+
chainid: chainId.toString(),
229+
module: 'contract',
230+
action: 'getcontractcreation',
231+
contractaddresses: args.address,
232+
};
233+
234+
// API call to get contract creation data
235+
const creationData = await makeEtherscanRequest(creationParams);
236+
237+
// Extract and format the required information
238+
const result = {
239+
contractName: null as string | null,
240+
contractAddress: args.address,
241+
abi: null as string | null,
242+
contractCreator: null as string | null,
243+
txHash: null as string | null,
244+
timestamp: null as string | null,
245+
ethBalance: formatUnits(ethBalance, 18) + ' ETH',
246+
};
247+
248+
if (
249+
sourceCodeData.status === '1' &&
250+
Array.isArray(sourceCodeData.result) &&
251+
sourceCodeData.result.length > 0
252+
) {
253+
const sourceCode = sourceCodeData.result[0];
254+
result.abi = sourceCode.ABI;
255+
result.contractName = sourceCode.ContractName;
256+
}
257+
258+
if (
259+
creationData.status === '1' &&
260+
Array.isArray(creationData.result) &&
261+
creationData.result.length > 0
262+
) {
263+
const creation = creationData.result[0];
264+
result.contractCreator = creation.contractCreator;
265+
result.txHash = creation.txHash;
266+
267+
// Convert timestamp to human-readable date
268+
if (creation.timestamp) {
269+
const date = new Date(parseInt(creation.timestamp) * 1000);
270+
result.timestamp = date.toISOString() + ' UTC';
271+
}
161272
}
162-
163-
return txData;
273+
274+
return JSON.stringify(result);
164275
}

src/tools/etherscan/index.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
import { generateTool } from '../../utils.js';
2-
import { getAddressTransactionsHandler } from './handlers.js';
3-
import { GetAddressTransactionsSchema } from './schemas.js';
2+
import {
3+
getAddressTransactionsHandler,
4+
getContractInfoHandler,
5+
} from './handlers.js';
6+
import {
7+
GetAddressTransactionsSchema,
8+
GetContractInfoSchema,
9+
} from './schemas.js';
410

511
export const getAddressTransactionsTool = generateTool({
612
name: 'etherscan_address_transactions',
713
description: 'Gets a list of transactions for an address using Etherscan API',
814
inputSchema: GetAddressTransactionsSchema,
915
toolHandler: getAddressTransactionsHandler,
1016
});
17+
18+
export const getContractInfoTool = generateTool({
19+
name: 'etherscan_contract_info',
20+
description: 'Gets contract information using Etherscan API',
21+
inputSchema: GetContractInfoSchema,
22+
toolHandler: getContractInfoHandler,
23+
});

0 commit comments

Comments
 (0)