1
1
import type { PublicActions , WalletClient } from 'viem' ;
2
+ import { formatGwei , formatUnits , isAddress } from 'viem' ;
3
+ import { getBalance , getCode } from 'viem/actions' ;
2
4
import { base } from 'viem/chains' ;
3
- import { formatUnits , formatGwei } from 'viem' ;
4
5
import type { z } from 'zod' ;
5
- import type { GetAddressTransactionsSchema } from './schemas.js' ;
6
+ import type {
7
+ GetAddressTransactionsSchema ,
8
+ GetContractInfoSchema ,
9
+ } from './schemas.js' ;
6
10
7
11
// Etherscan API endpoint for all supported chains
8
12
const ETHERSCAN_API_URL = 'https://api.etherscan.io/v2/api' ;
9
13
10
14
// Helper function to handle Etherscan API requests using V2 API
11
15
async function makeEtherscanRequest (
12
16
params : Record < string , string > ,
13
- ) : Promise < any > {
17
+ ) : Promise < Record < string , unknown > > {
14
18
// Add API key if available
15
19
const apiKey = process . env . ETHERSCAN_API_KEY ;
16
20
if ( apiKey ) {
17
21
params . apikey = apiKey ;
18
22
} else {
19
23
throw new Error ( 'ETHERSCAN_API_KEY is not set' ) ;
20
24
}
21
-
25
+
22
26
// Build query string
23
27
const queryParams = new URLSearchParams ( ) ;
24
28
Object . entries ( params ) . forEach ( ( [ key , value ] ) => {
25
29
queryParams . append ( key , value ) ;
26
30
} ) ;
27
31
28
32
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
+
31
37
if ( ! response . ok ) {
32
38
throw new Error ( `HTTP error! Status: ${ response . status } ` ) ;
33
39
}
34
-
40
+
35
41
const data = await response . json ( ) ;
36
-
42
+
37
43
// Handle Etherscan API errors
38
44
if ( data . status === '0' && data . message === 'NOTOK' ) {
39
45
throw new Error ( `Etherscan API error: ${ data . result } ` ) ;
40
46
}
41
-
47
+
42
48
return data ;
43
49
} 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
+ ) ;
45
53
}
46
54
}
47
55
48
56
export async function getAddressTransactionsHandler (
49
57
wallet : WalletClient & PublicActions ,
50
58
args : z . infer < typeof GetAddressTransactionsSchema > ,
51
- ) : Promise < any > {
59
+ ) : Promise < string > {
52
60
// Get chain ID from args or wallet
53
61
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
+
55
68
// Request parameters for normal transactions
56
69
const txParams : Record < string , string > = {
57
70
chainid : chainId . toString ( ) ,
58
71
module : 'account' ,
59
72
action : 'txlist' ,
60
73
address : args . address ,
61
74
startblock : ( args . startblock ?? 0 ) . toString ( ) ,
62
- endblock : ( args . endblock ?? " latest" ) . toString ( ) ,
75
+ endblock : ( args . endblock ?? ' latest' ) . toString ( ) ,
63
76
page : ( args . page ?? 1 ) . toString ( ) ,
64
77
offset : ( args . offset ?? 5 ) . toString ( ) ,
65
78
sort : args . sort ?? 'desc' ,
66
79
} ;
67
-
80
+
68
81
// API call to get 'normal' transaction data
69
82
const txData = await makeEtherscanRequest ( txParams ) ;
70
-
83
+
71
84
// 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
+ ) {
75
94
// 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
+ ) ;
77
98
78
99
let minBlock : number ;
79
100
let maxBlock : number ;
@@ -84,57 +105,64 @@ export async function getAddressTransactionsHandler(
84
105
minBlock = blockNumbers [ blockNumbers . length - 1 ] ;
85
106
maxBlock = blockNumbers [ 0 ] ;
86
107
}
87
-
108
+
88
109
// Request parameters for ERC20 token transfers
89
110
const tokenTxParams : Record < string , string > = {
90
111
chainid : chainId . toString ( ) ,
91
112
module : 'account' ,
92
113
action : 'tokentx' ,
93
114
address : args . address ,
94
- startblock : ( minBlock - 1 ) . toString ( ) ,
95
- endblock : ( maxBlock + 1 ) . toString ( ) ,
115
+ startblock : ( minBlock - 1 ) . toString ( ) ,
116
+ endblock : ( maxBlock + 1 ) . toString ( ) ,
96
117
page : '1' ,
97
- offset : '100' ,
118
+ offset : '100' ,
98
119
sort : args . sort ?? 'desc' ,
99
120
} ;
100
-
121
+
101
122
// API call to get ERC20 token transfer data
102
123
const tokenTxData = await makeEtherscanRequest ( tokenTxParams ) ;
103
-
124
+
104
125
if ( tokenTxData . status === '1' && Array . isArray ( tokenTxData . result ) ) {
105
-
106
126
// 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
+ ) ;
108
130
109
- tokenTxData . result . forEach ( ( tokenTx : any ) => {
131
+ tokenTxData . result . forEach ( ( tokenTx : Record < string , string > ) => {
110
132
if ( txHashes . has ( tokenTx . hash ) ) {
111
133
if ( ! tokenTransfersByHash [ tokenTx . hash ] ) {
112
134
tokenTransfersByHash [ tokenTx . hash ] = [ ] ;
113
135
}
114
-
136
+
115
137
tokenTransfersByHash [ tokenTx . hash ] . push ( {
116
138
from : tokenTx . from ,
117
139
contractAddress : tokenTx . contractAddress ,
118
140
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 ,
120
148
tokenName : tokenTx . tokenName ,
121
149
} ) ;
122
150
}
123
151
} ) ;
124
152
}
125
153
}
126
-
154
+
127
155
// Format the transaction data
128
156
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 > ) => {
130
158
// Convert Unix timestamp to human-readable date
131
159
const date = new Date ( parseInt ( tx . timeStamp ) * 1000 ) ;
132
160
const formattedDate = date . toISOString ( ) ;
133
-
134
- // Calculate paid fee in ETH
161
+
162
+ // Calculate paid fee in ETH
135
163
const feeWei = BigInt ( tx . gasUsed ) * BigInt ( tx . gasPrice ) ;
136
164
const feeInEth = formatUnits ( feeWei , 18 ) ;
137
-
165
+
138
166
const result = {
139
167
timeStamp : formattedDate + ' UTC' ,
140
168
hash : tx . hash ,
@@ -150,15 +178,98 @@ export async function getAddressTransactionsHandler(
150
178
feeInEth : feeInEth + ' ETH' ,
151
179
methodId : tx . methodId ,
152
180
functionName : tx . functionName ,
153
- tokenTransfers : tokenTransfersByHash [ tx . hash ] || [ ]
181
+ tokenTransfers : tokenTransfersByHash [ tx . hash ] || [ ] ,
154
182
} ;
155
-
183
+
156
184
return result ;
157
185
} ) ;
158
-
186
+
159
187
// 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
+ }
161
272
}
162
-
163
- return txData ;
273
+
274
+ return JSON . stringify ( result ) ;
164
275
}
0 commit comments