diff --git a/internal/dependencymanager/dependencyinstaller.go b/internal/dependencymanager/dependencyinstaller.go index 45bc9e7b9..8df6e16d1 100644 --- a/internal/dependencymanager/dependencyinstaller.go +++ b/internal/dependencymanager/dependencyinstaller.go @@ -495,16 +495,26 @@ func (di *DependencyInstaller) fetchDependenciesWithDepth(dependency config.Depe if program.HasAddressImports() { imports := program.AddressImportDeclarations() for _, imp := range imports { - importContractName := imp.Imports[0].Identifier.Identifier + + actualContractName := imp.Imports[0].Identifier.Identifier importAddress := flowsdk.HexToAddress(imp.Location.String()) + // Check if this import has an alias (e.g., "import FUSD as FUSD1 from 0xaddress") + // If aliased, use the alias as the dependency name so "import FUSD1" resolves correctly + dependencyName := actualContractName + if imp.Imports[0].Alias.Identifier != "" { + dependencyName = imp.Imports[0].Alias.Identifier + } + // Create a dependency for the import + // Name is the alias (or actual name if not aliased) - this is what gets resolved in imports + // ContractName is the actual contract name on chain - this is what gets fetched importDependency := config.Dependency{ - Name: importContractName, + Name: dependencyName, Source: config.Source{ NetworkName: networkName, Address: importAddress, - ContractName: importContractName, + ContractName: actualContractName, }, } @@ -567,13 +577,13 @@ func (di *DependencyInstaller) handleFoundContract(dependency config.Dependency, program.ConvertAddressImports() contractData := string(program.CodeWithUnprocessedImports()) - existingDependency := di.State.Dependencies().ByName(contractName) + existingDependency := di.State.Dependencies().ByName(dependency.Name) // If a dependency by this name already exists and its remote source network or address does not match, // allow it only if an existing alias matches the incoming network+address; otherwise terminate. if existingDependency != nil && (existingDependency.Source.NetworkName != networkName || existingDependency.Source.Address.String() != contractAddr) { - if !di.existingAliasMatches(contractName, networkName, 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("🚫"), contractName)) + if !di.existingAliasMatches(dependency.Name, networkName, 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("🚫"), dependency.Name)) os.Exit(0) return nil } @@ -586,7 +596,7 @@ func (di *DependencyInstaller) handleFoundContract(dependency config.Dependency, // Find existing pending prompt for this contract or create new one found := false for i := range di.pendingPrompts { - if di.pendingPrompts[i].contractName == contractName { + if di.pendingPrompts[i].contractName == dependency.Name { di.pendingPrompts[i].needsUpdate = true di.pendingPrompts[i].updateHash = originalContractDataHash found = true @@ -595,7 +605,7 @@ func (di *DependencyInstaller) handleFoundContract(dependency config.Dependency, } if !found { di.pendingPrompts = append(di.pendingPrompts, pendingPrompt{ - contractName: contractName, + contractName: dependency.Name, networkName: networkName, needsUpdate: true, updateHash: originalContractDataHash, @@ -605,7 +615,7 @@ func (di *DependencyInstaller) handleFoundContract(dependency config.Dependency, } // Check if this is a new dependency before updating state - isNewDep := di.State.Dependencies().ByName(contractName) == nil + isNewDep := di.State.Dependencies().ByName(dependency.Name) == nil err := di.updateDependencyState(dependency, originalContractDataHash) if err != nil { @@ -616,7 +626,7 @@ func (di *DependencyInstaller) handleFoundContract(dependency config.Dependency, // Handle additional tasks for new dependencies or when contract file doesn't exist // This makes sure prompts are collected for new dependencies regardless of whether contract file exists if isNewDep || !di.contractFileExists(contractAddr, contractName) { - err := di.handleAdditionalDependencyTasks(networkName, contractName) + err := di.handleAdditionalDependencyTasks(networkName, dependency.Name) if err != nil { di.Logger.Error(fmt.Sprintf("Error handling additional dependency tasks: %v", err)) return err @@ -786,6 +796,15 @@ func (di *DependencyInstaller) updateDependencyState(originalDependency config.D di.State.Dependencies().AddOrUpdate(dep) di.State.Contracts().AddDependencyAsContract(dep, originalDependency.Source.NetworkName) + // If this is an aliased import (Name differs from ContractName), set the Canonical field on the contract + // This enables flowkit to generate the correct "import X as Y from address" syntax + if dep.Name != dep.Source.ContractName { + contract, err := di.State.Contracts().ByName(dep.Name) + if err == nil && contract != nil { + contract.Canonical = dep.Source.ContractName + } + } + if isNewDep { msg := util.MessageWithEmojiPrefix("✅", fmt.Sprintf("%s added to flow.json", dep.Name)) di.logs.stateUpdates = append(di.logs.stateUpdates, msg) diff --git a/internal/dependencymanager/dependencyinstaller_test.go b/internal/dependencymanager/dependencyinstaller_test.go index 385e74bb6..f2f1ba1a5 100644 --- a/internal/dependencymanager/dependencyinstaller_test.go +++ b/internal/dependencymanager/dependencyinstaller_test.go @@ -675,3 +675,71 @@ func TestDependencyFlagsIntegration(t *testing.T) { assert.Nil(t, mainnetDeployment, "Should not create deployment on mainnet") }) } + +func TestAliasedImportHandling(t *testing.T) { + logger := output.NewStdoutLogger(output.NoneLog) + _, state, _ := util.TestMocks(t) + + gw := mocks.DefaultMockGateway() + + barAddr := flow.HexToAddress("0x0c") // testnet address hosting Bar + fooTestAddr := flow.HexToAddress("0x0b") // testnet Foo address + + t.Run("AliasedImportCreatesCanonicalMapping", func(t *testing.T) { + // Testnet GetAccount returns Bar at barAddr and Foo at fooTestAddr + gw.GetAccount.Run(func(args mock.Arguments) { + addr := args.Get(1).(flow.Address) + switch addr.String() { + case barAddr.String(): + acc := tests.NewAccountWithAddress(addr.String()) + // Bar imports Foo with an alias: import Foo as FooAlias from 0x0b + acc.Contracts = map[string][]byte{ + "Bar": []byte("import Foo as FooAlias from 0x0b\naccess(all) contract Bar {}"), + } + gw.GetAccount.Return(acc, nil) + case fooTestAddr.String(): + acc := tests.NewAccountWithAddress(addr.String()) + acc.Contracts = map[string][]byte{ + "Foo": []byte("access(all) contract Foo {}"), + } + gw.GetAccount.Return(acc, nil) + default: + gw.GetAccount.Return(nil, fmt.Errorf("not found")) + } + }) + + di := &DependencyInstaller{ + Gateways: map[string]gateway.Gateway{ + config.EmulatorNetwork.Name: gw.Mock, + config.TestnetNetwork.Name: gw.Mock, + config.MainnetNetwork.Name: gw.Mock, + }, + Logger: logger, + State: state, + SaveState: true, + TargetDir: "", + SkipDeployments: true, + SkipAlias: true, + dependencies: make(map[string]config.Dependency), + } + + err := di.AddBySourceString(fmt.Sprintf("%s://%s.%s", config.TestnetNetwork.Name, barAddr.String(), "Bar")) + assert.NoError(t, err) + + barDep := state.Dependencies().ByName("Bar") + assert.NotNil(t, barDep, "Bar dependency should exist") + + fooAliasDep := state.Dependencies().ByName("FooAlias") + assert.NotNil(t, fooAliasDep, "FooAlias dependency should exist") + assert.Equal(t, "Foo", fooAliasDep.Source.ContractName, "Source ContractName should be the actual contract name (Foo)") + + fooAliasContract, err := state.Contracts().ByName("FooAlias") + assert.NoError(t, err, "FooAlias contract should exist") + assert.Equal(t, "Foo", fooAliasContract.Canonical, "Canonical should be set to Foo") + + filePath := fmt.Sprintf("imports/%s/Foo.cdc", fooTestAddr.String()) + fileContent, err := state.ReaderWriter().ReadFile(filePath) + assert.NoError(t, err, "Contract file should exist at imports/address/Foo.cdc") + assert.NotNil(t, fileContent) + }) +}