Skip to content

Commit 535cefc

Browse files
committed
Support import aliasing in Dependency Manager
1 parent bfb9c1d commit 535cefc

2 files changed

Lines changed: 97 additions & 10 deletions

File tree

internal/dependencymanager/dependencyinstaller.go

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -495,16 +495,26 @@ func (di *DependencyInstaller) fetchDependenciesWithDepth(dependency config.Depe
495495
if program.HasAddressImports() {
496496
imports := program.AddressImportDeclarations()
497497
for _, imp := range imports {
498-
importContractName := imp.Imports[0].Identifier.Identifier
498+
499+
actualContractName := imp.Imports[0].Identifier.Identifier
499500
importAddress := flowsdk.HexToAddress(imp.Location.String())
500501

502+
// Check if this import has an alias (e.g., "import FUSD as FUSD1 from 0xaddress")
503+
// If aliased, use the alias as the dependency name so "import FUSD1" resolves correctly
504+
dependencyName := actualContractName
505+
if imp.Imports[0].Alias.Identifier != "" {
506+
dependencyName = imp.Imports[0].Alias.Identifier
507+
}
508+
501509
// Create a dependency for the import
510+
// Name is the alias (or actual name if not aliased) - this is what gets resolved in imports
511+
// ContractName is the actual contract name on chain - this is what gets fetched
502512
importDependency := config.Dependency{
503-
Name: importContractName,
513+
Name: dependencyName,
504514
Source: config.Source{
505515
NetworkName: networkName,
506516
Address: importAddress,
507-
ContractName: importContractName,
517+
ContractName: actualContractName,
508518
},
509519
}
510520

@@ -567,13 +577,13 @@ func (di *DependencyInstaller) handleFoundContract(dependency config.Dependency,
567577
program.ConvertAddressImports()
568578
contractData := string(program.CodeWithUnprocessedImports())
569579

570-
existingDependency := di.State.Dependencies().ByName(contractName)
580+
existingDependency := di.State.Dependencies().ByName(dependency.Name)
571581

572582
// If a dependency by this name already exists and its remote source network or address does not match,
573583
// allow it only if an existing alias matches the incoming network+address; otherwise terminate.
574584
if existingDependency != nil && (existingDependency.Source.NetworkName != networkName || existingDependency.Source.Address.String() != contractAddr) {
575-
if !di.existingAliasMatches(contractName, networkName, contractAddr) {
576-
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))
585+
if !di.existingAliasMatches(dependency.Name, networkName, contractAddr) {
586+
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))
577587
os.Exit(0)
578588
return nil
579589
}
@@ -586,7 +596,7 @@ func (di *DependencyInstaller) handleFoundContract(dependency config.Dependency,
586596
// Find existing pending prompt for this contract or create new one
587597
found := false
588598
for i := range di.pendingPrompts {
589-
if di.pendingPrompts[i].contractName == contractName {
599+
if di.pendingPrompts[i].contractName == dependency.Name {
590600
di.pendingPrompts[i].needsUpdate = true
591601
di.pendingPrompts[i].updateHash = originalContractDataHash
592602
found = true
@@ -595,7 +605,7 @@ func (di *DependencyInstaller) handleFoundContract(dependency config.Dependency,
595605
}
596606
if !found {
597607
di.pendingPrompts = append(di.pendingPrompts, pendingPrompt{
598-
contractName: contractName,
608+
contractName: dependency.Name,
599609
networkName: networkName,
600610
needsUpdate: true,
601611
updateHash: originalContractDataHash,
@@ -605,7 +615,7 @@ func (di *DependencyInstaller) handleFoundContract(dependency config.Dependency,
605615
}
606616

607617
// Check if this is a new dependency before updating state
608-
isNewDep := di.State.Dependencies().ByName(contractName) == nil
618+
isNewDep := di.State.Dependencies().ByName(dependency.Name) == nil
609619

610620
err := di.updateDependencyState(dependency, originalContractDataHash)
611621
if err != nil {
@@ -616,7 +626,7 @@ func (di *DependencyInstaller) handleFoundContract(dependency config.Dependency,
616626
// Handle additional tasks for new dependencies or when contract file doesn't exist
617627
// This makes sure prompts are collected for new dependencies regardless of whether contract file exists
618628
if isNewDep || !di.contractFileExists(contractAddr, contractName) {
619-
err := di.handleAdditionalDependencyTasks(networkName, contractName)
629+
err := di.handleAdditionalDependencyTasks(networkName, dependency.Name)
620630
if err != nil {
621631
di.Logger.Error(fmt.Sprintf("Error handling additional dependency tasks: %v", err))
622632
return err
@@ -786,6 +796,15 @@ func (di *DependencyInstaller) updateDependencyState(originalDependency config.D
786796
di.State.Dependencies().AddOrUpdate(dep)
787797
di.State.Contracts().AddDependencyAsContract(dep, originalDependency.Source.NetworkName)
788798

799+
// If this is an aliased import (Name differs from ContractName), set the Canonical field on the contract
800+
// This enables flowkit to generate the correct "import X as Y from address" syntax
801+
if dep.Name != dep.Source.ContractName {
802+
contract, err := di.State.Contracts().ByName(dep.Name)
803+
if err == nil && contract != nil {
804+
contract.Canonical = dep.Source.ContractName
805+
}
806+
}
807+
789808
if isNewDep {
790809
msg := util.MessageWithEmojiPrefix("✅", fmt.Sprintf("%s added to flow.json", dep.Name))
791810
di.logs.stateUpdates = append(di.logs.stateUpdates, msg)

internal/dependencymanager/dependencyinstaller_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,3 +675,71 @@ func TestDependencyFlagsIntegration(t *testing.T) {
675675
assert.Nil(t, mainnetDeployment, "Should not create deployment on mainnet")
676676
})
677677
}
678+
679+
func TestAliasedImportHandling(t *testing.T) {
680+
logger := output.NewStdoutLogger(output.NoneLog)
681+
_, state, _ := util.TestMocks(t)
682+
683+
gw := mocks.DefaultMockGateway()
684+
685+
barAddr := flow.HexToAddress("0x0c") // testnet address hosting Bar
686+
fooTestAddr := flow.HexToAddress("0x0b") // testnet Foo address
687+
688+
t.Run("AliasedImportCreatesCanonicalMapping", func(t *testing.T) {
689+
// Testnet GetAccount returns Bar at barAddr and Foo at fooTestAddr
690+
gw.GetAccount.Run(func(args mock.Arguments) {
691+
addr := args.Get(1).(flow.Address)
692+
switch addr.String() {
693+
case barAddr.String():
694+
acc := tests.NewAccountWithAddress(addr.String())
695+
// Bar imports Foo with an alias: import Foo as FooAlias from 0x0b
696+
acc.Contracts = map[string][]byte{
697+
"Bar": []byte("import Foo as FooAlias from 0x0b\naccess(all) contract Bar {}"),
698+
}
699+
gw.GetAccount.Return(acc, nil)
700+
case fooTestAddr.String():
701+
acc := tests.NewAccountWithAddress(addr.String())
702+
acc.Contracts = map[string][]byte{
703+
"Foo": []byte("access(all) contract Foo {}"),
704+
}
705+
gw.GetAccount.Return(acc, nil)
706+
default:
707+
gw.GetAccount.Return(nil, fmt.Errorf("not found"))
708+
}
709+
})
710+
711+
di := &DependencyInstaller{
712+
Gateways: map[string]gateway.Gateway{
713+
config.EmulatorNetwork.Name: gw.Mock,
714+
config.TestnetNetwork.Name: gw.Mock,
715+
config.MainnetNetwork.Name: gw.Mock,
716+
},
717+
Logger: logger,
718+
State: state,
719+
SaveState: true,
720+
TargetDir: "",
721+
SkipDeployments: true,
722+
SkipAlias: true,
723+
dependencies: make(map[string]config.Dependency),
724+
}
725+
726+
err := di.AddBySourceString(fmt.Sprintf("%s://%s.%s", config.TestnetNetwork.Name, barAddr.String(), "Bar"))
727+
assert.NoError(t, err)
728+
729+
barDep := state.Dependencies().ByName("Bar")
730+
assert.NotNil(t, barDep, "Bar dependency should exist")
731+
732+
fooAliasDep := state.Dependencies().ByName("FooAlias")
733+
assert.NotNil(t, fooAliasDep, "FooAlias dependency should exist")
734+
assert.Equal(t, "Foo", fooAliasDep.Source.ContractName, "Source ContractName should be the actual contract name (Foo)")
735+
736+
fooAliasContract, err := state.Contracts().ByName("FooAlias")
737+
assert.NoError(t, err, "FooAlias contract should exist")
738+
assert.Equal(t, "Foo", fooAliasContract.Canonical, "Canonical should be set to Foo")
739+
740+
filePath := fmt.Sprintf("imports/%s/Foo.cdc", fooTestAddr.String())
741+
fileContent, err := state.ReaderWriter().ReadFile(filePath)
742+
assert.NoError(t, err, "Contract file should exist at imports/address/Foo.cdc")
743+
assert.NotNil(t, fileContent)
744+
})
745+
}

0 commit comments

Comments
 (0)