Skip to content

Commit

Permalink
Merge pull request #1479 from onflow/chasefleming/1362
Browse files Browse the repository at this point in the history
Dependency Manager: Allow adding found dependencies to deployments interactively
  • Loading branch information
chasefleming authored Mar 28, 2024
2 parents 562cfde + 03e7863 commit 4adadb0
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 51 deletions.
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

0 comments on commit 4adadb0

Please sign in to comment.