From 1af67663a5387844b64b23741941ecab0dd0dd21 Mon Sep 17 00:00:00 2001 From: joaoluisam Date: Wed, 18 Mar 2026 16:23:26 +0000 Subject: [PATCH 1/4] Add tempo testnet moderato --- engine/cld/verification/evm/sourcify.go | 2 +- engine/cld/verification/evm/strategy_test.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/engine/cld/verification/evm/sourcify.go b/engine/cld/verification/evm/sourcify.go index 544ceaf6..d5468468 100644 --- a/engine/cld/verification/evm/sourcify.go +++ b/engine/cld/verification/evm/sourcify.go @@ -1,7 +1,7 @@ package evm var sourcifyChainIDs = map[uint64]struct{}{ - 295: {}, 296: {}, 2020: {}, 2021: {}, + 295: {}, 296: {}, 2020: {}, 2021: {}, 42431: {}, } func IsChainSupportedOnSourcify(chainID uint64) bool { diff --git a/engine/cld/verification/evm/strategy_test.go b/engine/cld/verification/evm/strategy_test.go index 8b2ef4ac..53f621d9 100644 --- a/engine/cld/verification/evm/strategy_test.go +++ b/engine/cld/verification/evm/strategy_test.go @@ -33,6 +33,7 @@ func TestGetVerificationStrategy(t *testing.T) { {"Mantle", 5000, StrategyEtherscan}, {"Scroll", 534352, StrategyEtherscan}, {"Sourcify chain", 295, StrategySourcify}, + {"Tempo Testnet Moderato", 42431, StrategySourcify}, {"L2Scan chain", 4200, StrategyL2Scan}, {"SocialScan chain", 688688, StrategySocialScan}, {"Unknown chain", 999999999, StrategyUnknown}, @@ -83,6 +84,7 @@ func TestIsChainSupportedOnSourcify(t *testing.T) { t.Parallel() require.True(t, IsChainSupportedOnSourcify(295)) + require.True(t, IsChainSupportedOnSourcify(42431)) require.False(t, IsChainSupportedOnSourcify(1)) } From 3747fea4bec114cc9479c601e46735eee40c0656 Mon Sep 17 00:00:00 2001 From: joaoluisam Date: Wed, 18 Mar 2026 19:09:02 +0000 Subject: [PATCH 2/4] Add sourcify verifier implmentation --- engine/cld/verification/evm/factory.go | 2 +- engine/cld/verification/evm/factory_test.go | 31 +- .../cld/verification/evm/sourcify_verifier.go | 302 +++++++++++++ .../evm/sourcify_verifier_test.go | 416 ++++++++++++++++++ 4 files changed, 748 insertions(+), 3 deletions(-) create mode 100644 engine/cld/verification/evm/sourcify_verifier.go create mode 100644 engine/cld/verification/evm/sourcify_verifier_test.go diff --git a/engine/cld/verification/evm/factory.go b/engine/cld/verification/evm/factory.go index c9a12733..6743caf5 100644 --- a/engine/cld/verification/evm/factory.go +++ b/engine/cld/verification/evm/factory.go @@ -48,7 +48,7 @@ func NewVerifier(strategy VerificationStrategy, cfg VerifierConfig) (verificatio case StrategyBlockscout: return newBlockscoutVerifier(cfg) case StrategySourcify: - return nil, errors.New("sourcify verifier not yet implemented") + return newSourcifyVerifier(cfg) case StrategyOkLink: return nil, errors.New("oklink verifier not yet implemented") case StrategyBtrScan: diff --git a/engine/cld/verification/evm/factory_test.go b/engine/cld/verification/evm/factory_test.go index d41a74dc..e89ba5ed 100644 --- a/engine/cld/verification/evm/factory_test.go +++ b/engine/cld/verification/evm/factory_test.go @@ -91,7 +91,7 @@ func TestNewVerifier_StrategyBlockscout(t *testing.T) { require.Equal(t, "chain ID 1 is not supported by the Blockscout API", err.Error()) } -func TestNewVerifier_StrategySourcify(t *testing.T) { +func TestNewVerifier_StrategySourcify_UnsupportedChain(t *testing.T) { t.Parallel() chain, ok := chainsel.ChainBySelector(chainsel.ETHEREUM_MAINNET.Selector) @@ -107,7 +107,34 @@ func TestNewVerifier_StrategySourcify(t *testing.T) { Logger: logger.Nop(), }) require.Error(t, err) - require.Equal(t, "sourcify verifier not yet implemented", err.Error()) + require.Contains(t, err.Error(), "not supported by Sourcify") +} + +func TestNewVerifier_StrategySourcify(t *testing.T) { + t.Parallel() + + chain := chainsel.Chain{ + EvmChainID: 295, + Selector: chainsel.HEDERA_MAINNET.Selector, + Name: "hedera-mainnet", + } + + v, err := NewVerifier(StrategySourcify, VerifierConfig{ + Chain: chain, + Network: cfgnet.Network{ChainSelector: chain.Selector}, + Address: "0x123", + Metadata: SolidityContractMetadata{ + Version: "0.8.19", + Language: "Solidity", + Name: "Test", + }, + ContractType: "Test", + Version: "1.0.0", + Logger: logger.Nop(), + }) + require.NoError(t, err) + require.NotNil(t, v) + require.Equal(t, "Test 1.0.0 (0x123 on hedera-mainnet)", v.String()) } func TestNewVerifier_StrategyRoutescan(t *testing.T) { diff --git a/engine/cld/verification/evm/sourcify_verifier.go b/engine/cld/verification/evm/sourcify_verifier.go new file mode 100644 index 00000000..56a5fc1a --- /dev/null +++ b/engine/cld/verification/evm/sourcify_verifier.go @@ -0,0 +1,302 @@ +package evm + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + chainsel "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/verification" + "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" +) + +const sourcifyServerURL = "https://sourcify.dev/server" + +func newSourcifyVerifier(cfg VerifierConfig) (verification.Verifiable, error) { + if !IsChainSupportedOnSourcify(cfg.Chain.EvmChainID) { + return nil, fmt.Errorf("chain ID %d is not supported by Sourcify", cfg.Chain.EvmChainID) + } + + baseURL := sourcifyServerURL + if cfg.Network.BlockExplorer.URL != "" { + baseURL = strings.TrimSuffix(cfg.Network.BlockExplorer.URL, "/") + } + + return &sourcifyVerifier{ + chain: cfg.Chain, + baseURL: baseURL, + address: cfg.Address, + metadata: cfg.Metadata, + contractType: cfg.ContractType, + version: cfg.Version, + pollInterval: cfg.PollInterval, + lggr: cfg.Logger, + httpClient: cfg.HTTPClient, + }, nil +} + +type sourcifyVerifier struct { + chain chainsel.Chain + baseURL string + address string + metadata SolidityContractMetadata + contractType string + version string + pollInterval time.Duration + lggr logger.Logger + httpClient *http.Client +} + +func (v *sourcifyVerifier) String() string { + return fmt.Sprintf("%s %s (%s on %s)", v.contractType, v.version, v.address, v.chain.Name) +} + +func (v *sourcifyVerifier) client() *http.Client { + if v.httpClient != nil { + return v.httpClient + } + return http.DefaultClient +} + +// IsVerified checks whether the contract is already verified on Sourcify via +// GET /v2/contract/{chainId}/{address}. +// A 200 with a non-null "match" field means verified; 404 means not verified. +func (v *sourcifyVerifier) IsVerified(ctx context.Context) (bool, error) { + url := fmt.Sprintf("%s/v2/contract/%d/%s", v.baseURL, v.chain.EvmChainID, v.address) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return false, err + } + + resp, err := v.client().Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return false, nil + } + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024)) + return false, fmt.Errorf("sourcify IsVerified: unexpected status %d: %s", resp.StatusCode, string(body)) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return false, err + } + + var result sourcifyContractResponse + if err := json.Unmarshal(body, &result); err != nil { + return false, fmt.Errorf("failed to decode sourcify response: %w", err) + } + + if result.Match != nil { + v.lggr.Infof("Contract %s is already verified on Sourcify (match=%s)", v.address, *result.Match) + return true, nil + } + + return false, nil +} + +// Verify submits the contract for verification on Sourcify and polls until completion. +func (v *sourcifyVerifier) Verify(ctx context.Context) error { + verified, err := v.IsVerified(ctx) + if err != nil { + return fmt.Errorf("failed to check verification status: %w", err) + } + if verified { + v.lggr.Infof("%s is already verified", v.String()) + return nil + } + + verificationID, err := v.submitVerification(ctx) + if err != nil { + return err + } + + v.lggr.Infof("Verification submitted for %s, verificationId=%s", v.String(), verificationID) + + return v.pollVerification(ctx, verificationID) +} + +func (v *sourcifyVerifier) submitVerification(ctx context.Context) (string, error) { + stdJsonInput := map[string]any{ + "language": v.metadata.Language, + "settings": v.metadata.Settings, + "sources": v.metadata.Sources, + } + + contractID := v.buildContractIdentifier() + + reqBody := sourcifyVerifyRequest{ + StdJsonInput: stdJsonInput, + CompilerVersion: v.metadata.Version, + ContractIdentifier: contractID, + } + + jsonData, err := json.Marshal(reqBody) + if err != nil { + return "", fmt.Errorf("failed to marshal verify request: %w", err) + } + + url := fmt.Sprintf("%s/v2/verify/%d/%s", v.baseURL, v.chain.EvmChainID, v.address) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(jsonData)) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := v.client().Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read verify response: %w", err) + } + + if resp.StatusCode == http.StatusConflict { + v.lggr.Infof("%s is already verified on Sourcify (409 conflict)", v.String()) + return "", nil + } + + if resp.StatusCode != http.StatusAccepted { + return "", fmt.Errorf("sourcify verify: unexpected status %d: %s", resp.StatusCode, string(body)) + } + + var verifyResp sourcifyVerifyResponse + if err := json.Unmarshal(body, &verifyResp); err != nil { + return "", fmt.Errorf("failed to decode verify response: %w", err) + } + + return verifyResp.VerificationID, nil +} + +func (v *sourcifyVerifier) pollVerification(ctx context.Context, verificationID string) error { + if verificationID == "" { + return nil + } + + pollDur := v.pollInterval + if pollDur <= 0 { + pollDur = 5 * time.Second + } + + url := fmt.Sprintf("%s/v2/verify/%s", v.baseURL, verificationID) + + for range maxVerificationPollAttempts { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return err + } + + resp, err := v.client().Do(req) + if err != nil { + return fmt.Errorf("failed to poll verification status: %w", err) + } + + body, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return fmt.Errorf("failed to read poll response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("sourcify poll: unexpected status %d: %s", resp.StatusCode, string(body)) + } + + var status sourcifyJobStatus + if err := json.Unmarshal(body, &status); err != nil { + return fmt.Errorf("failed to decode poll response: %w", err) + } + + if !status.IsJobCompleted { + v.lggr.Infof("Verification in progress for %s, checking again in %s", v.String(), pollDur) + select { + case <-time.After(pollDur): + case <-ctx.Done(): + return ctx.Err() + } + continue + } + + if status.Error != nil { + return fmt.Errorf("sourcify verification failed: %s - %s", status.Error.CustomCode, status.Error.Message) + } + + if status.Contract != nil && status.Contract.Match != nil { + v.lggr.Infof("Verification succeeded for %s (match=%s)", v.String(), *status.Contract.Match) + return nil + } + + return fmt.Errorf("sourcify verification completed but contract match is empty") + } + + return fmt.Errorf("verification timed out after %d attempts", maxVerificationPollAttempts) +} + +// buildContractIdentifier constructs "path/to/Contract.sol:ContractName" from metadata. +func (v *sourcifyVerifier) buildContractIdentifier() string { + name := v.metadata.Name + if name == "" { + name = v.contractType + } + + suffix := "/" + name + ".sol" + for source := range v.metadata.Sources { + if strings.HasSuffix(source, suffix) { + return source + ":" + name + } + } + + // Fall back: use the first source key with an exact filename match, or just the first key. + for source := range v.metadata.Sources { + if strings.HasSuffix(source, name+".sol") { + return source + ":" + name + } + } + for source := range v.metadata.Sources { + return source + ":" + name + } + + return name +} + +// Sourcify API types + +type sourcifyVerifyRequest struct { + StdJsonInput map[string]any `json:"stdJsonInput"` + CompilerVersion string `json:"compilerVersion"` + ContractIdentifier string `json:"contractIdentifier"` +} + +type sourcifyVerifyResponse struct { + VerificationID string `json:"verificationId"` +} + +type sourcifyContractResponse struct { + Match *string `json:"match"` +} + +type sourcifyJobStatus struct { + IsJobCompleted bool `json:"isJobCompleted"` + VerificationID string `json:"verificationId"` + Error *sourcifyJobError `json:"error"` + Contract *sourcifyContractResponse `json:"contract"` +} + +type sourcifyJobError struct { + CustomCode string `json:"customCode"` + Message string `json:"message"` +} diff --git a/engine/cld/verification/evm/sourcify_verifier_test.go b/engine/cld/verification/evm/sourcify_verifier_test.go new file mode 100644 index 00000000..17ae9b74 --- /dev/null +++ b/engine/cld/verification/evm/sourcify_verifier_test.go @@ -0,0 +1,416 @@ +package evm + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + + chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + cfgnet "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/config/network" + "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" +) + +func newTestSourcifyChain() chainsel.Chain { + return chainsel.Chain{ + EvmChainID: 295, + Selector: chainsel.HEDERA_MAINNET.Selector, + Name: "hedera-mainnet", + } +} + +func TestSourcifyVerifier_IsVerified_AlreadyVerified(t *testing.T) { + t.Parallel() + + match := "exact_match" + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/v2/contract/295/0xabc") + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(sourcifyContractResponse{Match: &match}) + })) + defer server.Close() + + chain := newTestSourcifyChain() + v, err := newSourcifyVerifier(VerifierConfig{ + Chain: chain, + Network: cfgnet.Network{ChainSelector: chain.Selector, BlockExplorer: cfgnet.BlockExplorer{URL: server.URL}}, + Address: "0xabc", + Metadata: SolidityContractMetadata{ + Name: "Test", + Version: "0.8.19", + Sources: map[string]any{"Test.sol": map[string]any{"content": "contract Test {}"}}, + }, + ContractType: "Test", + Version: "1.0.0", + Logger: logger.Nop(), + HTTPClient: server.Client(), + }) + require.NoError(t, err) + + verified, err := v.(*sourcifyVerifier).IsVerified(context.Background()) + require.NoError(t, err) + require.True(t, verified) +} + +func TestSourcifyVerifier_IsVerified_NotVerified(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + chain := newTestSourcifyChain() + v, err := newSourcifyVerifier(VerifierConfig{ + Chain: chain, + Network: cfgnet.Network{ChainSelector: chain.Selector, BlockExplorer: cfgnet.BlockExplorer{URL: server.URL}}, + Address: "0xabc", + Metadata: SolidityContractMetadata{Name: "Test", Version: "0.8.19"}, + ContractType: "Test", + Version: "1.0.0", + Logger: logger.Nop(), + HTTPClient: server.Client(), + }) + require.NoError(t, err) + + verified, err := v.(*sourcifyVerifier).IsVerified(context.Background()) + require.NoError(t, err) + require.False(t, verified) +} + +func TestSourcifyVerifier_IsVerified_NullMatch(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(sourcifyContractResponse{Match: nil}) + })) + defer server.Close() + + chain := newTestSourcifyChain() + v, err := newSourcifyVerifier(VerifierConfig{ + Chain: chain, + Network: cfgnet.Network{ChainSelector: chain.Selector, BlockExplorer: cfgnet.BlockExplorer{URL: server.URL}}, + Address: "0xabc", + Metadata: SolidityContractMetadata{Name: "Test", Version: "0.8.19"}, + ContractType: "Test", + Version: "1.0.0", + Logger: logger.Nop(), + HTTPClient: server.Client(), + }) + require.NoError(t, err) + + verified, err := v.(*sourcifyVerifier).IsVerified(context.Background()) + require.NoError(t, err) + require.False(t, verified) +} + +func TestSourcifyVerifier_Verify_AlreadyVerified(t *testing.T) { + t.Parallel() + + match := "match" + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(sourcifyContractResponse{Match: &match}) + })) + defer server.Close() + + chain := newTestSourcifyChain() + v, err := newSourcifyVerifier(VerifierConfig{ + Chain: chain, + Network: cfgnet.Network{ChainSelector: chain.Selector, BlockExplorer: cfgnet.BlockExplorer{URL: server.URL}}, + Address: "0xabc", + Metadata: SolidityContractMetadata{ + Name: "Test", + Version: "0.8.19", + Language: "Solidity", + Sources: map[string]any{"Test.sol": map[string]any{"content": "contract Test {}"}}, + }, + ContractType: "Test", + Version: "1.0.0", + Logger: logger.Nop(), + HTTPClient: server.Client(), + }) + require.NoError(t, err) + + err = v.Verify(context.Background()) + require.NoError(t, err) +} + +func TestSourcifyVerifier_Verify_SubmitAndPoll(t *testing.T) { + t.Parallel() + + var calls atomic.Int32 + match := "exact_match" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + callNum := calls.Add(1) + w.Header().Set("Content-Type", "application/json") + + switch { + case r.Method == http.MethodGet && callNum == 1: + // IsVerified check -> not found + w.WriteHeader(http.StatusNotFound) + + case r.Method == http.MethodPost: + // Submit verification + var body sourcifyVerifyRequest + _ = json.NewDecoder(r.Body).Decode(&body) + assert.Equal(t, "0.8.19", body.CompilerVersion) + assert.Equal(t, "contracts/Test.sol:Test", body.ContractIdentifier) + assert.NotNil(t, body.StdJsonInput) + + w.WriteHeader(http.StatusAccepted) + _ = json.NewEncoder(w).Encode(sourcifyVerifyResponse{VerificationID: "test-uuid-123"}) + + case r.Method == http.MethodGet && callNum == 3: + // First poll -> still processing + _ = json.NewEncoder(w).Encode(sourcifyJobStatus{ + IsJobCompleted: false, + VerificationID: "test-uuid-123", + }) + + case r.Method == http.MethodGet && callNum == 4: + // Second poll -> done + _ = json.NewEncoder(w).Encode(sourcifyJobStatus{ + IsJobCompleted: true, + VerificationID: "test-uuid-123", + Contract: &sourcifyContractResponse{Match: &match}, + }) + } + })) + defer server.Close() + + chain := newTestSourcifyChain() + v, err := newSourcifyVerifier(VerifierConfig{ + Chain: chain, + Network: cfgnet.Network{ChainSelector: chain.Selector, BlockExplorer: cfgnet.BlockExplorer{URL: server.URL}}, + Address: "0xabc", + Metadata: SolidityContractMetadata{ + Name: "Test", + Version: "0.8.19", + Language: "Solidity", + Sources: map[string]any{"contracts/Test.sol": map[string]any{"content": "contract Test {}"}}, + Settings: map[string]any{"optimizer": map[string]any{"enabled": false}}, + }, + ContractType: "Test", + Version: "1.0.0", + PollInterval: 1, // 1ns for fast tests + Logger: logger.Nop(), + HTTPClient: server.Client(), + }) + require.NoError(t, err) + + err = v.Verify(context.Background()) + require.NoError(t, err) +} + +func TestSourcifyVerifier_Verify_Conflict(t *testing.T) { + t.Parallel() + + var calls atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + callNum := calls.Add(1) + w.Header().Set("Content-Type", "application/json") + + if callNum == 1 { + // IsVerified -> not found + w.WriteHeader(http.StatusNotFound) + return + } + // Submit -> 409 already verified + w.WriteHeader(http.StatusConflict) + _ = json.NewEncoder(w).Encode(map[string]string{ + "customCode": "already_verified", + "message": "Already verified", + }) + })) + defer server.Close() + + chain := newTestSourcifyChain() + v, err := newSourcifyVerifier(VerifierConfig{ + Chain: chain, + Network: cfgnet.Network{ChainSelector: chain.Selector, BlockExplorer: cfgnet.BlockExplorer{URL: server.URL}}, + Address: "0xabc", + Metadata: SolidityContractMetadata{ + Name: "Test", + Version: "0.8.19", + Language: "Solidity", + Sources: map[string]any{"Test.sol": map[string]any{"content": "contract Test {}"}}, + }, + ContractType: "Test", + Version: "1.0.0", + Logger: logger.Nop(), + HTTPClient: server.Client(), + }) + require.NoError(t, err) + + err = v.Verify(context.Background()) + require.NoError(t, err) +} + +func TestSourcifyVerifier_Verify_Failed(t *testing.T) { + t.Parallel() + + var calls atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + callNum := calls.Add(1) + w.Header().Set("Content-Type", "application/json") + + switch { + case callNum == 1: + w.WriteHeader(http.StatusNotFound) + case r.Method == http.MethodPost: + w.WriteHeader(http.StatusAccepted) + _ = json.NewEncoder(w).Encode(sourcifyVerifyResponse{VerificationID: "fail-uuid"}) + default: + _ = json.NewEncoder(w).Encode(sourcifyJobStatus{ + IsJobCompleted: true, + VerificationID: "fail-uuid", + Error: &sourcifyJobError{ + CustomCode: "no_match", + Message: "The onchain and recompiled bytecodes don't match.", + }, + }) + } + })) + defer server.Close() + + chain := newTestSourcifyChain() + v, err := newSourcifyVerifier(VerifierConfig{ + Chain: chain, + Network: cfgnet.Network{ChainSelector: chain.Selector, BlockExplorer: cfgnet.BlockExplorer{URL: server.URL}}, + Address: "0xabc", + Metadata: SolidityContractMetadata{ + Name: "Test", + Version: "0.8.19", + Language: "Solidity", + Sources: map[string]any{"Test.sol": map[string]any{"content": "contract Test {}"}}, + }, + ContractType: "Test", + Version: "1.0.0", + PollInterval: 1, + Logger: logger.Nop(), + HTTPClient: server.Client(), + }) + require.NoError(t, err) + + err = v.Verify(context.Background()) + require.Error(t, err) + require.Contains(t, err.Error(), "no_match") +} + +func TestSourcifyVerifier_String(t *testing.T) { + t.Parallel() + + chain := newTestSourcifyChain() + v, err := newSourcifyVerifier(VerifierConfig{ + Chain: chain, + Network: cfgnet.Network{ChainSelector: chain.Selector}, + Address: "0xabc", + Metadata: SolidityContractMetadata{}, + ContractType: "MyContract", + Version: "2.0.0", + Logger: logger.Nop(), + }) + require.NoError(t, err) + require.Equal(t, "MyContract 2.0.0 (0xabc on hedera-mainnet)", v.String()) +} + +func TestSourcifyVerifier_BuildContractIdentifier(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + metadata SolidityContractMetadata + expected string + }{ + { + name: "exact path match", + metadata: SolidityContractMetadata{ + Name: "Storage", + Sources: map[string]any{"contracts/Storage.sol": map[string]any{"content": "..."}}, + }, + expected: "contracts/Storage.sol:Storage", + }, + { + name: "fallback to contract type when name empty", + metadata: SolidityContractMetadata{ + Sources: map[string]any{"src/Foo.sol": map[string]any{"content": "..."}}, + }, + expected: "src/Foo.sol:Test", + }, + { + name: "name only when no sources", + metadata: SolidityContractMetadata{ + Name: "Standalone", + Sources: map[string]any{}, + }, + expected: "Standalone", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + chain := newTestSourcifyChain() + sv := &sourcifyVerifier{ + chain: chain, + metadata: tt.metadata, + contractType: "Test", + } + got := sv.buildContractIdentifier() + require.Equal(t, tt.expected, got) + }) + } +} + +func TestSourcifyVerifier_CustomBaseURL(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/v2/contract/295/0xabc") + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + chain := newTestSourcifyChain() + v, err := newSourcifyVerifier(VerifierConfig{ + Chain: chain, + Network: cfgnet.Network{ChainSelector: chain.Selector, BlockExplorer: cfgnet.BlockExplorer{URL: server.URL}}, + Address: "0xabc", + Metadata: SolidityContractMetadata{Name: "Test", Version: "0.8.19"}, + ContractType: "Test", + Version: "1.0.0", + Logger: logger.Nop(), + HTTPClient: server.Client(), + }) + require.NoError(t, err) + + verified, err := v.(*sourcifyVerifier).IsVerified(context.Background()) + require.NoError(t, err) + require.False(t, verified) +} + +func TestSourcifyVerifier_DefaultBaseURL(t *testing.T) { + t.Parallel() + + chain := newTestSourcifyChain() + v, err := newSourcifyVerifier(VerifierConfig{ + Chain: chain, + Network: cfgnet.Network{ChainSelector: chain.Selector}, + Address: "0xabc", + Metadata: SolidityContractMetadata{}, + ContractType: "Test", + Version: "1.0.0", + Logger: logger.Nop(), + }) + require.NoError(t, err) + require.Equal(t, sourcifyServerURL, v.(*sourcifyVerifier).baseURL) +} From 0dd152a0da9e72e088d861ceed7f7c75d747daae Mon Sep 17 00:00:00 2001 From: joaoluisam Date: Wed, 18 Mar 2026 19:40:29 +0000 Subject: [PATCH 3/4] fix --- engine/cld/verification/evm/sourcify.go | 13 ++++ .../cld/verification/evm/sourcify_verifier.go | 12 ++-- .../evm/sourcify_verifier_test.go | 69 ++++++++++++++++++- 3 files changed, 87 insertions(+), 7 deletions(-) diff --git a/engine/cld/verification/evm/sourcify.go b/engine/cld/verification/evm/sourcify.go index d5468468..50d0a4f6 100644 --- a/engine/cld/verification/evm/sourcify.go +++ b/engine/cld/verification/evm/sourcify.go @@ -4,7 +4,20 @@ var sourcifyChainIDs = map[uint64]struct{}{ 295: {}, 296: {}, 2020: {}, 2021: {}, 42431: {}, } +// sourcifyCustomServerURLs maps chain IDs to custom Sourcify-compatible server URLs. +// Chains not listed here fall back to the default sourcifyServerURL. +var sourcifyCustomServerURLs = map[uint64]string{ + 42431: "https://contracts.tempo.xyz", +} + func IsChainSupportedOnSourcify(chainID uint64) bool { _, ok := sourcifyChainIDs[chainID] return ok } + +func getSourcifyServerURL(chainID uint64) string { + if url, ok := sourcifyCustomServerURLs[chainID]; ok { + return url + } + return sourcifyServerURL +} diff --git a/engine/cld/verification/evm/sourcify_verifier.go b/engine/cld/verification/evm/sourcify_verifier.go index 56a5fc1a..7906a84c 100644 --- a/engine/cld/verification/evm/sourcify_verifier.go +++ b/engine/cld/verification/evm/sourcify_verifier.go @@ -23,7 +23,7 @@ func newSourcifyVerifier(cfg VerifierConfig) (verification.Verifiable, error) { return nil, fmt.Errorf("chain ID %d is not supported by Sourcify", cfg.Chain.EvmChainID) } - baseURL := sourcifyServerURL + baseURL := getSourcifyServerURL(cfg.Chain.EvmChainID) if cfg.Network.BlockExplorer.URL != "" { baseURL = strings.TrimSuffix(cfg.Network.BlockExplorer.URL, "/") } @@ -246,21 +246,25 @@ func (v *sourcifyVerifier) pollVerification(ctx context.Context, verificationID return fmt.Errorf("verification timed out after %d attempts", maxVerificationPollAttempts) } -// buildContractIdentifier constructs "path/to/Contract.sol:ContractName" from metadata. +// buildContractIdentifier returns the Sourcify contractIdentifier in "path/to/File.sol:ContractName" format. +// If metadata.Name is already fully qualified (contains ":"), it is used as-is. +// Otherwise we attempt to locate the matching source file in the sources map. func (v *sourcifyVerifier) buildContractIdentifier() string { name := v.metadata.Name if name == "" { name = v.contractType } + if strings.Contains(name, ":") { + return name + } + suffix := "/" + name + ".sol" for source := range v.metadata.Sources { if strings.HasSuffix(source, suffix) { return source + ":" + name } } - - // Fall back: use the first source key with an exact filename match, or just the first key. for source := range v.metadata.Sources { if strings.HasSuffix(source, name+".sol") { return source + ":" + name diff --git a/engine/cld/verification/evm/sourcify_verifier_test.go b/engine/cld/verification/evm/sourcify_verifier_test.go index 17ae9b74..1e752720 100644 --- a/engine/cld/verification/evm/sourcify_verifier_test.go +++ b/engine/cld/verification/evm/sourcify_verifier_test.go @@ -158,12 +158,12 @@ func TestSourcifyVerifier_Verify_SubmitAndPoll(t *testing.T) { w.WriteHeader(http.StatusNotFound) case r.Method == http.MethodPost: - // Submit verification var body sourcifyVerifyRequest _ = json.NewDecoder(r.Body).Decode(&body) assert.Equal(t, "0.8.19", body.CompilerVersion) assert.Equal(t, "contracts/Test.sol:Test", body.ContractIdentifier) assert.NotNil(t, body.StdJsonInput) + assert.Equal(t, "Solidity", body.StdJsonInput["language"]) w.WriteHeader(http.StatusAccepted) _ = json.NewEncoder(w).Encode(sourcifyVerifyResponse{VerificationID: "test-uuid-123"}) @@ -192,7 +192,7 @@ func TestSourcifyVerifier_Verify_SubmitAndPoll(t *testing.T) { Network: cfgnet.Network{ChainSelector: chain.Selector, BlockExplorer: cfgnet.BlockExplorer{URL: server.URL}}, Address: "0xabc", Metadata: SolidityContractMetadata{ - Name: "Test", + Name: "contracts/Test.sol:Test", Version: "0.8.19", Language: "Solidity", Sources: map[string]any{"contracts/Test.sol": map[string]any{"content": "contract Test {}"}}, @@ -331,7 +331,28 @@ func TestSourcifyVerifier_BuildContractIdentifier(t *testing.T) { expected string }{ { - name: "exact path match", + name: "fully qualified name used as-is", + metadata: SolidityContractMetadata{ + Name: "contracts/pools/BurnMintTokenPool.sol:BurnMintTokenPool", + Sources: map[string]any{ + "contracts/pools/BurnMintTokenPool.sol": map[string]any{"content": "..."}, + "node_modules/@openzeppelin/contracts/access/AccessControl.sol": map[string]any{"content": "..."}, + }, + }, + expected: "contracts/pools/BurnMintTokenPool.sol:BurnMintTokenPool", + }, + { + name: "fully qualified name with different source path", + metadata: SolidityContractMetadata{ + Name: "src/v0.8/shared/test/helpers/BurnMintERC20WithDrip.sol:BurnMintERC20WithDrip", + Sources: map[string]any{ + "src/v0.8/shared/test/helpers/BurnMintERC20WithDrip.sol": map[string]any{"content": "..."}, + }, + }, + expected: "src/v0.8/shared/test/helpers/BurnMintERC20WithDrip.sol:BurnMintERC20WithDrip", + }, + { + name: "plain name matched from sources", metadata: SolidityContractMetadata{ Name: "Storage", Sources: map[string]any{"contracts/Storage.sol": map[string]any{"content": "..."}}, @@ -414,3 +435,45 @@ func TestSourcifyVerifier_DefaultBaseURL(t *testing.T) { require.NoError(t, err) require.Equal(t, sourcifyServerURL, v.(*sourcifyVerifier).baseURL) } + +func TestSourcifyVerifier_TempoCustomURL(t *testing.T) { + t.Parallel() + + chain := chainsel.Chain{ + EvmChainID: 42431, + Selector: 1, + Name: "tempo-testnet-moderato", + } + v, err := newSourcifyVerifier(VerifierConfig{ + Chain: chain, + Network: cfgnet.Network{ChainSelector: chain.Selector}, + Address: "0xabc", + Metadata: SolidityContractMetadata{}, + ContractType: "Test", + Version: "1.0.0", + Logger: logger.Nop(), + }) + require.NoError(t, err) + require.Equal(t, "https://contracts.tempo.xyz", v.(*sourcifyVerifier).baseURL) +} + +func TestSourcifyVerifier_BlockExplorerURLOverridesCustom(t *testing.T) { + t.Parallel() + + chain := chainsel.Chain{ + EvmChainID: 42431, + Selector: 1, + Name: "tempo-testnet-moderato", + } + v, err := newSourcifyVerifier(VerifierConfig{ + Chain: chain, + Network: cfgnet.Network{ChainSelector: chain.Selector, BlockExplorer: cfgnet.BlockExplorer{URL: "https://custom.sourcify.example"}}, + Address: "0xabc", + Metadata: SolidityContractMetadata{}, + ContractType: "Test", + Version: "1.0.0", + Logger: logger.Nop(), + }) + require.NoError(t, err) + require.Equal(t, "https://custom.sourcify.example", v.(*sourcifyVerifier).baseURL) +} From f0a1fec4b8c167e64e35556a15e1be80518d7697 Mon Sep 17 00:00:00 2001 From: joaoluisam Date: Tue, 31 Mar 2026 12:01:41 +0100 Subject: [PATCH 4/4] add tempo mainnet --- engine/cld/verification/evm/sourcify.go | 3 ++- .../evm/sourcify_verifier_test.go | 21 +++++++++++++++++++ engine/cld/verification/evm/strategy_test.go | 2 ++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/engine/cld/verification/evm/sourcify.go b/engine/cld/verification/evm/sourcify.go index 50d0a4f6..1b3589f8 100644 --- a/engine/cld/verification/evm/sourcify.go +++ b/engine/cld/verification/evm/sourcify.go @@ -1,12 +1,13 @@ package evm var sourcifyChainIDs = map[uint64]struct{}{ - 295: {}, 296: {}, 2020: {}, 2021: {}, 42431: {}, + 295: {}, 296: {}, 2020: {}, 2021: {}, 4217: {}, 42431: {}, } // sourcifyCustomServerURLs maps chain IDs to custom Sourcify-compatible server URLs. // Chains not listed here fall back to the default sourcifyServerURL. var sourcifyCustomServerURLs = map[uint64]string{ + 4217: "https://contracts.tempo.xyz", 42431: "https://contracts.tempo.xyz", } diff --git a/engine/cld/verification/evm/sourcify_verifier_test.go b/engine/cld/verification/evm/sourcify_verifier_test.go index 1e752720..f03cd150 100644 --- a/engine/cld/verification/evm/sourcify_verifier_test.go +++ b/engine/cld/verification/evm/sourcify_verifier_test.go @@ -457,6 +457,27 @@ func TestSourcifyVerifier_TempoCustomURL(t *testing.T) { require.Equal(t, "https://contracts.tempo.xyz", v.(*sourcifyVerifier).baseURL) } +func TestSourcifyVerifier_TempoMainnetCustomURL(t *testing.T) { + t.Parallel() + + chain := chainsel.Chain{ + EvmChainID: 4217, + Selector: 7281642695469137430, + Name: "tempo-mainnet", + } + v, err := newSourcifyVerifier(VerifierConfig{ + Chain: chain, + Network: cfgnet.Network{ChainSelector: chain.Selector}, + Address: "0xabc", + Metadata: SolidityContractMetadata{}, + ContractType: "Test", + Version: "1.0.0", + Logger: logger.Nop(), + }) + require.NoError(t, err) + require.Equal(t, "https://contracts.tempo.xyz", v.(*sourcifyVerifier).baseURL) +} + func TestSourcifyVerifier_BlockExplorerURLOverridesCustom(t *testing.T) { t.Parallel() diff --git a/engine/cld/verification/evm/strategy_test.go b/engine/cld/verification/evm/strategy_test.go index 53f621d9..9404a653 100644 --- a/engine/cld/verification/evm/strategy_test.go +++ b/engine/cld/verification/evm/strategy_test.go @@ -33,6 +33,7 @@ func TestGetVerificationStrategy(t *testing.T) { {"Mantle", 5000, StrategyEtherscan}, {"Scroll", 534352, StrategyEtherscan}, {"Sourcify chain", 295, StrategySourcify}, + {"Tempo Mainnet", 4217, StrategySourcify}, {"Tempo Testnet Moderato", 42431, StrategySourcify}, {"L2Scan chain", 4200, StrategyL2Scan}, {"SocialScan chain", 688688, StrategySocialScan}, @@ -84,6 +85,7 @@ func TestIsChainSupportedOnSourcify(t *testing.T) { t.Parallel() require.True(t, IsChainSupportedOnSourcify(295)) + require.True(t, IsChainSupportedOnSourcify(4217)) require.True(t, IsChainSupportedOnSourcify(42431)) require.False(t, IsChainSupportedOnSourcify(1)) }