diff --git a/internal/dependencymanager/dependencyinstaller.go b/internal/dependencymanager/dependencyinstaller.go index cae67bb78..5c006d5d8 100644 --- a/internal/dependencymanager/dependencyinstaller.go +++ b/internal/dependencymanager/dependencyinstaller.go @@ -392,17 +392,6 @@ func (di *DependencyInstaller) handleFileSystem(contractAddr, contractName, cont return nil } -func isCoreContract(contractName string) bool { - sc := systemcontracts.SystemContractsForChain(flowGo.Emulator) - - for _, coreContract := range sc.All() { - if coreContract.Name == contractName { - return true - } - } - return false -} - func (di *DependencyInstaller) handleFoundContract(networkName, contractAddr, assignedName, contractName string, program *project.Program) error { hash := sha256.New() hash.Write(program.CodeWithUnprocessedImports()) @@ -455,7 +444,7 @@ func (di *DependencyInstaller) handleFoundContract(networkName, contractAddr, as func (di *DependencyInstaller) handleAdditionalDependencyTasks(networkName, contractName string) error { // If the contract is not a core contract and the user does not want to skip deployments, then prompt for a deployment - if !di.SkipDeployments && !isCoreContract(contractName) { + if !di.SkipDeployments && !util.IsCoreContract(contractName) { err := di.updateDependencyDeployment(contractName) if err != nil { di.Logger.Error(fmt.Sprintf("Error updating deployment: %v", err)) @@ -467,7 +456,7 @@ func (di *DependencyInstaller) handleAdditionalDependencyTasks(networkName, cont } // If the contract is not a core contract and the user does not want to skip aliasing, then prompt for an alias - if !di.SkipAlias && !isCoreContract(contractName) { + if !di.SkipAlias && !util.IsCoreContract(contractName) { err := di.updateDependencyAlias(contractName, networkName) if err != nil { di.Logger.Error(fmt.Sprintf("Error updating alias: %v", err)) diff --git a/internal/super/flix.go b/internal/super/flix.go index 92d7ed6ad..4df9858d4 100644 --- a/internal/super/flix.go +++ b/internal/super/flix.go @@ -23,9 +23,8 @@ import ( "fmt" "os" + "github.com/onflow/cadence/runtime/parser" "github.com/onflow/flixkit-go/v2/flixkit" - "github.com/onflow/flow-go/fvm/systemcontracts" - "github.com/onflow/flow-go/model/flow" "github.com/spf13/cobra" @@ -38,6 +37,7 @@ import ( "github.com/onflow/flow-cli/internal/command" "github.com/onflow/flow-cli/internal/scripts" "github.com/onflow/flow-cli/internal/transactions" + "github.com/onflow/flow-cli/internal/util" ) type flixFlags struct { @@ -225,7 +225,6 @@ func generateFlixCmd( ) (result command.Result, err error) { cadenceFile := args[0] depContracts := getContractsFromState(state, flags.ExcludeNetworks) - coreContracts, err := getCoreContracts(flags.ExcludeNetworks) if err != nil { return nil, fmt.Errorf("could not get core contracts %w", err) } @@ -261,15 +260,12 @@ func generateFlixCmd( } } - ctx := context.Background() - - // merge deployed contracts and core contracts - for name, addresses := range coreContracts { - if _, exists := depContracts[name]; !exists { - depContracts[name] = addresses - } + err = validateImports(string(code), depContracts, depNetworks) + if err != nil { + return nil, fmt.Errorf("could not validate imported contracts: %w", err) } + ctx := context.Background() prettyJSON, err := flixService.CreateTemplate(ctx, depContracts, string(code), flags.PreFill, depNetworks) if err != nil { return nil, fmt.Errorf("could not generate flix %w", err) @@ -296,34 +292,37 @@ func (fr *flixResult) Oneliner() string { return fr.result } -func getCoreContracts(excludeNetworks []string) (flixkit.ContractInfos, error) { - coreContracts := make(flixkit.ContractInfos) - - networkConfigs := []struct { - chainID flow.ChainID - networkName string - excluded bool - }{ - {flow.ChainID("flow-mainnet"), config.MainnetNetwork.Name, slices.Contains(excludeNetworks, config.MainnetNetwork.Name)}, - {flow.ChainID("flow-testnet"), config.TestnetNetwork.Name, slices.Contains(excludeNetworks, config.TestnetNetwork.Name)}, - {flow.ChainID("flow-emulator"), config.EmulatorNetwork.Name, slices.Contains(excludeNetworks, config.EmulatorNetwork.Name)}, +func validateImports(code string, depContracts flixkit.ContractInfos, depNetworks []flixkit.NetworkConfig) error { + // Check all imported contracts in the cadence code + astProgram, err := parser.ParseProgram(nil, []byte(code), parser.Config{}) + if err != nil { + return fmt.Errorf("could not parse Cadence code %w", err) } - for _, nc := range networkConfigs { - if nc.excluded { - continue + // Check for any missing string imports + for _, imp := range astProgram.ImportDeclarations() { + if len(imp.Identifiers) > 0 || imp.Location == nil { + return fmt.Errorf("only string imports of the form `import \"ContractName\"` are supported") } - contracts := systemcontracts.SystemContractsForChain(nc.chainID) - for _, c := range contracts.All() { - if _, exists := coreContracts[c.Name]; !exists { - coreContracts[c.Name] = make(flixkit.NetworkAddressMap) + contractName := imp.Location.String() + + if depContracts[contractName] == nil { + if util.IsCoreContract(contractName) { + return fmt.Errorf("contract %[1]s is not found in the flow.json configuration, if this refers to the %[1]s core contract, please add it using `flow deps install %[1]s`", contractName) + } + + return fmt.Errorf("contract %[1]s is not found in the flow.json configuration, if it refers to an external contract, please add it using `flow deps install ://
.%[1]s`", contractName) + } + + for _, network := range depNetworks { + if depContracts[contractName][network.Name] == "" { + return fmt.Errorf("contract %s was found in the flow.json configuration, but is missing an alias for network %s", contractName, network.Name) } - coreContracts[c.Name][nc.networkName] = c.Address.HexWithPrefix() } } - return coreContracts, nil + return nil } func getContractsFromState(state *flowkit.State, excludeNetworks []string) flixkit.ContractInfos { @@ -385,7 +384,7 @@ func getContractsFromState(state *flowkit.State, excludeNetworks []string) flixk } // add contracts that have not been deployed - for _, c := range *state.Dependencies() { + for _, c := range *state.Contracts() { if _, ok := allContracts[c.Name]; !ok { allContracts[c.Name] = make(flixkit.NetworkAddressMap) } diff --git a/internal/super/flix_test.go b/internal/super/flix_test.go index 7e297e954..072575e16 100644 --- a/internal/super/flix_test.go +++ b/internal/super/flix_test.go @@ -27,6 +27,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/onflow/flixkit-go/v2/flixkit" + "github.com/onflow/flow-go-sdk" "github.com/onflow/flowkit/v2" "github.com/onflow/flowkit/v2/config" @@ -278,3 +279,94 @@ func Test_GenerateFlixPrefill(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, result) } + +func Test_GenerateFlixMissingCoreContract(t *testing.T) { + logger := output.NewStdoutLogger(output.NoneLog) + srv := mocks.DefaultMockServices() + cadenceFile := "cadence.cdc" + + mockFS := afero.NewMemMapFs() + rw := afero.Afero{Fs: mockFS} + script := "import \"FungibleToken\"\n access(all) fun main() {}" + err := rw.WriteFile(cadenceFile, []byte(script), 0o644) + assert.NoError(t, err) + state, _ := flowkit.Init(rw) + + mockFlixService := new(MockFlixService) + + _, err = generateFlixCmd( + []string{cadenceFile}, + command.GlobalFlags{}, + logger, + srv.Mock, + state, + mockFlixService, + flixFlags{ + ExcludeNetworks: []string{"emulator", "testnet"}, + }, + ) + assert.Error(t, err) + assert.Contains(t, err.Error(), "flow deps install FungibleToken") +} + +func Test_GenerateFlixMissingExternalContract(t *testing.T) { + logger := output.NewStdoutLogger(output.NoneLog) + srv := mocks.DefaultMockServices() + cadenceFile := "cadence.cdc" + + mockFS := afero.NewMemMapFs() + rw := afero.Afero{Fs: mockFS} + script := "import \"SomeContract\"\n access(all) fun main() {}" + err := rw.WriteFile(cadenceFile, []byte(script), 0o644) + assert.NoError(t, err) + state, _ := flowkit.Init(rw) + + mockFlixService := new(MockFlixService) + + _, err = generateFlixCmd( + []string{cadenceFile}, + command.GlobalFlags{}, + logger, + srv.Mock, + state, + mockFlixService, + flixFlags{ + ExcludeNetworks: []string{"emulator", "testnet"}, + }, + ) + assert.Error(t, err) + assert.Contains(t, err.Error(), "flow deps install ://
.SomeContract") +} + +func Test_GenerateFlixMissingAlias(t *testing.T) { + logger := output.NewStdoutLogger(output.NoneLog) + srv := mocks.DefaultMockServices() + cadenceFile := "cadence.cdc" + + mockFS := afero.NewMemMapFs() + rw := afero.Afero{Fs: mockFS} + script := "import \"Foobar\"\n access(all) fun main() {}" + err := rw.WriteFile(cadenceFile, []byte(script), 0o644) + assert.NoError(t, err) + state, _ := flowkit.Init(rw) + state.Contracts().AddOrUpdate(config.Contract{ + Name: "Foobar", + Aliases: []config.Alias{{Address: flow.Address{0x01}, Network: "mainnet"}}, + }) + + mockFlixService := new(MockFlixService) + + _, err = generateFlixCmd( + []string{cadenceFile}, + command.GlobalFlags{}, + logger, + srv.Mock, + state, + mockFlixService, + flixFlags{ + ExcludeNetworks: []string{"emulator"}, + }, + ) + assert.Error(t, err) + assert.Contains(t, err.Error(), "missing an alias") +} diff --git a/internal/util/util.go b/internal/util/util.go index 20a95a3e4..562be06a3 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -30,6 +30,8 @@ import ( "github.com/onflow/flow-go-sdk" flowsdk "github.com/onflow/flow-go-sdk" "github.com/onflow/flow-go-sdk/crypto" + "github.com/onflow/flow-go/fvm/systemcontracts" + flowGo "github.com/onflow/flow-go/model/flow" "github.com/onflow/flowkit/v2" "github.com/onflow/flowkit/v2/accounts" @@ -175,3 +177,14 @@ func Pluralize(word string, count int) string { } return word + "s" } + +func IsCoreContract(contractName string) bool { + sc := systemcontracts.SystemContractsForChain(flowGo.Emulator) + + for _, coreContract := range sc.All() { + if coreContract.Name == contractName { + return true + } + } + return false +} \ No newline at end of file