@openzeppelin/ui-builder-adapter-evm 1.2.0 → 1.4.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.
Files changed (105) hide show
  1. package/README.md +33 -18
  2. package/dist/index.cjs +4651 -4448
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +12 -226
  5. package/dist/index.d.ts +12 -226
  6. package/dist/index.js +4737 -4535
  7. package/dist/index.js.map +1 -1
  8. package/package.json +7 -6
  9. package/src/__tests__/adapter-parsing.test.ts +4 -3
  10. package/src/__tests__/getDefaultServiceConfig.test.ts +185 -0
  11. package/src/__tests__/mocks/mock-network-configs.ts +1 -1
  12. package/src/__tests__/provenanceLinks.test.ts +6 -4
  13. package/src/__tests__/providerSelection.test.ts +5 -4
  14. package/src/__tests__/timeouts.test.ts +5 -3
  15. package/src/__tests__/wallet-connect.test.ts +2 -2
  16. package/src/adapter.ts +61 -107
  17. package/src/configuration/execution.ts +1 -52
  18. package/src/configuration/index.ts +2 -3
  19. package/src/configuration/network-services.ts +47 -60
  20. package/src/index.ts +22 -13
  21. package/src/networks/index.ts +2 -1
  22. package/src/networks/mainnet.ts +1 -1
  23. package/src/networks/testnet.ts +1 -1
  24. package/src/query/adapter-query.ts +72 -0
  25. package/src/query/index.ts +2 -2
  26. package/src/transaction/components/useEvmRelayerOptions.ts +5 -3
  27. package/src/transaction/index.ts +1 -5
  28. package/src/types/artifacts.ts +5 -30
  29. package/src/types/providers.ts +7 -18
  30. package/src/wallet/components/EvmWalletUiRoot.tsx +1 -1
  31. package/src/wallet/evmUiKitManager.ts +26 -129
  32. package/src/wallet/hooks/index.ts +0 -1
  33. package/src/wallet/implementation/wagmi-implementation.ts +45 -577
  34. package/src/wallet/index.ts +2 -3
  35. package/src/wallet/rainbowkit/__tests__/export-service.test.ts +1 -2
  36. package/src/wallet/rainbowkit/componentFactory.ts +10 -8
  37. package/src/wallet/rainbowkit/components.tsx +16 -133
  38. package/src/wallet/rainbowkit/index.ts +27 -5
  39. package/src/wallet/utils/__tests__/uiKitService.test.ts +5 -1
  40. package/src/wallet/utils/connection.ts +8 -52
  41. package/src/wallet/utils/index.ts +0 -2
  42. package/src/wallet/utils/uiKitService.ts +7 -3
  43. package/src/wallet/utils/walletImplementationManager.ts +5 -4
  44. package/src/wallet/utils.ts +1 -65
  45. package/src/abi/__tests__/etherscan-v2.test.ts +0 -117
  46. package/src/abi/__tests__/transformer.test.ts +0 -342
  47. package/src/abi/comparison.ts +0 -389
  48. package/src/abi/etherscan-v2.ts +0 -243
  49. package/src/abi/etherscan.ts +0 -158
  50. package/src/abi/index.ts +0 -7
  51. package/src/abi/loader.ts +0 -415
  52. package/src/abi/sourcify.ts +0 -75
  53. package/src/abi/transformer.ts +0 -163
  54. package/src/abi/types.ts +0 -101
  55. package/src/configuration/__tests__/explorer.test.ts +0 -174
  56. package/src/configuration/__tests__/rpc.test.ts +0 -176
  57. package/src/configuration/explorer.ts +0 -243
  58. package/src/configuration/rpc.ts +0 -257
  59. package/src/mapping/__tests__/field-generator.test.ts +0 -137
  60. package/src/mapping/__tests__/type-mapper.test.ts +0 -139
  61. package/src/mapping/constants.ts +0 -57
  62. package/src/mapping/field-generator.ts +0 -115
  63. package/src/mapping/index.ts +0 -4
  64. package/src/mapping/type-mapper.ts +0 -80
  65. package/src/proxy/detection.ts +0 -465
  66. package/src/query/handler.ts +0 -227
  67. package/src/query/view-checker.ts +0 -10
  68. package/src/transaction/eoa.ts +0 -98
  69. package/src/transaction/execution-strategy.ts +0 -33
  70. package/src/transaction/formatter.ts +0 -101
  71. package/src/transaction/relayer.ts +0 -380
  72. package/src/transaction/sender.ts +0 -185
  73. package/src/transform/index.ts +0 -3
  74. package/src/transform/input-parser.ts +0 -177
  75. package/src/transform/output-formatter.ts +0 -64
  76. package/src/types/__tests__/artifacts.test.ts +0 -105
  77. package/src/types.ts +0 -92
  78. package/src/utils/__tests__/artifacts.test.ts +0 -81
  79. package/src/utils/artifacts.ts +0 -30
  80. package/src/utils/formatting.ts +0 -25
  81. package/src/utils/gas.ts +0 -17
  82. package/src/utils/index.ts +0 -6
  83. package/src/utils/json.ts +0 -19
  84. package/src/utils/validation.ts +0 -10
  85. package/src/validation/eoa.ts +0 -33
  86. package/src/validation/index.ts +0 -2
  87. package/src/validation/relayer.ts +0 -13
  88. package/src/wallet/__tests__/utils.test.ts +0 -149
  89. package/src/wallet/components/account/AccountDisplay.tsx +0 -52
  90. package/src/wallet/components/connect/ConnectButton.tsx +0 -125
  91. package/src/wallet/components/connect/ConnectorDialog.tsx +0 -140
  92. package/src/wallet/components/index.ts +0 -4
  93. package/src/wallet/components/network/NetworkSwitcher.tsx +0 -90
  94. package/src/wallet/context/index.ts +0 -1
  95. package/src/wallet/context/wagmi-context.tsx +0 -7
  96. package/src/wallet/hooks/useIsWagmiProviderInitialized.ts +0 -11
  97. package/src/wallet/rainbowkit/config-generator.ts +0 -56
  98. package/src/wallet/rainbowkit/config-service.ts +0 -169
  99. package/src/wallet/rainbowkit/export-service.ts +0 -18
  100. package/src/wallet/rainbowkit/rainbowkitAssetManager.ts +0 -74
  101. package/src/wallet/rainbowkit/types.ts +0 -74
  102. package/src/wallet/rainbowkit/utils.ts +0 -96
  103. package/src/wallet/services/configResolutionService.ts +0 -65
  104. package/src/wallet/utils/SafeWagmiComponent.tsx +0 -72
  105. package/src/wallet/utils/filterWalletComponents.ts +0 -89
@@ -1,389 +0,0 @@
1
- /**
2
- * ABI comparison service for EVM contracts
3
- * Provides detailed analysis of differences between ABIs
4
- */
5
-
6
- import type { Abi } from 'viem';
7
-
8
- import { logger, simpleHash } from '@openzeppelin/ui-utils';
9
-
10
- import type { AbiComparisonResult, AbiDifference, AbiValidationResult } from './types';
11
- import { isValidAbiArray } from './types';
12
-
13
- /**
14
- * Service for comparing and validating EVM ABIs
15
- */
16
- export class AbiComparisonService {
17
- /**
18
- * Compares two ABIs and returns detailed difference analysis
19
- */
20
- public compareAbis(abi1: string, abi2: string): AbiComparisonResult {
21
- try {
22
- const validation1 = this.validateAbi(abi1);
23
- const validation2 = this.validateAbi(abi2);
24
-
25
- if (!validation1.valid || !validation2.valid) {
26
- return {
27
- identical: false,
28
- differences: [],
29
- severity: 'breaking',
30
- summary: 'One or both ABIs are invalid and cannot be compared',
31
- };
32
- }
33
-
34
- const normalized1 = this.normalizeAbi(validation1.normalizedAbi!);
35
- const normalized2 = this.normalizeAbi(validation2.normalizedAbi!);
36
-
37
- const hash1 = simpleHash(JSON.stringify(normalized1));
38
- const hash2 = simpleHash(JSON.stringify(normalized2));
39
-
40
- if (hash1 === hash2) {
41
- return {
42
- identical: true,
43
- differences: [],
44
- severity: 'none',
45
- summary: 'ABIs are identical',
46
- };
47
- }
48
-
49
- const differences = this.findDifferences(normalized1, normalized2);
50
- const severity = this.calculateSeverity(differences);
51
-
52
- return {
53
- identical: false,
54
- differences,
55
- severity,
56
- summary: this.generateSummary(differences),
57
- };
58
- } catch (error) {
59
- logger.error('ABI comparison failed:', (error as Error).message);
60
- return {
61
- identical: false,
62
- differences: [],
63
- severity: 'breaking',
64
- summary: `Comparison failed: ${(error as Error).message}`,
65
- };
66
- }
67
- }
68
-
69
- /**
70
- * Validates ABI structure and format
71
- */
72
- public validateAbi(abiString: string): AbiValidationResult {
73
- const errors: string[] = [];
74
- const warnings: string[] = [];
75
-
76
- try {
77
- // Parse JSON
78
- const abi = JSON.parse(abiString);
79
-
80
- if (!Array.isArray(abi)) {
81
- errors.push('ABI must be an array');
82
- return { valid: false, errors, warnings };
83
- }
84
-
85
- // Empty ABI arrays are not valid contract definitions
86
- if (abi.length === 0) {
87
- errors.push(
88
- 'ABI array cannot be empty - contract must have at least one function, event, or constructor'
89
- );
90
- return { valid: false, errors, warnings };
91
- }
92
-
93
- // Validate each ABI item
94
- for (let i = 0; i < abi.length; i++) {
95
- const item = abi[i];
96
-
97
- if (!item.type) {
98
- errors.push(`Item ${i}: Missing 'type' field`);
99
- continue;
100
- }
101
-
102
- if (
103
- !['function', 'event', 'constructor', 'error', 'fallback', 'receive'].includes(item.type)
104
- ) {
105
- errors.push(`Item ${i}: Invalid type '${item.type}'`);
106
- }
107
-
108
- if (item.type === 'function' && !item.name) {
109
- errors.push(`Item ${i}: Function missing 'name' field`);
110
- }
111
-
112
- if ((item.type === 'function' || item.type === 'event') && !Array.isArray(item.inputs)) {
113
- errors.push(`Item ${i}: Missing or invalid 'inputs' array`);
114
- }
115
-
116
- if (item.type === 'function' && !Array.isArray(item.outputs)) {
117
- warnings.push(`Item ${i}: Function missing 'outputs' array`);
118
- }
119
- }
120
-
121
- // Additional viem-specific validation
122
- if (errors.length === 0 && !isValidAbiArray(abi)) {
123
- errors.push('ABI does not conform to expected format');
124
- }
125
-
126
- return {
127
- valid: errors.length === 0,
128
- errors,
129
- warnings,
130
- normalizedAbi: errors.length === 0 ? (abi as Abi) : undefined,
131
- };
132
- } catch (parseError) {
133
- errors.push(`Invalid JSON: ${(parseError as Error).message}`);
134
- return { valid: false, errors, warnings };
135
- }
136
- }
137
-
138
- /**
139
- * Creates deterministic hash of ABI for quick comparison
140
- */
141
- public hashAbi(abiString: string): string {
142
- try {
143
- const validation = this.validateAbi(abiString);
144
- if (!validation.valid || !validation.normalizedAbi) {
145
- throw new Error('Cannot hash invalid ABI');
146
- }
147
-
148
- const normalized = this.normalizeAbi(validation.normalizedAbi);
149
- const normalizedString = JSON.stringify(normalized);
150
-
151
- return simpleHash(normalizedString);
152
- } catch (error) {
153
- logger.error('ABI hashing failed:', (error as Error).message);
154
- throw new Error(`Failed to hash ABI: ${(error as Error).message}`);
155
- }
156
- }
157
-
158
- /**
159
- * Normalizes ABI for consistent comparison
160
- */
161
- private normalizeAbi(abi: Abi): Abi {
162
- return abi
163
- .map((item) => {
164
- // Remove ordering-dependent fields and sort inputs/outputs
165
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
166
- const normalized: any = { ...item };
167
-
168
- if (normalized.inputs) {
169
- normalized.inputs = [...normalized.inputs].sort((a, b) =>
170
- (a.name || '').localeCompare(b.name || '')
171
- );
172
- }
173
-
174
- if (normalized.outputs) {
175
- normalized.outputs = [...normalized.outputs].sort((a, b) =>
176
- (a.name || '').localeCompare(b.name || '')
177
- );
178
- }
179
-
180
- return normalized;
181
- })
182
- .sort((a, b) => {
183
- // Sort by type first, then by name
184
- const typeOrder = {
185
- constructor: 0,
186
- fallback: 1,
187
- receive: 2,
188
- function: 3,
189
- event: 4,
190
- error: 5,
191
- };
192
- const aOrder = typeOrder[a.type as keyof typeof typeOrder] ?? 99;
193
- const bOrder = typeOrder[b.type as keyof typeof typeOrder] ?? 99;
194
-
195
- if (aOrder !== bOrder) return aOrder - bOrder;
196
-
197
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
198
- const aName = (a as any).name || '';
199
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
200
- const bName = (b as any).name || '';
201
- return aName.localeCompare(bName);
202
- }) as Abi;
203
- }
204
-
205
- /**
206
- * Finds detailed differences between two normalized ABIs
207
- */
208
- private findDifferences(abi1: Abi, abi2: Abi): AbiDifference[] {
209
- const differences: AbiDifference[] = [];
210
-
211
- // Create maps for efficient lookup
212
- const map1 = this.createAbiMap(abi1);
213
- const map2 = this.createAbiMap(abi2);
214
-
215
- // Find removed items
216
- for (const [key, item] of map1) {
217
- if (!map2.has(key)) {
218
- differences.push({
219
- type: 'removed',
220
- section: item.type as AbiDifference['section'],
221
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
222
- name: (item as any).name || item.type,
223
- details: `${item.type} was removed`,
224
- impact: this.calculateImpact(item.type, 'removed'),
225
- oldSignature: this.generateSignature(item),
226
- });
227
- }
228
- }
229
-
230
- // Find added items
231
- for (const [key, item] of map2) {
232
- if (!map1.has(key)) {
233
- differences.push({
234
- type: 'added',
235
- section: item.type as AbiDifference['section'],
236
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
237
- name: (item as any).name || item.type,
238
- details: `${item.type} was added`,
239
- impact: this.calculateImpact(item.type, 'added'),
240
- newSignature: this.generateSignature(item),
241
- });
242
- }
243
- }
244
-
245
- // Find modified items
246
- for (const [key, item1] of map1) {
247
- const item2 = map2.get(key);
248
- if (item2 && !this.itemsEqual(item1, item2)) {
249
- differences.push({
250
- type: 'modified',
251
- section: item1.type as AbiDifference['section'],
252
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
253
- name: (item1 as any).name || item1.type,
254
- details: `${item1.type} signature changed`,
255
- impact: this.calculateImpact(item1.type, 'modified'),
256
- oldSignature: this.generateSignature(item1),
257
- newSignature: this.generateSignature(item2),
258
- });
259
- }
260
- }
261
-
262
- return differences;
263
- }
264
-
265
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
266
- private createAbiMap(abi: Abi): Map<string, any> {
267
- const map = new Map();
268
-
269
- for (const item of abi) {
270
- const key = this.generateItemKey(item);
271
- map.set(key, item);
272
- }
273
-
274
- return map;
275
- }
276
-
277
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
278
- private generateItemKey(item: any): string {
279
- if (item.type === 'constructor' || item.type === 'fallback' || item.type === 'receive') {
280
- return item.type;
281
- }
282
-
283
- const name = item.name || '';
284
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
285
- const inputs = item.inputs?.map((input: any) => input.type).join(',') || '';
286
- return `${item.type}:${name}(${inputs})`;
287
- }
288
-
289
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
290
- private generateSignature(item: any): string {
291
- if (item.type === 'constructor') {
292
- const inputs =
293
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
294
- item.inputs?.map((input: any) => `${input.type} ${input.name || ''}`).join(', ') || '';
295
- return `constructor(${inputs})`;
296
- }
297
-
298
- if (item.type === 'fallback' || item.type === 'receive') {
299
- return item.type + '()';
300
- }
301
-
302
- if (item.type === 'function') {
303
- const inputs =
304
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
305
- item.inputs?.map((input: any) => `${input.type} ${input.name || ''}`).join(', ') || '';
306
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
307
- const outputs = item.outputs?.map((output: any) => output.type).join(', ') || '';
308
- const mutability = item.stateMutability ? ` ${item.stateMutability}` : '';
309
- return `function ${item.name}(${inputs})${mutability}${outputs ? ` returns (${outputs})` : ''}`;
310
- }
311
-
312
- if (item.type === 'event') {
313
- const inputs =
314
- item.inputs
315
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
316
- ?.map((input: any) => {
317
- const indexed = input.indexed ? ' indexed' : '';
318
- return `${input.type}${indexed} ${input.name || ''}`;
319
- })
320
- .join(', ') || '';
321
- return `event ${item.name}(${inputs})`;
322
- }
323
-
324
- return JSON.stringify(item);
325
- }
326
-
327
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
328
- private itemsEqual(item1: any, item2: any): boolean {
329
- return JSON.stringify(item1) === JSON.stringify(item2);
330
- }
331
-
332
- private calculateImpact(
333
- type: string,
334
- changeType: 'added' | 'removed' | 'modified'
335
- ): 'low' | 'medium' | 'high' {
336
- if (type === 'constructor' || type === 'fallback' || type === 'receive') {
337
- return changeType === 'modified' ? 'high' : 'medium';
338
- }
339
-
340
- if (type === 'function') {
341
- if (changeType === 'removed') return 'high'; // Breaking: removes functionality
342
- if (changeType === 'modified') return 'medium'; // Major: changes behavior
343
- if (changeType === 'added') return 'low'; // Minor: adds functionality
344
- }
345
-
346
- if (type === 'event') {
347
- return 'low'; // Events don't break existing functionality
348
- }
349
-
350
- if (type === 'error') {
351
- return 'low'; // Custom errors don't break existing functionality
352
- }
353
-
354
- return 'medium';
355
- }
356
-
357
- private calculateSeverity(differences: AbiDifference[]): 'none' | 'minor' | 'major' | 'breaking' {
358
- if (differences.length === 0) return 'none';
359
-
360
- const hasHighImpact = differences.some((d) => d.impact === 'high');
361
- const hasMediumImpact = differences.some((d) => d.impact === 'medium');
362
- const hasRemovedFunctions = differences.some(
363
- (d) => d.type === 'removed' && d.section === 'function'
364
- );
365
-
366
- if (hasRemovedFunctions || hasHighImpact) return 'breaking';
367
- if (hasMediumImpact) return 'major';
368
- return 'minor';
369
- }
370
-
371
- private generateSummary(differences: AbiDifference[]): string {
372
- const counts = {
373
- added: differences.filter((d) => d.type === 'added').length,
374
- removed: differences.filter((d) => d.type === 'removed').length,
375
- modified: differences.filter((d) => d.type === 'modified').length,
376
- };
377
-
378
- const parts: string[] = [];
379
- if (counts.added > 0) parts.push(`${counts.added} added`);
380
- if (counts.removed > 0) parts.push(`${counts.removed} removed`);
381
- if (counts.modified > 0) parts.push(`${counts.modified} modified`);
382
-
383
- const summary = parts.join(', ');
384
- return `${summary}`;
385
- }
386
- }
387
-
388
- // Export singleton instance
389
- export const abiComparisonService = new AbiComparisonService();
@@ -1,243 +0,0 @@
1
- import type { ContractSchema } from '@openzeppelin/ui-types';
2
- import { logger } from '@openzeppelin/ui-utils';
3
-
4
- import { resolveExplorerConfig } from '../configuration/explorer';
5
- import type { AbiItem, TypedEvmNetworkConfig } from '../types';
6
- import { transformAbiToSchema } from './transformer';
7
-
8
- /**
9
- * Result type for Etherscan ABI loading that includes the original ABI string
10
- */
11
- export interface EtherscanAbiResult {
12
- schema: ContractSchema;
13
- originalAbi: string;
14
- }
15
-
16
- // Etherscan V2 unified API base URL
17
- const ETHERSCAN_V2_BASE_URL = 'https://api.etherscan.io/v2/api';
18
-
19
- /**
20
- * Builds a V2 API URL for Etherscan-compatible explorers
21
- */
22
- function buildV2ApiUrl(
23
- chainId: number,
24
- module: string,
25
- action: string,
26
- params: Record<string, string>,
27
- apiKey?: string // Some explorers don't require an API key (routescan.io)
28
- ): string {
29
- const url = new URL(ETHERSCAN_V2_BASE_URL);
30
- url.searchParams.append('chainid', chainId.toString());
31
- url.searchParams.append('module', module);
32
- url.searchParams.append('action', action);
33
-
34
- Object.entries(params).forEach(([key, value]) => {
35
- url.searchParams.append(key, value);
36
- });
37
-
38
- if (apiKey) {
39
- url.searchParams.append('apikey', apiKey);
40
- }
41
-
42
- return url.toString();
43
- }
44
-
45
- /**
46
- * Checks if the network supports V2 API
47
- */
48
- export function shouldUseV2Api(networkConfig: TypedEvmNetworkConfig): boolean {
49
- if (!networkConfig.supportsEtherscanV2) {
50
- return false;
51
- }
52
-
53
- return true;
54
- }
55
-
56
- /**
57
- * Fetches and parses an ABI from Etherscan V2 API using a contract address and network config.
58
- */
59
- export async function loadAbiFromEtherscanV2(
60
- address: string,
61
- networkConfig: TypedEvmNetworkConfig
62
- ): Promise<EtherscanAbiResult> {
63
- const explorerConfig = resolveExplorerConfig(networkConfig);
64
-
65
- const url = buildV2ApiUrl(
66
- networkConfig.chainId,
67
- 'contract',
68
- 'getabi',
69
- { address },
70
- explorerConfig.apiKey
71
- );
72
-
73
- let response: Response;
74
- try {
75
- logger.info(
76
- 'loadAbiFromEtherscanV2',
77
- `Fetching ABI from Etherscan V2 API for address: ${address} on chain ${networkConfig.chainId}`
78
- );
79
- response = await fetch(url);
80
- } catch (networkError) {
81
- logger.error(
82
- 'loadAbiFromEtherscanV2',
83
- `Network error fetching ABI from Explorer V2 API: ${networkError}`
84
- );
85
- throw new Error(`Network error fetching ABI: ${(networkError as Error).message}`);
86
- }
87
-
88
- if (!response.ok) {
89
- logger.error(
90
- 'loadAbiFromEtherscanV2',
91
- `Explorer V2 API request failed with status: ${response.status}`
92
- );
93
- throw new Error(`Explorer V2 API request failed: ${response.status} ${response.statusText}`);
94
- }
95
-
96
- let apiResult: { status: string; message: string; result: string };
97
- try {
98
- apiResult = await response.json();
99
- } catch (jsonError) {
100
- logger.error(
101
- 'loadAbiFromEtherscanV2',
102
- `Failed to parse Explorer V2 API response as JSON: ${jsonError}`
103
- );
104
- throw new Error('Invalid JSON response received from Explorer V2 API.');
105
- }
106
-
107
- if (apiResult.status !== '1') {
108
- logger.warn(
109
- 'loadAbiFromEtherscanV2',
110
- `Explorer V2 API error: Status ${apiResult.status}, Message: ${apiResult.message}, Result: ${apiResult.result}`
111
- );
112
-
113
- // Handle specific V2 API error messages
114
- if (apiResult.message?.includes('NOTOK')) {
115
- if (apiResult.result?.includes('Invalid API Key')) {
116
- throw new Error(
117
- `Invalid API key for Etherscan V2. Please check your API key configuration.`
118
- );
119
- }
120
- if (apiResult.result?.includes('Contract source code not verified')) {
121
- throw new Error(
122
- `Contract not verified on ${networkConfig.name} explorer (address: ${address}). ABI not available. You can provide the contract's ABI manually.`
123
- );
124
- }
125
- if (apiResult.result?.includes('Invalid chain')) {
126
- throw new Error(
127
- `Chain ID ${networkConfig.chainId} is not supported by Etherscan V2 API. Please check if this chain is available.`
128
- );
129
- }
130
- }
131
-
132
- throw new Error(`Explorer V2 API Error: ${apiResult.result || apiResult.message}`);
133
- }
134
-
135
- // Store the original raw ABI string before parsing
136
- const originalAbiString = apiResult.result;
137
-
138
- let abi: AbiItem[];
139
- try {
140
- abi = JSON.parse(originalAbiString);
141
- if (!Array.isArray(abi)) {
142
- throw new Error('Parsed ABI from Explorer V2 API is not an array.');
143
- }
144
- } catch (error) {
145
- logger.error(
146
- 'loadAbiFromEtherscanV2',
147
- `Failed to parse ABI JSON string from Explorer V2 API result: ${error}`
148
- );
149
- throw new Error(`Invalid ABI JSON received from Explorer V2 API: ${(error as Error).message}`);
150
- }
151
-
152
- logger.info(
153
- 'loadAbiFromEtherscanV2',
154
- `Successfully parsed ABI for ${networkConfig.name} with ${abi.length} items using V2 API.`
155
- );
156
-
157
- // TODO: Fetch contract name?
158
- const contractName = `Contract_${address.substring(0, 6)}`;
159
- const schema = transformAbiToSchema(abi, contractName, address);
160
-
161
- return {
162
- schema,
163
- originalAbi: originalAbiString,
164
- };
165
- }
166
-
167
- /**
168
- * Test connection to Etherscan V2 API
169
- */
170
- export async function testEtherscanV2Connection(
171
- networkConfig: TypedEvmNetworkConfig,
172
- apiKey?: string
173
- ): Promise<{
174
- success: boolean;
175
- latency?: number;
176
- error?: string;
177
- }> {
178
- const startTime = Date.now();
179
-
180
- const requiresApiKey = networkConfig.requiresExplorerApiKey ?? true;
181
-
182
- if (requiresApiKey && !apiKey) {
183
- return {
184
- success: false,
185
- error: 'API key is required for testing connection to this explorer',
186
- };
187
- }
188
-
189
- try {
190
- // Test with a simple API call - get the latest block number
191
- const url = buildV2ApiUrl(networkConfig.chainId, 'proxy', 'eth_blockNumber', {}, apiKey);
192
-
193
- const response = await fetch(url);
194
- const latency = Date.now() - startTime;
195
-
196
- if (!response.ok) {
197
- return {
198
- success: false,
199
- error: `HTTP ${response.status}: ${response.statusText}`,
200
- latency,
201
- };
202
- }
203
-
204
- const data = await response.json();
205
-
206
- // Check for API errors in the response
207
- if (data.status === '0' && data.message) {
208
- // Handle specific V2 API error messages
209
- if (data.result?.includes('Invalid API Key')) {
210
- return {
211
- success: false,
212
- error: 'Invalid API key. Please check your Etherscan API key.',
213
- latency,
214
- };
215
- }
216
- if (data.result?.includes('Invalid chain')) {
217
- return {
218
- success: false,
219
- error: `Chain ID ${networkConfig.chainId} is not supported by Etherscan V2 API.`,
220
- latency,
221
- };
222
- }
223
-
224
- return {
225
- success: false,
226
- error: data.result || data.message,
227
- latency,
228
- };
229
- }
230
-
231
- // Success if we got a valid response
232
- return {
233
- success: true,
234
- latency,
235
- };
236
- } catch (error) {
237
- return {
238
- success: false,
239
- error: error instanceof Error ? error.message : 'Connection test failed',
240
- latency: Date.now() - startTime,
241
- };
242
- }
243
- }