UNPKG

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

Version:
206 lines (178 loc) 7.3 kB
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { appConfigService, userExplorerConfigService, } from '@openzeppelin/contracts-ui-builder-utils'; import { loadEvmContract } from '../abi/loader'; import type { TypedEvmNetworkConfig } from '../types'; import type { EvmContractArtifacts } from '../types/artifacts'; describe('EVM provider selection (etherscan → sourcify, forced/app/ui precedence)', () => { const mockFetch = vi.fn(); beforeEach(() => { vi.stubGlobal('fetch', mockFetch); // Initialize app config with empty defaults for deterministic behavior vi.spyOn(appConfigService, 'initialize').mockResolvedValue(); // Reset user explorer config vi.spyOn(userExplorerConfigService, 'getUserExplorerConfig').mockReturnValue( null as unknown as ReturnType<typeof userExplorerConfigService.getUserExplorerConfig> ); }); afterEach(() => { vi.restoreAllMocks(); }); it('tries Etherscan first (V2) when supported (adapter default)', async () => { const network: TypedEvmNetworkConfig = { id: 'ethereum-mainnet', name: 'Ethereum', network: 'mainnet', type: 'mainnet', isTestnet: false, explorerUrl: 'https://etherscan.io', apiUrl: 'https://api.etherscan.io/v2/api', supportsEtherscanV2: true, primaryExplorerApiIdentifier: 'etherscan-v2', chainId: 1, } as unknown as TypedEvmNetworkConfig; // Simulate Etherscan V2 ABI success (returns JSON {status:'1', result:'[ ... ]'}) mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ status: '1', result: '[]' }), }); const artifacts: EvmContractArtifacts = { contractAddress: '0x0000000000000000000000000000000000000001', }; const result = await loadEvmContract(artifacts, network); expect(result.source).toBe('fetched'); expect(result.schema).toBeDefined(); }); it('falls back to Sourcify when Etherscan times out or fails', async () => { const network: TypedEvmNetworkConfig = { id: 'ethereum-mainnet', name: 'Ethereum', network: 'mainnet', type: 'mainnet', isTestnet: false, explorerUrl: 'https://etherscan.io', apiUrl: 'https://api.etherscan.io/v2/api', supportsEtherscanV2: true, primaryExplorerApiIdentifier: 'etherscan-v2', chainId: 1, } as unknown as TypedEvmNetworkConfig; // First call: Etherscan V2 returns non-ok immediately (triggers move to next provider) mockFetch.mockResolvedValueOnce({ ok: false, status: 504, statusText: 'Gateway Timeout', } as Response); // Second call: Simulate Sourcify success mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ output: { abi: [] }, contractName: 'C' }), }); const artifacts: EvmContractArtifacts = { contractAddress: '0x0000000000000000000000000000000000000001', }; const result = await loadEvmContract(artifacts, network); expect(result.source).toBe('fetched'); expect(result.metadata?.fetchedFrom).toContain('sourcify'); }); it('enforces per-provider timeout before moving to next provider', async () => { const network: TypedEvmNetworkConfig = { id: 'ethereum-mainnet', name: 'Ethereum', network: 'mainnet', type: 'mainnet', isTestnet: false, explorerUrl: 'https://etherscan.io', apiUrl: 'https://api.etherscan.io/v2/api', supportsEtherscanV2: true, primaryExplorerApiIdentifier: 'etherscan-v2', } as unknown as TypedEvmNetworkConfig; // Simulate hanging Etherscan response; test will require AbortController in impl mockFetch.mockImplementationOnce( () => new Promise((resolve) => setTimeout(() => resolve({ ok: false } as Response), 6000)) ); const artifacts: EvmContractArtifacts = { contractAddress: '0x0000000000000000000000000000000000000001', }; await expect(loadEvmContract(artifacts, network)).rejects.toBeDefined(); }); it('honors forced service and does not fallback on failure', async () => { const network: TypedEvmNetworkConfig = { id: 'ethereum-mainnet', name: 'Ethereum', network: 'mainnet', type: 'mainnet', isTestnet: false, explorerUrl: 'https://etherscan.io', apiUrl: 'https://api.etherscan.io/v2/api', supportsEtherscanV2: true, primaryExplorerApiIdentifier: 'etherscan-v2', chainId: 1, } as unknown as TypedEvmNetworkConfig; // Force sourcify via artifacts and make it fail mockFetch.mockResolvedValueOnce({ ok: false, status: 404, statusText: 'Not Found' }); const artifacts: EvmContractArtifacts = { contractAddress: '0x0000000000000000000000000000000000000001', __forcedProvider: 'sourcify' as unknown as 'sourcify', }; await expect(loadEvmContract(artifacts, network)).rejects.toBeDefined(); }); it('uses UI default provider first when set in user config', async () => { const network: TypedEvmNetworkConfig = { id: 'ethereum-mainnet', name: 'Ethereum', network: 'mainnet', type: 'mainnet', isTestnet: false, explorerUrl: 'https://etherscan.io', apiUrl: 'https://api.etherscan.io/v2/api', supportsEtherscanV2: true, primaryExplorerApiIdentifier: 'etherscan-v2', chainId: 1, } as unknown as TypedEvmNetworkConfig; vi.spyOn(userExplorerConfigService, 'getUserExplorerConfig').mockReturnValue({ isCustom: true, name: 'Custom', defaultProvider: 'sourcify', } as unknown as ReturnType<typeof userExplorerConfigService.getUserExplorerConfig>); // First provider: sourcify success mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ output: { abi: [] }, contractName: 'C' }), }); const artifacts: EvmContractArtifacts = { contractAddress: '0x0000000000000000000000000000000000000001', }; const result = await loadEvmContract(artifacts, network); expect(result.metadata?.fetchedFrom).toContain('sourcify'); }); it('uses app-config default provider when UI default is not set', async () => { const network: TypedEvmNetworkConfig = { id: 'ethereum-mainnet', name: 'Ethereum', network: 'mainnet', type: 'mainnet', isTestnet: false, explorerUrl: 'https://etherscan.io', apiUrl: 'https://api.etherscan.io/v2/api', supportsEtherscanV2: true, primaryExplorerApiIdentifier: 'etherscan-v2', chainId: 1, } as unknown as TypedEvmNetworkConfig; vi.spyOn(userExplorerConfigService, 'getUserExplorerConfig').mockReturnValue( null as unknown as ReturnType<typeof userExplorerConfigService.getUserExplorerConfig> ); vi.spyOn(appConfigService, 'getGlobalServiceParam').mockReturnValue('sourcify'); // First provider per app default: sourcify mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ output: { abi: [] }, contractName: 'C' }), }); const artifacts: EvmContractArtifacts = { contractAddress: '0x0000000000000000000000000000000000000001', }; const result = await loadEvmContract(artifacts, network); expect(result.metadata?.fetchedFrom).toContain('sourcify'); }); });