diff --git a/internal/dependencymanager/add.go b/internal/dependencymanager/add.go index 30bc66e7b..ac6e5f94a 100644 --- a/internal/dependencymanager/add.go +++ b/internal/dependencymanager/add.go @@ -30,7 +30,8 @@ import ( ) type addFlagsCollection struct { - name string `default:"" flag:"name" info:"Name of the dependency"` + name string `default:"" flag:"name" info:"Name of the dependency"` + skipDeployments bool `default:"false" flag:"skip-deployments" info:"Skip adding the dependency to deployments"` } var addFlags = addFlagsCollection{} @@ -57,7 +58,7 @@ func add( dep := args[0] - installer, err := NewDependencyInstaller(logger, state) + installer, err := NewDependencyInstaller(logger, state, addFlags.skipDeployments) if err != nil { logger.Error(fmt.Sprintf("Error: %v", err)) return nil, err diff --git a/internal/dependencymanager/dependencyinstaller.go b/internal/dependencymanager/dependencyinstaller.go index 67f14595b..5e90df1a2 100644 --- a/internal/dependencymanager/dependencyinstaller.go +++ b/internal/dependencymanager/dependencyinstaller.go @@ -25,7 +25,9 @@ import ( "fmt" "os" "path/filepath" - "sync" + + "github.com/onflow/flow-go/fvm/systemcontracts" + flowGo "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-cli/internal/util" @@ -42,14 +44,14 @@ import ( ) type DependencyInstaller struct { - Gateways map[string]gateway.Gateway - Logger output.Logger - State *flowkit.State - Mutex sync.Mutex + Gateways map[string]gateway.Gateway + Logger output.Logger + State *flowkit.State + SkipDeployments bool } // NewDependencyInstaller creates a new instance of DependencyInstaller -func NewDependencyInstaller(logger output.Logger, state *flowkit.State) (*DependencyInstaller, error) { +func NewDependencyInstaller(logger output.Logger, state *flowkit.State, skipDeployments bool) (*DependencyInstaller, error) { emulatorGateway, err := gateway.NewGrpcGateway(config.EmulatorNetwork) if err != nil { return nil, fmt.Errorf("error creating emulator gateway: %v", err) @@ -72,9 +74,10 @@ func NewDependencyInstaller(logger output.Logger, state *flowkit.State) (*Depend } return &DependencyInstaller{ - Gateways: gateways, - Logger: logger, - State: state, + Gateways: gateways, + Logger: logger, + State: state, + SkipDeployments: skipDeployments, }, nil } @@ -137,13 +140,6 @@ func (di *DependencyInstaller) fetchDependencies(networkName string, address flo return fmt.Errorf("contracts are nil for account: %s", address) } - var wg sync.WaitGroup - errCh := make(chan error, len(account.Contracts)) - - // Create a max number of goroutines so that we don't rate limit the access node - maxGoroutines := 5 - semaphore := make(chan struct{}, maxGoroutines) - found := false for _, contract := range account.Contracts { @@ -167,18 +163,11 @@ func (di *DependencyInstaller) fetchDependencies(networkName string, address flo if program.HasAddressImports() { imports := program.AddressImportDeclarations() for _, imp := range imports { - wg.Add(1) - go func(importAddress flowsdk.Address, contractName string) { - semaphore <- struct{}{} - defer func() { - <-semaphore - wg.Done() - }() - err := di.fetchDependencies(networkName, importAddress, contractName, contractName) - if err != nil { - errCh <- err - } - }(flowsdk.HexToAddress(imp.Location.String()), imp.Identifiers[0].String()) + contractName := imp.Identifiers[0].String() + err := di.fetchDependencies(networkName, flowsdk.HexToAddress(imp.Location.String()), contractName, contractName) + if err != nil { + return err + } } } } @@ -189,16 +178,6 @@ func (di *DependencyInstaller) fetchDependencies(networkName string, address flo di.Logger.Error(errMsg) } - wg.Wait() - close(errCh) - close(semaphore) - - for err := range errCh { - if err != nil { - return err - } - } - return nil } @@ -228,9 +207,6 @@ func (di *DependencyInstaller) createContractFile(address, contractName, data st } func (di *DependencyInstaller) handleFileSystem(contractAddr, contractName, contractData, networkName string) error { - di.Mutex.Lock() - defer di.Mutex.Unlock() - if !di.contractFileExists(contractAddr, contractName) { if err := di.createContractFile(contractAddr, contractName, contractData); err != nil { return fmt.Errorf("failed to create contract file: %w", err) @@ -242,6 +218,17 @@ 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()) @@ -274,16 +261,54 @@ func (di *DependencyInstaller) handleFoundContract(networkName, contractAddr, as return fmt.Errorf("error handling file system: %w", err) } - err = di.updateState(networkName, contractAddr, assignedName, contractName, originalContractDataHash) + err = di.updateDependencyState(networkName, contractAddr, assignedName, contractName, originalContractDataHash) if err != nil { di.Logger.Error(fmt.Sprintf("Error updating state: %v", err)) return err } + if !di.SkipDeployments && !isCoreContract(contractName) { + err = di.updateDependencyDeployment(contractName) + if err != nil { + di.Logger.Error(fmt.Sprintf("Error updating deployment: %v", err)) + return err + } + } + + return nil +} + +func (di *DependencyInstaller) updateDependencyDeployment(contractName string) error { + // Add to deployments + // If a deployment already exists for that account, contract, and network, then ignore + raw := util.AddContractToDeploymentPrompt("emulator", *di.State.Accounts(), contractName) + + if raw != nil { + deployment := di.State.Deployments().ByAccountAndNetwork(raw.Account, raw.Network) + if deployment == nil { + di.State.Deployments().AddOrUpdate(config.Deployment{ + Network: raw.Network, + Account: raw.Account, + }) + deployment = di.State.Deployments().ByAccountAndNetwork(raw.Account, raw.Network) + } + + for _, c := range raw.Contracts { + deployment.AddContract(config.ContractDeployment{Name: c}) + } + + err := di.State.SaveDefault() + if err != nil { + return err + } + + di.Logger.Info(fmt.Sprintf("Dependency Manager: %s added to emulator deployments in flow.json", contractName)) + } + return nil } -func (di *DependencyInstaller) updateState(networkName, contractAddress, assignedName, contractName, contractHash string) error { +func (di *DependencyInstaller) updateDependencyState(networkName, contractAddress, assignedName, contractName, contractHash string) error { dep := config.Dependency{ Name: assignedName, Source: config.Source{ diff --git a/internal/dependencymanager/dependencyinstaller_test.go b/internal/dependencymanager/dependencyinstaller_test.go index ef8bba2e6..22ef485cf 100644 --- a/internal/dependencymanager/dependencyinstaller_test.go +++ b/internal/dependencymanager/dependencyinstaller_test.go @@ -74,8 +74,9 @@ func TestDependencyInstallerInstall(t *testing.T) { config.TestnetNetwork.Name: gw.Mock, config.MainnetNetwork.Name: gw.Mock, }, - Logger: logger, - State: state, + Logger: logger, + State: state, + SkipDeployments: true, } err := di.Install() @@ -116,8 +117,9 @@ func TestDependencyInstallerAdd(t *testing.T) { config.TestnetNetwork.Name: gw.Mock, config.MainnetNetwork.Name: gw.Mock, }, - Logger: logger, - State: state, + Logger: logger, + State: state, + SkipDeployments: true, } sourceStr := fmt.Sprintf("emulator://%s.%s", serviceAddress.String(), tests.ContractHelloString.Name) diff --git a/internal/dependencymanager/install.go b/internal/dependencymanager/install.go index f1a49fea1..dcb4d47a8 100644 --- a/internal/dependencymanager/install.go +++ b/internal/dependencymanager/install.go @@ -29,7 +29,9 @@ import ( "github.com/onflow/flow-cli/internal/command" ) -type installFlagsCollection struct{} +type installFlagsCollection struct { + skipDeployments bool `default:"false" flag:"skip-deployments" info:"Skip adding the dependency to deployments"` +} var installFlags = installFlagsCollection{} @@ -52,7 +54,7 @@ func install( ) (result command.Result, err error) { logger.Info("🔄 Installing dependencies from flow.json...") - installer, err := NewDependencyInstaller(logger, state) + installer, err := NewDependencyInstaller(logger, state, installFlags.skipDeployments) if err != nil { logger.Error(fmt.Sprintf("Error: %v", err)) return nil, err diff --git a/internal/project/deploy.go b/internal/project/deploy.go index 0a7a05ed4..a51e7d877 100644 --- a/internal/project/deploy.go +++ b/internal/project/deploy.go @@ -22,6 +22,7 @@ import ( "context" "errors" "fmt" + "github.com/onflow/flow-go/fvm/systemcontracts" flowGo "github.com/onflow/flow-go/model/flow" diff --git a/internal/util/prompt.go b/internal/util/prompt.go index cffb1c49f..116dddcbb 100644 --- a/internal/util/prompt.go +++ b/internal/util/prompt.go @@ -25,6 +25,8 @@ import ( "strconv" "strings" + "github.com/onflow/flowkit/accounts" + "github.com/gosuri/uilive" "github.com/manifoldco/promptui" "github.com/onflow/flow-go-sdk" @@ -496,6 +498,41 @@ func NewDeploymentPrompt( return deploymentData } +// AddContractToDeploymentPrompt prompts a user to select an account to deploy a given contract on a given network +func AddContractToDeploymentPrompt(networkName string, accounts accounts.Accounts, contractName string) *DeploymentData { + deploymentData := &DeploymentData{ + Network: networkName, + Contracts: []string{contractName}, + } + var err error + + accountNames := make([]string, 0) + for _, account := range accounts { + accountNames = append(accountNames, account.Name) + } + + // Add a "none" option to the list of accounts + accountNames = append(accountNames, "none") + + accountPrompt := promptui.Select{ + Label: fmt.Sprintf("Choose an account to deploy %s to on %s (or 'none' to skip)", contractName, networkName), + Items: accountNames, + } + selectedIndex, _, err := accountPrompt.Run() + if err == promptui.ErrInterrupt { + os.Exit(-1) + } + + // Handle the "none" selection based on its last position + if selectedIndex == len(accountNames)-1 { + return nil + } + + deploymentData.Account = accounts[selectedIndex].Name + + return deploymentData +} + func RemoveAccountPrompt(accounts config.Accounts) string { accountNames := make([]string, 0)