diff --git a/compiler.go b/compiler.go index 9a99acb..b616340 100644 --- a/compiler.go +++ b/compiler.go @@ -7,6 +7,8 @@ import ( "fmt" "os/exec" "strings" + + "go.uber.org/zap" ) // Compiler represents a Solidity compiler instance. @@ -18,6 +20,7 @@ type Compiler struct { } // NewCompiler creates a new Compiler instance with the given context, configuration, and source. +// It returns an error if the provided configuration, solc instance, or source is invalid. func NewCompiler(ctx context.Context, solc *Solc, config *CompilerConfig, source string) (*Compiler, error) { if config == nil { return nil, fmt.Errorf("config must be provided to create new compiler") @@ -31,8 +34,10 @@ func NewCompiler(ctx context.Context, solc *Solc, config *CompilerConfig, source return nil, fmt.Errorf("source code must be provided to create new compiler") } - if err := config.Validate(); err != nil { - return nil, fmt.Errorf("invalid compiler configuration: %w", err) + if config.JsonConfig == nil { + if err := config.Validate(); err != nil { + return nil, fmt.Errorf("invalid compiler configuration: %w", err) + } } return &Compiler{ @@ -64,7 +69,8 @@ func (v *Compiler) GetSources() string { } // Compile compiles the Solidity sources using the configured compiler version and arguments. -func (v *Compiler) Compile() (*CompilerResults, error) { +// It returns the compilation results or an error if the compilation fails. +func (v *Compiler) Compile() ([]*CompilerResults, error) { compilerVersion := v.GetCompilerVersion() if compilerVersion == "" { return nil, fmt.Errorf("no compiler version specified") @@ -82,8 +88,10 @@ func (v *Compiler) Compile() (*CompilerResults, error) { } args = append(args, sanitizedArgs...) - if err := v.config.Validate(); err != nil { - return nil, err + if v.config.JsonConfig == nil { + if err := v.config.Validate(); err != nil { + return nil, err + } } // #nosec G204 @@ -100,6 +108,11 @@ func (v *Compiler) Compile() (*CompilerResults, error) { cmd.Stderr = &stderr if err := cmd.Run(); err != nil { + zap.L().Error( + "Failed to compile Solidity sources", + zap.String("version", compilerVersion), + zap.String("stderr", stderr.String()), + ) var errors []string var warnings []string @@ -117,9 +130,20 @@ func (v *Compiler) Compile() (*CompilerResults, error) { Errors: errors, Warnings: warnings, } - return results, err + return []*CompilerResults{results}, err + } + + if v.config.JsonConfig != nil { + return v.resultsFromJson(compilerVersion, out) } + return v.resultsFromSimple(compilerVersion, out) +} + +// resultsFromSimple parses the output from the solc compiler when the output is in a simple format. +// It extracts the compilation details such as bytecode, ABI, and any errors or warnings. +// The method returns a slice of CompilerResults or an error if the output cannot be parsed. +func (v *Compiler) resultsFromSimple(compilerVersion string, out bytes.Buffer) ([]*CompilerResults, error) { // Parse the output var compilationOutput struct { Contracts map[string]struct { @@ -130,20 +154,74 @@ func (v *Compiler) Compile() (*CompilerResults, error) { Version string `json:"version"` } - err = json.Unmarshal(out.Bytes(), &compilationOutput) - if err != nil { + if err := json.Unmarshal(out.Bytes(), &compilationOutput); err != nil { return nil, err } - // Extract the first contract's results (assuming one contract for simplicity) - var firstContractKey string - for key := range compilationOutput.Contracts { - firstContractKey = key - break + // Separate errors and warnings + var errors, warnings []string + for _, msg := range compilationOutput.Errors { + if strings.Contains(msg, "Warning:") { + warnings = append(warnings, msg) + } else { + errors = append(errors, msg) + } } - if firstContractKey == "" { - return nil, fmt.Errorf("no contracts found") + var results []*CompilerResults + + for key, output := range compilationOutput.Contracts { + isEntryContract := false + if v.config.GetEntrySourceName() != "" && key == ":"+v.config.GetEntrySourceName() { + isEntryContract = true + } + + abi, err := json.Marshal(output.Abi) + if err != nil { + return nil, err + } + + results = append(results, &CompilerResults{ + IsEntryContract: isEntryContract, + RequestedVersion: compilerVersion, + CompilerVersion: compilationOutput.Version, + Bytecode: output.Bin, + ABI: string(abi), + ContractName: strings.TrimLeft(key, ":"), + Errors: errors, + Warnings: warnings, + }) + } + + return results, nil +} + +// resultsFromJson parses the output from the solc compiler when the output is in a JSON format. +// It extracts detailed compilation information including bytecode, ABI, opcodes, and metadata. +// Additionally, it separates any errors and warnings from the compilation process. +// The method returns a slice of CompilerResults or an error if the output cannot be parsed. +func (v *Compiler) resultsFromJson(compilerVersion string, out bytes.Buffer) ([]*CompilerResults, error) { + // Parse the output + var compilationOutput struct { + Contracts map[string]map[string]struct { + Abi interface{} `json:"abi"` + Evm struct { + Bytecode struct { + GeneratedSources []interface{} `json:"generatedSources"` + LinkReferences map[string]interface{} `json:"linkReferences"` + Object string `json:"object"` + Opcodes string `json:"opcodes"` + SourceMap string `json:"sourceMap"` + } `json:"bytecode"` + } `json:"evm"` + Metadata string `json:"metadata"` + } `json:"contracts"` + Errors []string `json:"errors"` + Version string `json:"version"` + } + + if err := json.Unmarshal(out.Bytes(), &compilationOutput); err != nil { + return nil, err } // Separate errors and warnings @@ -156,19 +234,32 @@ func (v *Compiler) Compile() (*CompilerResults, error) { } } - abi, err := json.Marshal(compilationOutput.Contracts[firstContractKey].Abi) - if err != nil { - return nil, err - } + var results []*CompilerResults - results := &CompilerResults{ - RequestedVersion: compilerVersion, - CompilerVersion: compilationOutput.Version, - Bytecode: compilationOutput.Contracts[firstContractKey].Bin, - ABI: string(abi), - ContractName: strings.ReplaceAll(firstContractKey, ":", ""), - Errors: errors, - Warnings: warnings, + for key := range compilationOutput.Contracts { + for key, output := range compilationOutput.Contracts[key] { + isEntryContract := false + if v.config.GetEntrySourceName() != "" && key == v.config.GetEntrySourceName() { + isEntryContract = true + } + + abi, err := json.Marshal(output.Abi) + if err != nil { + return nil, err + } + + results = append(results, &CompilerResults{ + IsEntryContract: isEntryContract, + RequestedVersion: compilerVersion, + Bytecode: output.Evm.Bytecode.Object, + ABI: string(abi), + Opcodes: output.Evm.Bytecode.Opcodes, + ContractName: key, + Errors: errors, + Warnings: warnings, + Metadata: output.Metadata, + }) + } } return results, nil @@ -176,11 +267,14 @@ func (v *Compiler) Compile() (*CompilerResults, error) { // CompilerResults represents the results of a solc compilation. type CompilerResults struct { + IsEntryContract bool `json:"is_entry_contract"` RequestedVersion string `json:"requested_version"` CompilerVersion string `json:"compiler_version"` + ContractName string `json:"contract_name"` Bytecode string `json:"bytecode"` ABI string `json:"abi"` - ContractName string `json:"contract_name"` + Opcodes string `json:"opcodes"` + Metadata string `json:"metadata"` Errors []string `json:"errors"` Warnings []string `json:"warnings"` } diff --git a/compiler_config.go b/compiler_config.go index 0a96d78..9784a99 100644 --- a/compiler_config.go +++ b/compiler_config.go @@ -15,6 +15,7 @@ var allowedArgs = map[string]bool{ "--evm-version": true, "--overwrite": true, "--libraries": true, + "--standard-json": true, } // requiredArgs defines a list of required arguments for solc. @@ -26,8 +27,10 @@ var requiredArgs = map[string]bool{ // CompilerConfig represents the compiler configuration for the solc binaries. type CompilerConfig struct { - CompilerVersion string // The version of the compiler to use. - Arguments []string // Arguments to pass to the solc tool. + CompilerVersion string // The version of the compiler to use. + EntrySourceName string // The name of the entry source file. + Arguments []string // Arguments to pass to the solc tool. + JsonConfig *CompilerJsonConfig // The json config to pass to the solc tool. } // NewDefaultCompilerConfig creates and returns a default CompilerConfiguration for compiler to use. @@ -50,6 +53,48 @@ func NewDefaultCompilerConfig(compilerVersion string) (*CompilerConfig, error) { return toReturn, nil } +// NewDefaultCompilerConfig creates and returns a default CompilerConfiguration for compiler to use with provided JSON settings. +func NewCompilerConfigFromJSON(compilerVersion string, entrySourceName string, config *CompilerJsonConfig) (*CompilerConfig, error) { + toReturn := &CompilerConfig{ + EntrySourceName: entrySourceName, + CompilerVersion: compilerVersion, + Arguments: []string{ + "--standard-json", // Output to stdout. + }, + JsonConfig: config, + } + + if _, err := toReturn.SanitizeArguments(toReturn.Arguments); err != nil { + return nil, err + } + + /* if err := toReturn.Validate(); err != nil { + return nil, err + } */ + + return toReturn, nil +} + +// SetJsonConfig sets the json config to pass to the solc tool. +func (c *CompilerConfig) SetJsonConfig(config *CompilerJsonConfig) { + c.JsonConfig = config +} + +// GetJsonConfig returns the json config to pass to the solc tool. +func (c *CompilerConfig) GetJsonConfig() *CompilerJsonConfig { + return c.JsonConfig +} + +// SetEntrySourceName sets the name of the entry source file. +func (c *CompilerConfig) SetEntrySourceName(name string) { + c.EntrySourceName = name +} + +// GetEntrySourceName returns the name of the entry source file. +func (c *CompilerConfig) GetEntrySourceName() string { + return c.EntrySourceName +} + // SetCompilerVersion sets the version of the solc compiler to use. func (c *CompilerConfig) SetCompilerVersion(version string) { c.CompilerVersion = version diff --git a/compiler_json_config.go b/compiler_json_config.go new file mode 100644 index 0000000..6b23505 --- /dev/null +++ b/compiler_json_config.go @@ -0,0 +1,35 @@ +package solc + +import "encoding/json" + +// Source represents the content of a Solidity source file. +type Source struct { + Content string `json:"content"` // The content of the Solidity source file. +} + +// Settings defines the configuration settings for the Solidity compiler. +type Settings struct { + Optimizer Optimizer `json:"optimizer"` // Configuration for the optimizer. + EVMVersion string `json:"evmVersion,omitempty"` // The version of the Ethereum Virtual Machine to target. Optional. + Remappings []string `json:"remappings,omitempty"` // List of remappings for library addresses. Optional. + OutputSelection map[string]map[string][]string `json:"outputSelection"` // Specifies the type of information to output (e.g., ABI, AST). +} + +// Optimizer represents the configuration for the Solidity compiler's optimizer. +type Optimizer struct { + Enabled bool `json:"enabled"` // Indicates whether the optimizer is enabled. + Runs int `json:"runs"` // Specifies the number of optimization runs. +} + +// CompilerJsonConfig represents the JSON configuration for the Solidity compiler. +type CompilerJsonConfig struct { + Language string `json:"language"` // Specifies the language version (e.g., "Solidity"). + Sources map[string]Source `json:"sources"` // Map of source file names to their content. + Settings Settings `json:"settings"` // Compiler settings. +} + +// ToJSON converts the CompilerJsonConfig to its JSON representation. +// It returns the JSON byte array or an error if the conversion fails. +func (c *CompilerJsonConfig) ToJSON() ([]byte, error) { + return json.Marshal(c) +} diff --git a/compiler_test.go b/compiler_test.go index 04d536d..51d9bd4 100644 --- a/compiler_test.go +++ b/compiler_test.go @@ -212,11 +212,11 @@ func TestCompiler(t *testing.T) { compilerResults, err := compiler.Compile() if testCase.wantCompileErr { - if compilerResults != nil { - assert.True(t, compilerResults.HasErrors()) - assert.False(t, compilerResults.HasWarnings()) - assert.GreaterOrEqual(t, len(compilerResults.GetWarnings()), 0) - assert.GreaterOrEqual(t, len(compilerResults.GetErrors()), 1) + for _, result := range compilerResults { + assert.True(t, result.HasErrors()) + assert.False(t, result.HasWarnings()) + assert.GreaterOrEqual(t, len(result.GetWarnings()), 0) + assert.GreaterOrEqual(t, len(result.GetErrors()), 1) } return @@ -225,13 +225,15 @@ func TestCompiler(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, compilerResults) - assert.NotEmpty(t, compilerResults.GetRequestedVersion()) - assert.NotEmpty(t, compilerResults.GetCompilerVersion()) - assert.NotEmpty(t, compilerResults.GetBytecode()) - assert.NotEmpty(t, compilerResults.GetABI()) - assert.NotEmpty(t, compilerResults.GetContractName()) - assert.GreaterOrEqual(t, len(compilerResults.GetWarnings()), 0) - assert.GreaterOrEqual(t, len(compilerResults.GetErrors()), 0) + for _, result := range compilerResults { + assert.NotEmpty(t, result.GetRequestedVersion()) + assert.NotEmpty(t, result.GetCompilerVersion()) + assert.NotEmpty(t, result.GetBytecode()) + assert.NotEmpty(t, result.GetABI()) + assert.NotEmpty(t, result.GetContractName()) + assert.GreaterOrEqual(t, len(result.GetWarnings()), 0) + assert.GreaterOrEqual(t, len(result.GetErrors()), 0) + } }) } } @@ -412,11 +414,147 @@ func TestCompilerFromSolc(t *testing.T) { compilerResults, err := solc.Compile(context.TODO(), testCase.source, testCase.compilerConfig) if testCase.wantCompileErr { - if compilerResults != nil { - assert.True(t, compilerResults.HasErrors()) - assert.False(t, compilerResults.HasWarnings()) - assert.GreaterOrEqual(t, len(compilerResults.GetWarnings()), 0) - assert.GreaterOrEqual(t, len(compilerResults.GetErrors()), 1) + for _, result := range compilerResults { + assert.True(t, result.HasErrors()) + assert.False(t, result.HasWarnings()) + assert.GreaterOrEqual(t, len(result.GetWarnings()), 0) + assert.GreaterOrEqual(t, len(result.GetErrors()), 1) + } + + return + } + + assert.NoError(t, err) + assert.NotNil(t, compilerResults) + + for _, result := range compilerResults { + assert.NotEmpty(t, result.GetRequestedVersion()) + assert.NotEmpty(t, result.GetCompilerVersion()) + assert.NotEmpty(t, result.GetBytecode()) + assert.NotEmpty(t, result.GetABI()) + assert.NotEmpty(t, result.GetContractName()) + assert.GreaterOrEqual(t, len(result.GetWarnings()), 0) + assert.GreaterOrEqual(t, len(result.GetErrors()), 0) + } + }) + } +} + +func TestCompilerWithJSON(t *testing.T) { + logger, err := GetDevelopmentLogger(zapcore.DebugLevel) + assert.NoError(t, err) + assert.NotNil(t, logger) + zap.ReplaceGlobals(logger) + + // Replace the global logger. + zap.ReplaceGlobals(logger) + + solcConfig, err := NewDefaultConfig() + assert.NoError(t, err) + assert.NotNil(t, solcConfig) + + solc, err := New(context.TODO(), solcConfig) + assert.NoError(t, err) + assert.NotNil(t, solc) + + testCases := []struct { + name string + wantErr bool + wantCompileErr bool + compilerConfig *CompilerConfig + sync bool + solc *Solc + }{ + { + name: "Valid Source", + wantCompileErr: false, + compilerConfig: func() *CompilerConfig { + jsonConfig := &CompilerJsonConfig{ + Language: "Solidity", + Sources: map[string]Source{ + "SimpleStorage.sol": { + Content: `// SPDX-License-Identifier: MIT + pragma solidity ^0.8.0; + + contract SimpleStorage { + uint256 private storedData; + + function set(uint256 x) public { + storedData = x; + } + + function get() public view returns (uint256) { + return storedData; + } + }`, + }, + }, + Settings: Settings{ + Optimizer: Optimizer{ + Enabled: false, + Runs: 200, + }, + OutputSelection: map[string]map[string][]string{ + "*": { + "*": []string{ + "abi", + "evm.bytecode", + "evm.runtimeBytecode", + "metadata", + "evm.deployedBytecode*", + }, + }, + }, + }, + } + + config, err := NewCompilerConfigFromJSON("0.8.0", "SimpleStorage", jsonConfig) + assert.NoError(t, err) + assert.NotNil(t, config) + return config + }(), + solc: solc, + sync: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + source, err := testCase.compilerConfig.GetJsonConfig().ToJSON() + assert.NoError(t, err) + assert.NotNil(t, source) + + compiler, err := NewCompiler(context.Background(), testCase.solc, testCase.compilerConfig, string(source)) + if testCase.wantErr { + assert.Error(t, err) + assert.Nil(t, compiler) + return + } + + assert.NoError(t, err) + assert.NotNil(t, compiler) + assert.NotNil(t, compiler.GetContext()) + assert.NotNil(t, compiler.GetSources()) + + // In case we drop releases path ability to test that it syncs successfully prior + // to compiling. + if testCase.sync { + err := solc.Sync() + assert.NoError(t, err) + + // Just so the function is tested, nothing else... + compiler.SetCompilerVersion(compiler.GetCompilerVersion()) + currentVersion := compiler.GetCompilerVersion() + assert.NotEmpty(t, currentVersion) + } + + compilerResults, err := compiler.Compile() + if testCase.wantCompileErr { + for _, result := range compilerResults { + assert.True(t, result.HasErrors()) + assert.False(t, result.HasWarnings()) + assert.GreaterOrEqual(t, len(result.GetWarnings()), 0) + assert.GreaterOrEqual(t, len(result.GetErrors()), 1) } return @@ -425,13 +563,14 @@ func TestCompilerFromSolc(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, compilerResults) - assert.NotEmpty(t, compilerResults.GetRequestedVersion()) - assert.NotEmpty(t, compilerResults.GetCompilerVersion()) - assert.NotEmpty(t, compilerResults.GetBytecode()) - assert.NotEmpty(t, compilerResults.GetABI()) - assert.NotEmpty(t, compilerResults.GetContractName()) - assert.GreaterOrEqual(t, len(compilerResults.GetWarnings()), 0) - assert.GreaterOrEqual(t, len(compilerResults.GetErrors()), 0) + for _, result := range compilerResults { + assert.NotEmpty(t, result.GetRequestedVersion()) + assert.NotEmpty(t, result.GetBytecode()) + assert.NotEmpty(t, result.GetABI()) + assert.NotEmpty(t, result.GetContractName()) + assert.GreaterOrEqual(t, len(result.GetWarnings()), 0) + assert.GreaterOrEqual(t, len(result.GetErrors()), 0) + } }) } } diff --git a/releases/releases.json b/releases/releases.json index c58c9bf..90ad7ce 100644 --- a/releases/releases.json +++ b/releases/releases.json @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:863fcb78eb72033f6a58ede1c92156c0e04e00694cc8febac6a060710a18e222 -size 965663 +oid sha256:7bf7e15c096f56db434942f2ed506b3364d7841d42cdbc3dc4a7238987e0a020 +size 965666 diff --git a/solc.go b/solc.go index 10cf9da..43e294b 100644 --- a/solc.go +++ b/solc.go @@ -60,7 +60,7 @@ func (s *Solc) GetHTTPClient() *http.Client { } // Compile compiles the provided Solidity source code using the specified compiler configuration. -func (s *Solc) Compile(ctx context.Context, source string, config *CompilerConfig) (*CompilerResults, error) { +func (s *Solc) Compile(ctx context.Context, source string, config *CompilerConfig) ([]*CompilerResults, error) { compiler, err := NewCompiler(ctx, s, config, source) if err != nil { return nil, err diff --git a/syncer.go b/syncer.go index 41452ba..d1a99ba 100644 --- a/syncer.go +++ b/syncer.go @@ -56,6 +56,11 @@ func (s *Solc) SyncReleases() ([]Version, error) { var versions []Version if err := json.Unmarshal(bodyBytes, &versions); err != nil { + zap.L().Error( + "Failed to unmarshal releases response", + zap.Error(err), + zap.Any("response", string(bodyBytes)), + ) return nil, err }