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: Install all contracts from an address #1867

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 79 additions & 31 deletions internal/dependencymanager/dependencyinstaller.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,55 @@ func (di *DependencyInstaller) AddMany(dependencies []config.Dependency) error {
}
}

di.checkForConflictingContracts()

if err := di.saveState(); err != nil {
return err
}

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)

Expand All @@ -282,18 +324,41 @@ 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 {
return nil // Skip already processed dependencies
}

err := di.addDependency(config.Dependency{
Name: assignedName,
Name: contractName,
Source: config.Source{
NetworkName: networkName,
Address: address,
Expand All @@ -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())
}
Expand All @@ -327,24 +384,15 @@ 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)
}

if program.HasAddressImports() {
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
}
Expand Down Expand Up @@ -392,19 +440,19 @@ 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))

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
}
Expand All @@ -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
Expand Down Expand Up @@ -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),
Expand Down
77 changes: 68 additions & 9 deletions internal/dependencymanager/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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]
}
Loading