UNPKG

@openzeppelin/contracts-ui-builder-adapter-evm

Version:
242 lines (213 loc) 7.07 kB
import { NetworkConfig, UserExplorerConfig } from '@openzeppelin/contracts-ui-builder-types'; import { appConfigService, logger, userExplorerConfigService, } from '@openzeppelin/contracts-ui-builder-utils'; import { shouldUseV2Api, testEtherscanV2Connection } from '../abi/etherscan-v2'; import { TypedEvmNetworkConfig } from '../types'; import { isValidEvmAddress } from '../utils'; /** * Resolves the explorer configuration for a given EVM network. * Priority order: * 1. User-configured explorer (from UserExplorerConfigService) * 2. For V2 API networks: Global Etherscan V2 API key (from AppConfigService global service configs) * 3. App-configured explorer API key (from AppConfigService network service configs) * 4. Default network explorer (from NetworkConfig) * * @param networkConfig - The EVM network configuration. * @returns The resolved explorer configuration. */ export function resolveExplorerConfig(networkConfig: TypedEvmNetworkConfig): UserExplorerConfig { // 1. Check for user-configured explorer const userConfig = userExplorerConfigService.getUserExplorerConfig(networkConfig.id); if (userConfig) { logger.info('ExplorerConfig', `Using user-configured explorer for ${networkConfig.name}`); return userConfig; } // 2. For V2 API networks using 'etherscan-v2' identifier, check for global Etherscan V2 API key if ( networkConfig.supportsEtherscanV2 && networkConfig.primaryExplorerApiIdentifier === 'etherscan-v2' ) { const globalV2ApiKey = appConfigService.getGlobalServiceConfig('etherscanv2')?.apiKey as | string | undefined; if (globalV2ApiKey) { logger.info('ExplorerConfig', `Using global Etherscan V2 API key for ${networkConfig.name}`); return { explorerUrl: networkConfig.explorerUrl, apiUrl: networkConfig.apiUrl, apiKey: globalV2ApiKey, name: `${networkConfig.name} Explorer (V2 API)`, isCustom: false, }; } } // 3. Check for app-configured API key (V1 style network-specific keys) const apiKey = networkConfig.primaryExplorerApiIdentifier ? appConfigService.getExplorerApiKey(networkConfig.primaryExplorerApiIdentifier) : undefined; if (apiKey) { logger.info('ExplorerConfig', `Using app-configured API key for ${networkConfig.name}`); return { explorerUrl: networkConfig.explorerUrl, apiUrl: networkConfig.apiUrl, apiKey, name: `${networkConfig.name} Explorer`, isCustom: false, }; } // 4. Use default network explorer (no API key) logger.info( 'ExplorerConfig', `Using default explorer for ${networkConfig.name} (no API key configured)` ); return { explorerUrl: networkConfig.explorerUrl, apiUrl: networkConfig.apiUrl, name: `${networkConfig.name} Explorer`, isCustom: false, }; } /** * Gets a blockchain explorer URL for an EVM address. * Uses the resolved explorer configuration. */ export function getEvmExplorerAddressUrl( address: string, networkConfig: NetworkConfig ): string | null { if (!isValidEvmAddress(address)) { return null; } const explorerConfig = resolveExplorerConfig(networkConfig as TypedEvmNetworkConfig); if (!explorerConfig.explorerUrl) { return null; } // Construct the URL using the explorerUrl from the config const baseUrl = explorerConfig.explorerUrl.replace(/\/+$/, ''); return `${baseUrl}/address/${address}`; } /** * Gets a blockchain explorer URL for an EVM transaction. * Uses the resolved explorer configuration. */ export function getEvmExplorerTxUrl(txHash: string, networkConfig: NetworkConfig): string | null { if (!txHash) { return null; } const explorerConfig = resolveExplorerConfig(networkConfig as TypedEvmNetworkConfig); if (!explorerConfig.explorerUrl) { return null; } // Construct the URL using the explorerUrl from the config const baseUrl = explorerConfig.explorerUrl.replace(/\/+$/, ''); return `${baseUrl}/tx/${txHash}`; } /** * Validates an EVM explorer configuration. * Checks URL formats and API key format. */ export function validateEvmExplorerConfig(explorerConfig: UserExplorerConfig): boolean { // Validate URLs if provided if (explorerConfig.explorerUrl) { try { new URL(explorerConfig.explorerUrl); } catch { return false; } } if (explorerConfig.apiUrl) { try { new URL(explorerConfig.apiUrl); } catch { return false; } } // Basic API key validation (not empty) if (explorerConfig.apiKey !== undefined && explorerConfig.apiKey.trim().length === 0) { return false; } return true; } /** * Tests the connection to an EVM explorer API. * Makes a test API call to verify the API key works. */ export async function testEvmExplorerConnection( explorerConfig: UserExplorerConfig, networkConfig?: TypedEvmNetworkConfig ): Promise<{ success: boolean; latency?: number; error?: string; }> { // Check if API key is required for this network const requiresApiKey = networkConfig && 'requiresExplorerApiKey' in networkConfig ? networkConfig.requiresExplorerApiKey !== false : true; if (requiresApiKey && !explorerConfig.apiKey) { return { success: false, error: 'API key is required for testing connection to this explorer', }; } // Check if we should use V2 API if (networkConfig && shouldUseV2Api(networkConfig)) { return testEtherscanV2Connection(networkConfig, explorerConfig.apiKey); } // Use V1 API (legacy) // Use provided API URL or fall back to network config if available let apiUrl = explorerConfig.apiUrl; if (!apiUrl && networkConfig?.apiUrl) { apiUrl = networkConfig.apiUrl; } if (!apiUrl) { return { success: false, error: 'API URL is required for testing connection. Please provide an API URL or ensure the network has a default API URL configured.', }; } const startTime = Date.now(); try { // Test with a simple API call - get the latest block number const url = new URL(apiUrl); url.searchParams.append('module', 'proxy'); url.searchParams.append('action', 'eth_blockNumber'); if (explorerConfig.apiKey) { url.searchParams.append('apikey', explorerConfig.apiKey); } const response = await fetch(url.toString()); const latency = Date.now() - startTime; if (!response.ok) { return { success: false, error: `HTTP ${response.status}: ${response.statusText}`, latency, }; } const data = await response.json(); // Check for API errors in the response if (data.status === '0' && data.message) { return { success: false, error: data.message, latency, }; } // Success if we got a valid response return { success: true, latency, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Connection test failed', latency: Date.now() - startTime, }; } }