Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dependency Manager: Allow adding found dependencies to deployments interactively #1479

Merged
merged 10 commits into from
Mar 28, 2024
5 changes: 3 additions & 2 deletions internal/dependencymanager/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand All @@ -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
Expand Down
111 changes: 68 additions & 43 deletions internal/dependencymanager/dependencyinstaller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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)
Expand All @@ -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
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
}
}
}
Expand All @@ -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
}

Expand Down Expand Up @@ -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)
Expand All @@ -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())
Expand Down Expand Up @@ -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{
Expand Down
10 changes: 6 additions & 4 deletions internal/dependencymanager/dependencyinstaller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions internal/dependencymanager/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}

Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions internal/project/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"context"
"errors"
"fmt"

"github.com/onflow/flow-go/fvm/systemcontracts"
flowGo "github.com/onflow/flow-go/model/flow"

Expand Down
37 changes: 37 additions & 0 deletions internal/util/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)

Expand Down
Loading