diff --git a/.changeset/fair-kiwis-act.md b/.changeset/fair-kiwis-act.md new file mode 100644 index 00000000..e1a32057 --- /dev/null +++ b/.changeset/fair-kiwis-act.md @@ -0,0 +1,5 @@ +--- +"chainlink-deployments-framework": minor +--- + +feat: adds pre hook contract verification check diff --git a/engine/cld/hooks/verification/require_verified.go b/engine/cld/hooks/verification/require_verified.go new file mode 100644 index 00000000..f3c7d1a0 --- /dev/null +++ b/engine/cld/hooks/verification/require_verified.go @@ -0,0 +1,104 @@ +// Package verification provides pre-hooks that enforce contract verification +// before changeset execution. Use RequireVerified to block changesets when +// referenced contracts must be verified on block explorers first. +package verification + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/changeset" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/config" + fdomain "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/domain" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/verification/evm" +) + +const hookTimeout = 60 * time.Second + +// RequireVerifiedOption configures RequireVerified behavior. +type RequireVerifiedOption func(*requireVerifiedOpts) + +type requireVerifiedOpts struct { + httpClient *http.Client +} + +// WithHTTPClient sets the HTTP client for block explorer API calls. Use for testing. +func WithHTTPClient(c *http.Client) RequireVerifiedOption { + return func(o *requireVerifiedOpts) { + o.httpClient = c + } +} + +// RefsProvider returns the address refs to check for a given changeset. +// Return nil or empty slice to skip verification for that changeset. +type RefsProvider func(params changeset.PreHookParams) ([]datastore.AddressRef, error) + +// RefsForChangeset returns a RefsProvider that looks up refs by changeset key. +// Use this when each changeset has a fixed set of contracts to verify. +func RefsForChangeset(m map[string][]datastore.AddressRef) RefsProvider { + return func(params changeset.PreHookParams) ([]datastore.AddressRef, error) { + return m[params.ChangesetKey], nil + } +} + +// RequireVerified returns a PreHook that blocks changeset execution when any +// of the provided refs are not verified on block explorers. Uses evm.CheckVerified +// under the hood. Skips when refsProvider returns no refs. +// +// Requires domain (for loading network config), a refsProvider (to get refs per +// changeset), and a ContractInputsProvider (for contract metadata). Uses Abort +// policy with a 60s timeout. +func RequireVerified( + domain fdomain.Domain, + refsProvider RefsProvider, + contractInputsProvider evm.ContractInputsProvider, + opts ...RequireVerifiedOption, +) changeset.PreHook { + var o requireVerifiedOpts + for _, opt := range opts { + opt(&o) + } + + return changeset.PreHook{ + HookDefinition: changeset.HookDefinition{ + Name: "require-verified", + FailurePolicy: changeset.Abort, + Timeout: hookTimeout, + }, + Func: func(ctx context.Context, params changeset.PreHookParams) error { + refs, err := refsProvider(params) + if err != nil { + return fmt.Errorf("require-verified: get refs: %w", err) + } + if len(refs) == 0 { + return nil + } + + networkCfg, err := config.LoadNetworks(params.Env.Name, domain, params.Env.Logger) + if err != nil { + return fmt.Errorf("require-verified: load networks: %w", err) + } + + checkCfg := evm.CheckConfig{ + ContractInputsProvider: contractInputsProvider, + NetworkConfig: networkCfg, + Logger: params.Env.Logger, + } + if o.httpClient != nil { + checkCfg.HTTPClient = o.httpClient + } + unverified, err := evm.CheckVerified(ctx, refs, checkCfg) + if err != nil { + return fmt.Errorf("require-verified: %w", err) + } + if len(unverified) > 0 { + return fmt.Errorf("require-verified: %d contract(s) not verified: %v", len(unverified), unverified) + } + + return nil + }, + } +} diff --git a/engine/cld/hooks/verification/require_verified_test.go b/engine/cld/hooks/verification/require_verified_test.go new file mode 100644 index 00000000..8e8375d9 --- /dev/null +++ b/engine/cld/hooks/verification/require_verified_test.go @@ -0,0 +1,236 @@ +package verification + +import ( + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "testing" + + "github.com/Masterminds/semver/v3" + "github.com/stretchr/testify/require" + + chainsel "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/changeset" + fdomain "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/domain" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/verification/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" +) + +func TestRefsForChangeset(t *testing.T) { + t.Parallel() + + ref1 := datastore.AddressRef{Address: "0x1", ChainSelector: 1, Type: "A", Version: semver.MustParse("1.0.0")} + ref2 := datastore.AddressRef{Address: "0x2", ChainSelector: 2, Type: "B", Version: semver.MustParse("2.0.0")} + + provider := RefsForChangeset(map[string][]datastore.AddressRef{ + "cs-a": {ref1}, + "cs-b": {ref2}, + "cs-both": {ref1, ref2}, + }) + + t.Run("returns refs for known key", func(t *testing.T) { + t.Parallel() + params := changeset.PreHookParams{ChangesetKey: "cs-a"} + refs, err := provider(params) + require.NoError(t, err) + require.Len(t, refs, 1) + require.Equal(t, "0x1", refs[0].Address) + }) + + t.Run("returns empty for unknown key", func(t *testing.T) { + t.Parallel() + params := changeset.PreHookParams{ChangesetKey: "unknown"} + refs, err := provider(params) + require.NoError(t, err) + require.Empty(t, refs) + }) + + t.Run("returns multiple refs", func(t *testing.T) { + t.Parallel() + params := changeset.PreHookParams{ChangesetKey: "cs-both"} + refs, err := provider(params) + require.NoError(t, err) + require.Len(t, refs, 2) + }) +} + +func TestRequireVerified_EmptyRefsSkips(t *testing.T) { + t.Parallel() + + dom := fdomain.NewDomain(t.TempDir(), "test") + provider := RefsForChangeset(map[string][]datastore.AddressRef{ + "my-cs": {}, // empty - should skip + }) + hook := RequireVerified(dom, provider, &mockContractInputsProvider{}) + + params := changeset.PreHookParams{ + Env: changeset.HookEnv{Name: "staging", Logger: logger.Nop()}, + ChangesetKey: "my-cs", + } + err := hook.Func(t.Context(), params) + require.NoError(t, err) +} + +func TestRequireVerified_RefsProviderError(t *testing.T) { + t.Parallel() + + dom := fdomain.NewDomain(t.TempDir(), "test") + provider := func(changeset.PreHookParams) ([]datastore.AddressRef, error) { + return nil, errors.New("refs lookup failed") + } + hook := RequireVerified(dom, provider, &mockContractInputsProvider{}) + + params := changeset.PreHookParams{ + Env: changeset.HookEnv{Name: "staging", Logger: logger.Nop()}, + ChangesetKey: "my-cs", + } + err := hook.Func(t.Context(), params) + require.Error(t, err) + require.Contains(t, err.Error(), "require-verified: get refs") + require.Contains(t, err.Error(), "refs lookup failed") +} + +func TestRequireVerified_HookDefinition(t *testing.T) { + t.Parallel() + + dom := fdomain.NewDomain(t.TempDir(), "test") + hook := RequireVerified(dom, RefsForChangeset(map[string][]datastore.AddressRef{}), &mockContractInputsProvider{}) + + require.Equal(t, "require-verified", hook.Name) + require.Equal(t, changeset.Abort, hook.FailurePolicy) + require.NotZero(t, hook.Timeout) +} + +func TestRequireVerified_FullFlow_AllVerified(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]string{ + "status": "1", + "message": "OK", + "result": `[{"type":"constructor"}]`, + }) + })) + defer server.Close() + + targetURL, _ := url.Parse(server.URL) + httpClient := &http.Client{Transport: &redirectTransport{target: targetURL}} + + dom := setupDomainWithNetworks(t) + ethSelector := chainsel.ETHEREUM_MAINNET.Selector + refs := []datastore.AddressRef{ + {Address: "0xVerified", ChainSelector: ethSelector, Type: "LinkToken", Version: semver.MustParse("1.0.0")}, + } + provider := RefsForChangeset(map[string][]datastore.AddressRef{"my-cs": refs}) + hook := RequireVerified(dom, provider, &mockContractInputsProvider{}, WithHTTPClient(httpClient)) + + params := changeset.PreHookParams{ + Env: changeset.HookEnv{Name: "staging", Logger: logger.Nop()}, + ChangesetKey: "my-cs", + } + err := hook.Func(t.Context(), params) + require.NoError(t, err) +} + +func TestRequireVerified_FullFlow_UnverifiedFails(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]string{ + "status": "1", + "message": "OK", + "result": "Contract source code not verified", + }) + })) + defer server.Close() + + targetURL, _ := url.Parse(server.URL) + httpClient := &http.Client{Transport: &redirectTransport{target: targetURL}} + + dom := setupDomainWithNetworks(t) + ethSelector := chainsel.ETHEREUM_MAINNET.Selector + refs := []datastore.AddressRef{ + {Address: "0xUnverified", ChainSelector: ethSelector, Type: "LinkToken", Version: semver.MustParse("1.0.0")}, + } + provider := RefsForChangeset(map[string][]datastore.AddressRef{"my-cs": refs}) + hook := RequireVerified(dom, provider, &mockContractInputsProvider{}, WithHTTPClient(httpClient)) + + params := changeset.PreHookParams{ + Env: changeset.HookEnv{Name: "staging", Logger: logger.Nop()}, + ChangesetKey: "my-cs", + } + err := hook.Func(t.Context(), params) + require.Error(t, err) + require.Contains(t, err.Error(), "require-verified:") + require.Contains(t, err.Error(), "not verified") +} + +// setupDomainWithNetworks creates a domain with .config/domain.yaml and .config/networks/*.yaml +// so config.LoadNetworks works. +func setupDomainWithNetworks(t *testing.T) fdomain.Domain { + t.Helper() + + rootDir := t.TempDir() + domainKey := "test-domain" + domainDir := filepath.Join(rootDir, domainKey) + configDir := filepath.Join(domainDir, ".config") + networksDir := filepath.Join(configDir, "networks") + + require.NoError(t, os.MkdirAll(networksDir, 0700)) + + domainYAML := `environments: + staging: + network_types: + - mainnet +` + require.NoError(t, os.WriteFile(filepath.Join(configDir, "domain.yaml"), []byte(domainYAML), 0600)) + + // Ethereum mainnet with block explorer - etherscan needs API key for NewVerifier + networksYAML := `networks: + - type: mainnet + chain_selector: 5009297550715157269 + block_explorer: + type: Etherscan + api_key: test-key + url: https://etherscan.io + rpcs: + - rpc_name: mainnet-rpc + preferred_url_scheme: http + http_url: https://eth.llamarpc.com + ws_url: wss://eth.llamarpc.com +` + require.NoError(t, os.WriteFile(filepath.Join(networksDir, "networks-mainnet.yaml"), []byte(networksYAML), 0600)) + + return fdomain.NewDomain(rootDir, domainKey) +} + +// redirectTransport redirects HTTP requests to a target URL for testing. +type redirectTransport struct { + target *url.URL +} + +func (r *redirectTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req.URL.Scheme = r.target.Scheme + req.URL.Host = r.target.Host + + return http.DefaultTransport.RoundTrip(req) +} + +// mockContractInputsProvider is a test double for evm.ContractInputsProvider. +type mockContractInputsProvider struct{} + +func (m *mockContractInputsProvider) GetInputs(_ datastore.ContractType, _ *semver.Version) (evm.SolidityContractMetadata, error) { + return evm.SolidityContractMetadata{ + Name: "Test", + Version: "0.8.19", + Sources: map[string]any{"test.sol": map[string]any{"content": "contract Test {}"}}, + }, nil +} diff --git a/engine/cld/verification/evm/check.go b/engine/cld/verification/evm/check.go new file mode 100644 index 00000000..7260db10 --- /dev/null +++ b/engine/cld/verification/evm/check.go @@ -0,0 +1,98 @@ +package evm + +import ( + "context" + "errors" + "fmt" + "net/http" + + chain_selectors "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cfgnet "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/config/network" + "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" +) + +// CheckConfig holds the configuration for checking contract verification status. +type CheckConfig struct { + // ContractInputsProvider supplies contract metadata. Required. + ContractInputsProvider ContractInputsProvider + // NetworkConfig is the loaded network configuration (EVM networks only). Required. + NetworkConfig *cfgnet.Config + // Logger for diagnostics. + Logger logger.Logger + // HTTPClient is optional; when nil, verifiers use http.DefaultClient. Set for testing. + HTTPClient *http.Client +} + +// CheckVerified checks which of the given address refs are already verified on block explorers. +// Returns the refs that are NOT verified. Skips refs for chains with no verifier strategy +// (those are not included in the unverified list). Returns an error if any ref cannot be +// checked (e.g. network not in config, metadata unavailable, API error). +// +// Use this in pre-hooks to block changesets when contracts must be verified first. +func CheckVerified(ctx context.Context, refs []datastore.AddressRef, cfg CheckConfig) (unverified []datastore.AddressRef, err error) { + if cfg.ContractInputsProvider == nil { + return nil, errors.New("CheckConfig.ContractInputsProvider is required") + } + if cfg.NetworkConfig == nil { + return nil, errors.New("CheckConfig.NetworkConfig is required") + } + if cfg.Logger == nil { + cfg.Logger = logger.Nop() + } + + evmConfig := cfg.NetworkConfig.FilterWith(cfgnet.ChainFamilyFilter(chain_selectors.FamilyEVM)) + + for _, ref := range refs { + if ref.Version == nil { + return nil, fmt.Errorf("address %s on chain %d: version is required", ref.Address, ref.ChainSelector) + } + + network, err := evmConfig.NetworkBySelector(ref.ChainSelector) + if err != nil { + return nil, fmt.Errorf("address %s: %w", ref.Address, err) + } + + chain, ok := chain_selectors.ChainBySelector(ref.ChainSelector) + if !ok { + return nil, fmt.Errorf("address %s: no chain found for selector %d", ref.Address, ref.ChainSelector) + } + + strategy := GetVerificationStrategy(chain.EvmChainID) + if strategy == StrategyUnknown { + return nil, fmt.Errorf("address %s on %s: no verification strategy for chain ID %d", ref.Address, chain.Name, chain.EvmChainID) + } + + metadata, err := cfg.ContractInputsProvider.GetInputs(ref.Type, ref.Version) + if err != nil { + return nil, fmt.Errorf("address %s: %w", ref.Address, err) + } + + verifier, err := NewVerifier(strategy, VerifierConfig{ + Chain: chain, + Network: network, + Address: ref.Address, + Metadata: metadata, + ContractType: string(ref.Type), + Version: ref.Version.String(), + PollInterval: 0, + Logger: cfg.Logger, + HTTPClient: cfg.HTTPClient, + }) + if err != nil { + return nil, fmt.Errorf("address %s on %s: %w", ref.Address, chain.Name, err) + } + + verified, err := verifier.IsVerified(ctx) + if err != nil { + return nil, fmt.Errorf("address %s on %s: %w", ref.Address, chain.Name, err) + } + + if !verified { + unverified = append(unverified, ref) + } + } + + return unverified, nil +} diff --git a/engine/cld/verification/evm/check_test.go b/engine/cld/verification/evm/check_test.go new file mode 100644 index 00000000..8162c7d3 --- /dev/null +++ b/engine/cld/verification/evm/check_test.go @@ -0,0 +1,256 @@ +package evm + +import ( + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/Masterminds/semver/v3" + "github.com/stretchr/testify/require" + + chainsel "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cfgnet "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/config/network" + "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" +) + +func TestCheckVerified_ConfigValidation(t *testing.T) { + t.Parallel() + + ethSelector := chainsel.ETHEREUM_MAINNET.Selector + refs := []datastore.AddressRef{ + {Address: "0x123", ChainSelector: ethSelector, Type: "Test", Version: semver.MustParse("1.0.0")}, + } + networkCfg := cfgnet.NewConfig([]cfgnet.Network{ + { + Type: cfgnet.NetworkTypeMainnet, + ChainSelector: ethSelector, + BlockExplorer: cfgnet.BlockExplorer{APIKey: "test"}, + RPCs: []cfgnet.RPC{{HTTPURL: "https://eth.llamarpc.com"}}, + }, + }) + + t.Run("nil ContractInputsProvider", func(t *testing.T) { + t.Parallel() + unverified, err := CheckVerified(t.Context(), refs, CheckConfig{ + ContractInputsProvider: nil, + NetworkConfig: networkCfg, + Logger: logger.Nop(), + }) + require.Error(t, err) + require.Nil(t, unverified) + require.Contains(t, err.Error(), "ContractInputsProvider is required") + }) + + t.Run("nil NetworkConfig", func(t *testing.T) { + t.Parallel() + unverified, err := CheckVerified(t.Context(), refs, CheckConfig{ + ContractInputsProvider: &mockContractInputsProvider{}, + NetworkConfig: nil, + Logger: logger.Nop(), + }) + require.Error(t, err) + require.Nil(t, unverified) + require.Contains(t, err.Error(), "NetworkConfig is required") + }) +} + +func TestCheckVerified_NilVersion(t *testing.T) { + t.Parallel() + + ethSelector := chainsel.ETHEREUM_MAINNET.Selector + refs := []datastore.AddressRef{ + {Address: "0x123", ChainSelector: ethSelector, Type: "Test", Version: nil}, + } + networkCfg := cfgnet.NewConfig([]cfgnet.Network{ + { + Type: cfgnet.NetworkTypeMainnet, + ChainSelector: ethSelector, + BlockExplorer: cfgnet.BlockExplorer{APIKey: "test"}, + RPCs: []cfgnet.RPC{{HTTPURL: "https://eth.llamarpc.com"}}, + }, + }) + + unverified, err := CheckVerified(t.Context(), refs, CheckConfig{ + ContractInputsProvider: &mockContractInputsProvider{}, + NetworkConfig: networkCfg, + Logger: logger.Nop(), + }) + require.Error(t, err) + require.Nil(t, unverified) + require.Contains(t, err.Error(), "version is required") +} + +func TestCheckVerified_NetworkNotFound(t *testing.T) { + t.Parallel() + + ethSelector := chainsel.ETHEREUM_MAINNET.Selector + refs := []datastore.AddressRef{ + {Address: "0x123", ChainSelector: ethSelector, Type: "Test", Version: semver.MustParse("1.0.0")}, + } + // Empty network config - eth mainnet not in config + networkCfg := cfgnet.NewConfig([]cfgnet.Network{}) + + unverified, err := CheckVerified(t.Context(), refs, CheckConfig{ + ContractInputsProvider: &mockContractInputsProvider{}, + NetworkConfig: networkCfg, + Logger: logger.Nop(), + }) + require.Error(t, err) + require.Nil(t, unverified) + require.Contains(t, err.Error(), "not found in configuration") +} + +func TestCheckVerified_MetadataProviderError(t *testing.T) { + t.Parallel() + + ethSelector := chainsel.ETHEREUM_MAINNET.Selector + refs := []datastore.AddressRef{ + {Address: "0x123", ChainSelector: ethSelector, Type: "Test", Version: semver.MustParse("1.0.0")}, + } + networkCfg := cfgnet.NewConfig([]cfgnet.Network{ + { + Type: cfgnet.NetworkTypeMainnet, + ChainSelector: ethSelector, + BlockExplorer: cfgnet.BlockExplorer{APIKey: "test"}, + RPCs: []cfgnet.RPC{{HTTPURL: "https://eth.llamarpc.com"}}, + }, + }) + + provider := &mockContractInputsProvider{} + provider.getInputsErr = errors.New("metadata unavailable") + + unverified, err := CheckVerified(t.Context(), refs, CheckConfig{ + ContractInputsProvider: provider, + NetworkConfig: networkCfg, + Logger: logger.Nop(), + }) + require.Error(t, err) + require.Nil(t, unverified) + require.Contains(t, err.Error(), "metadata unavailable") +} + +func TestCheckVerified_AllVerified(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(etherscanAPIResponse[string]{ + Status: statusOK, + Message: messageOK, + Result: `[{"type":"constructor"}]`, + }) + })) + defer server.Close() + + targetURL, _ := url.Parse(server.URL) + client := &http.Client{Transport: &redirectTransport{target: targetURL}} + + ethSelector := chainsel.ETHEREUM_MAINNET.Selector + refs := []datastore.AddressRef{ + {Address: "0x123", ChainSelector: ethSelector, Type: "LinkToken", Version: semver.MustParse("1.0.0")}, + } + networkCfg := cfgnet.NewConfig([]cfgnet.Network{ + { + Type: cfgnet.NetworkTypeMainnet, + ChainSelector: ethSelector, + BlockExplorer: cfgnet.BlockExplorer{APIKey: "test"}, + RPCs: []cfgnet.RPC{{HTTPURL: "https://eth.llamarpc.com"}}, + }, + }) + + unverified, err := CheckVerified(t.Context(), refs, CheckConfig{ + ContractInputsProvider: &mockContractInputsProvider{}, + NetworkConfig: networkCfg, + Logger: logger.Nop(), + HTTPClient: client, + }) + require.NoError(t, err) + require.Empty(t, unverified) +} + +func TestCheckVerified_SomeUnverified(t *testing.T) { + t.Parallel() + + callCount := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + callCount++ + w.Header().Set("Content-Type", "application/json") + // First contract verified, second not + if callCount == 1 { + _ = json.NewEncoder(w).Encode(etherscanAPIResponse[string]{ + Status: statusOK, + Message: messageOK, + Result: `[{"type":"constructor"}]`, + }) + } else { + _ = json.NewEncoder(w).Encode(etherscanAPIResponse[string]{ + Status: statusOK, + Message: messageOK, + Result: "Contract source code not verified", + }) + } + })) + defer server.Close() + + targetURL, _ := url.Parse(server.URL) + client := &http.Client{Transport: &redirectTransport{target: targetURL}} + + ethSelector := chainsel.ETHEREUM_MAINNET.Selector + refs := []datastore.AddressRef{ + {Address: "0xVerified", ChainSelector: ethSelector, Type: "LinkToken", Version: semver.MustParse("1.0.0")}, + {Address: "0xUnverified", ChainSelector: ethSelector, Type: "LinkToken", Version: semver.MustParse("1.0.0")}, + } + networkCfg := cfgnet.NewConfig([]cfgnet.Network{ + { + Type: cfgnet.NetworkTypeMainnet, + ChainSelector: ethSelector, + BlockExplorer: cfgnet.BlockExplorer{APIKey: "test"}, + RPCs: []cfgnet.RPC{{HTTPURL: "https://eth.llamarpc.com"}}, + }, + }) + + unverified, err := CheckVerified(t.Context(), refs, CheckConfig{ + ContractInputsProvider: &mockContractInputsProvider{}, + NetworkConfig: networkCfg, + Logger: logger.Nop(), + HTTPClient: client, + }) + require.NoError(t, err) + require.Len(t, unverified, 1) + require.Equal(t, "0xUnverified", unverified[0].Address) +} + +func TestCheckVerified_EmptyRefs(t *testing.T) { + t.Parallel() + + networkCfg := cfgnet.NewConfig([]cfgnet.Network{}) + unverified, err := CheckVerified(t.Context(), nil, CheckConfig{ + ContractInputsProvider: &mockContractInputsProvider{}, + NetworkConfig: networkCfg, + Logger: logger.Nop(), + }) + require.NoError(t, err) + require.Empty(t, unverified) +} + +// mockContractInputsProvider is a test double for ContractInputsProvider. +type mockContractInputsProvider struct { + getInputsErr error +} + +func (m *mockContractInputsProvider) GetInputs(_ datastore.ContractType, _ *semver.Version) (SolidityContractMetadata, error) { + if m.getInputsErr != nil { + return SolidityContractMetadata{}, m.getInputsErr + } + + return SolidityContractMetadata{ + Name: "Test", + Version: "0.8.19", + Sources: map[string]any{"test.sol": map[string]any{"content": "contract Test {}"}}, + }, nil +}