diff --git a/internal/dependencymanager/dependencyinstaller.go b/internal/dependencymanager/dependencyinstaller.go index 5c006d5d8..c7cce1a41 100644 --- a/internal/dependencymanager/dependencyinstaller.go +++ b/internal/dependencymanager/dependencyinstaller.go @@ -250,6 +250,8 @@ func (di *DependencyInstaller) AddMany(dependencies []config.Dependency) error { } } + di.checkForConflictingContracts() + if err := di.saveState(); err != nil { return err } @@ -257,6 +259,46 @@ func (di *DependencyInstaller) AddMany(dependencies []config.Dependency) error { return nil } +func (di *DependencyInstaller) AddAllByNetworkAddress(sourceStr string) error { + network, address := ParseNetworkAddressString(sourceStr) + + accountContracts, err := di.getContracts(network, flowsdk.HexToAddress(address)) + if err != nil { + return fmt.Errorf("failed to fetch account contracts: %w", err) + } + + var dependencies []config.Dependency + + for _, contract := range accountContracts { + program, err := project.NewProgram(contract, nil, "") + if err != nil { + return fmt.Errorf("failed to parse program: %w", err) + } + + contractName, err := program.Name() + if err != nil { + return fmt.Errorf("failed to parse contract name: %w", err) + } + + dep := config.Dependency{ + Name: contractName, + Source: config.Source{ + NetworkName: network, + Address: flowsdk.HexToAddress(address), + ContractName: contractName, + }, + } + + dependencies = append(dependencies, dep) + } + + if err := di.AddMany(dependencies); err != nil { + return err + } + + return nil +} + func (di *DependencyInstaller) addDependency(dep config.Dependency) error { sourceString := fmt.Sprintf("%s://%s.%s", dep.Source.NetworkName, dep.Source.Address.String(), dep.Source.ContractName) @@ -282,10 +324,33 @@ func (di *DependencyInstaller) checkForConflictingContracts() { func (di *DependencyInstaller) processDependency(dependency config.Dependency) error { depAddress := flowsdk.HexToAddress(dependency.Source.Address.String()) - return di.fetchDependencies(dependency.Source.NetworkName, depAddress, dependency.Name, dependency.Source.ContractName) + return di.fetchDependencies(dependency.Source.NetworkName, depAddress, dependency.Source.ContractName) } -func (di *DependencyInstaller) fetchDependencies(networkName string, address flowsdk.Address, assignedName, contractName string) error { +func (di *DependencyInstaller) getContracts(network string, address flowsdk.Address) (map[string][]byte, error) { + gw, ok := di.Gateways[network] + if !ok { + return nil, fmt.Errorf("gateway for network %s not found", network) + } + + ctx := context.Background() + acct, err := gw.GetAccount(ctx, address) + if err != nil { + return nil, fmt.Errorf("failed to get account at %s on %s: %w", address, network, err) + } + + if acct == nil { + return nil, fmt.Errorf("no account found at address %s on network %s", address, network) + } + + if len(acct.Contracts) == 0 { + return nil, fmt.Errorf("no contracts found at address %s on network %s", address, network) + } + + return acct.Contracts, nil +} + +func (di *DependencyInstaller) fetchDependencies(networkName string, address flowsdk.Address, contractName string) error { sourceString := fmt.Sprintf("%s://%s.%s", networkName, address.String(), contractName) if _, exists := di.dependencies[sourceString]; exists { @@ -293,7 +358,7 @@ func (di *DependencyInstaller) fetchDependencies(networkName string, address flo } err := di.addDependency(config.Dependency{ - Name: assignedName, + Name: contractName, Source: config.Source{ NetworkName: networkName, Address: address, @@ -304,20 +369,12 @@ func (di *DependencyInstaller) fetchDependencies(networkName string, address flo return fmt.Errorf("error adding dependency: %w", err) } - ctx := context.Background() - account, err := di.Gateways[networkName].GetAccount(ctx, address) + accountContracts, err := di.getContracts(networkName, address) if err != nil { - return fmt.Errorf("failed to get account: %w", err) - } - if account == nil { - return fmt.Errorf("account is nil for address: %s", address) - } - - if account.Contracts == nil { - return fmt.Errorf("contracts are nil for account: %s", address) + return fmt.Errorf("error fetching contracts: %w", err) } - contract, ok := account.Contracts[contractName] + contract, ok := accountContracts[contractName] if !ok { return fmt.Errorf("contract %s not found at address %s", contractName, address.String()) } @@ -327,16 +384,7 @@ func (di *DependencyInstaller) fetchDependencies(networkName string, address flo return fmt.Errorf("failed to parse program: %w", err) } - parsedContractName, err := program.Name() - if err != nil { - return fmt.Errorf("failed to parse contract name: %w", err) - } - - if parsedContractName != contractName { - return fmt.Errorf("contract name mismatch: expected %s, got %s", contractName, parsedContractName) - } - - if err := di.handleFoundContract(networkName, address.String(), assignedName, contractName, program); err != nil { + if err := di.handleFoundContract(networkName, address.String(), contractName, program); err != nil { return fmt.Errorf("failed to handle found contract: %w", err) } @@ -344,7 +392,7 @@ func (di *DependencyInstaller) fetchDependencies(networkName string, address flo imports := program.AddressImportDeclarations() for _, imp := range imports { contractName := imp.Identifiers[0].String() - err := di.fetchDependencies(networkName, flowsdk.HexToAddress(imp.Location.String()), contractName, contractName) + err := di.fetchDependencies(networkName, flowsdk.HexToAddress(imp.Location.String()), contractName) if err != nil { return err } @@ -392,7 +440,7 @@ func (di *DependencyInstaller) handleFileSystem(contractAddr, contractName, cont return nil } -func (di *DependencyInstaller) handleFoundContract(networkName, contractAddr, assignedName, contractName string, program *project.Program) error { +func (di *DependencyInstaller) handleFoundContract(networkName, contractAddr, contractName string, program *project.Program) error { hash := sha256.New() hash.Write(program.CodeWithUnprocessedImports()) originalContractDataHash := hex.EncodeToString(hash.Sum(nil)) @@ -400,11 +448,11 @@ func (di *DependencyInstaller) handleFoundContract(networkName, contractAddr, as program.ConvertAddressImports() contractData := string(program.CodeWithUnprocessedImports()) - dependency := di.State.Dependencies().ByName(assignedName) + dependency := di.State.Dependencies().ByName(contractName) // If a dependency by this name already exists and its remote source network or address does not match, then give option to stop or continue if dependency != nil && (dependency.Source.NetworkName != networkName || dependency.Source.Address.String() != contractAddr) { - di.Logger.Info(fmt.Sprintf("%s A dependency named %s already exists with a different remote source. Please fix the conflict and retry.", util.PrintEmoji("🚫"), assignedName)) + di.Logger.Info(fmt.Sprintf("%s A dependency named %s already exists with a different remote source. Please fix the conflict and retry.", util.PrintEmoji("🚫"), contractName)) os.Exit(0) return nil } @@ -419,7 +467,7 @@ func (di *DependencyInstaller) handleFoundContract(networkName, contractAddr, as } } - err := di.updateDependencyState(networkName, contractAddr, assignedName, contractName, originalContractDataHash) + err := di.updateDependencyState(networkName, contractAddr, contractName, originalContractDataHash) if err != nil { di.Logger.Error(fmt.Sprintf("Error updating state: %v", err)) return err @@ -520,9 +568,9 @@ func (di *DependencyInstaller) updateDependencyAlias(contractName, aliasNetwork return nil } -func (di *DependencyInstaller) updateDependencyState(networkName, contractAddress, assignedName, contractName, contractHash string) error { +func (di *DependencyInstaller) updateDependencyState(networkName, contractAddress, contractName, contractHash string) error { dep := config.Dependency{ - Name: assignedName, + Name: contractName, Source: config.Source{ NetworkName: networkName, Address: flowsdk.HexToAddress(contractAddress), diff --git a/internal/dependencymanager/install.go b/internal/dependencymanager/install.go index 87e648aa5..b1352b454 100644 --- a/internal/dependencymanager/install.go +++ b/internal/dependencymanager/install.go @@ -39,10 +39,41 @@ var installFlags = Flags{} var installCommand = &command.Command{ Cmd: &cobra.Command{ - Use: "install", - Short: "Install contract and dependencies.", + Use: "install [dependencies...]", + Short: "Install contracts and their dependencies.", + Long: `Install Flow contracts and their dependencies. + +By default, this command will install any dependencies listed in the flow.json file at the root of your project. +You can also specify one or more dependencies directly on the command line, using any of the following formats: + + • network://address + • network://address.ContractName + • core contract name (e.g., FlowToken, NonFungibleToken) + +Examples: + 1. Install dependencies listed in flow.json: + flow dependencies install + + 2. Install a specific core contract by name: + flow dependencies install FlowToken + + 3. Install a single contract by network and address (all contracts at that address): + flow dependencies install testnet://0x1234abcd + + 4. Install a specific contract by network, address, and contract name: + flow dependencies install testnet://0x1234abcd.MyContract + + 5. Install multiple dependencies: + flow dependencies install FungibleToken NonFungibleToken + +Note: +• Using 'network://address' will attempt to install all contracts deployed at that address. +• Using 'network://address.ContractName' will install only the specified contract. +• Specifying a known core contract (e.g., FlowToken) will install it from the official system contracts + address on Mainnet or Testnet (depending on your project's default network). +`, Example: `flow dependencies install -flow dependencies install testnet://0afe396ebc8eee65.FlowToken +flow dependencies install testnet://0x7e60df042a9c0868.FlowToken flow dependencies install FlowToken flow dependencies install FlowToken NonFungibleToken`, Args: cobra.ArbitraryArgs, @@ -78,13 +109,26 @@ func install( continue } - if err := installer.AddBySourceString(dep); err != nil { - if strings.Contains(err.Error(), "invalid dependency source format") { - logger.Error(fmt.Sprintf("Error: '%s' is neither a core contract nor a valid dependency source format.\nPlease provide a valid dependency source in the format 'network://address.ContractName', e.g., 'testnet://0x1234567890abcdef.MyContract', or use a valid core contract name such as 'FlowToken'.", dep)) - } else { - logger.Error(fmt.Sprintf("Error adding dependency %s: %v", dep, err)) + // Check if the dependency is in the "network://address" format (address only) + hasContract, err := hasContractName(dep) + if err != nil { + return nil, fmt.Errorf("invalid dependency format") + } + + if !hasContract { + if err := installer.AddAllByNetworkAddress(dep); err != nil { + logger.Error(fmt.Sprintf("Error adding contracts by address: %v", err)) + return nil, err + } + } else { + if err := installer.AddBySourceString(dep); err != nil { + if strings.Contains(err.Error(), "invalid dependency source format") { + logger.Error(fmt.Sprintf("Error: '%s' is neither a core contract nor a valid dependency source format.\nPlease provide a valid dependency source in the format 'network://address.ContractName', e.g., 'testnet://0x1234567890abcdef.MyContract', or use a valid core contract name such as 'FlowToken'.", dep)) + } else { + logger.Error(fmt.Sprintf("Error adding dependency %s: %v", dep, err)) + } + return nil, err } - return nil, err } } @@ -120,3 +164,18 @@ func findCoreContractCaseInsensitive(name string) string { } return "" } + +// Check if the input is in "network://address" or "network://address.contract" format +func hasContractName(dep string) (bool, error) { + parts := strings.SplitN(dep, "://", 2) + if len(parts) != 2 { + return false, fmt.Errorf("invalid format: missing '://'") + } + + return strings.Contains(parts[1], "."), nil +} + +func ParseNetworkAddressString(sourceStr string) (network, address string) { + parts := strings.Split(sourceStr, "://") + return parts[0], parts[1] +}