Skip to content
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
39 changes: 29 additions & 10 deletions internal/dependencymanager/dependencyinstaller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens if imports > 0

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each address import statement imports exactly one contract, so imp.Imports always has one element. The outer loop handles multiple import statements

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,
},
}

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