Skip to content

Commit 59472c3

Browse files
authored
Merge pull request #2192 from onflow/cf/support-import-aliasing
Support Cadence import aliasing in Dependency Manager
2 parents 4806819 + 00c1a88 commit 59472c3

File tree

3 files changed

+246
-16
lines changed

3 files changed

+246
-16
lines changed

internal/dependencymanager/dependencyinstaller.go

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ type DependencyFlags struct {
103103
skipAlias bool `default:"false" flag:"skip-alias" info:"Skip prompting for an alias"`
104104
skipUpdatePrompts bool `default:"false" flag:"skip-update-prompts" info:"Skip prompting to update existing dependencies"`
105105
deploymentAccount string `default:"" flag:"deployment-account,d" info:"Account name to use for deployments (skips deployment account prompt)"`
106+
name string `default:"" flag:"name" info:"Import alias name for the dependency (sets canonical field for Cadence import aliasing)"`
106107
}
107108

108109
func (f *DependencyFlags) AddToCommand(cmd *cobra.Command) {
@@ -125,6 +126,7 @@ type DependencyInstaller struct {
125126
SkipDeployments bool
126127
SkipAlias bool
127128
DeploymentAccount string
129+
Name string
128130
logs categorizedLogs
129131
dependencies map[string]config.Dependency
130132
accountAliases map[string]map[string]flowsdk.Address // network -> account -> alias
@@ -164,6 +166,7 @@ func NewDependencyInstaller(logger output.Logger, state *flowkit.State, saveStat
164166
SkipDeployments: flags.skipDeployments,
165167
SkipAlias: flags.skipAlias,
166168
DeploymentAccount: flags.deploymentAccount,
169+
Name: flags.name,
167170
dependencies: make(map[string]config.Dependency),
168171
logs: categorizedLogs{},
169172
accountAliases: make(map[string]map[string]flowsdk.Address),
@@ -222,6 +225,13 @@ func (di *DependencyInstaller) AddBySourceString(depSource string) error {
222225
},
223226
}
224227

228+
// If a name is provided, use it as the import alias and set canonical for Cadence import aliasing
229+
// This enables "import OriginalContract as AliasName from address" syntax
230+
if di.Name != "" {
231+
dep.Name = di.Name
232+
dep.Canonical = depContractName
233+
}
234+
225235
return di.Add(dep)
226236
}
227237

@@ -257,6 +267,13 @@ func (di *DependencyInstaller) AddByCoreContractName(coreContractName string) er
257267
},
258268
}
259269

270+
// If a name is provided, use it as the import alias and set canonical for Cadence import aliasing
271+
// This enables "import OriginalContract as AliasName from address" syntax
272+
if di.Name != "" {
273+
dep.Name = di.Name
274+
dep.Canonical = depContractName
275+
}
276+
260277
return di.Add(dep)
261278
}
262279

@@ -275,6 +292,12 @@ func (di *DependencyInstaller) AddByDefiContractName(defiContractName string) er
275292
return fmt.Errorf("contract %s not found in DeFi actions contracts", defiContractName)
276293
}
277294

295+
// If a custom name is provided, use it as the dependency name and set canonical
296+
if di.Name != "" {
297+
targetDep.Name = di.Name
298+
targetDep.Canonical = defiContractName
299+
}
300+
278301
return di.Add(*targetDep)
279302
}
280303

@@ -333,6 +356,11 @@ func (di *DependencyInstaller) AddMany(dependencies []config.Dependency) error {
333356
}
334357

335358
func (di *DependencyInstaller) AddAllByNetworkAddress(sourceStr string) error {
359+
// Check if name flag is set - not supported when installing all contracts at an address
360+
if di.Name != "" {
361+
return fmt.Errorf("--name flag is not supported when installing all contracts at an address (network://address). Please specify a specific contract using network://address.ContractName format")
362+
}
363+
336364
network, address := ParseNetworkAddressString(sourceStr)
337365

338366
accountContracts, err := di.getContracts(network, flowsdk.HexToAddress(address))
@@ -495,16 +523,26 @@ func (di *DependencyInstaller) fetchDependenciesWithDepth(dependency config.Depe
495523
if program.HasAddressImports() {
496524
imports := program.AddressImportDeclarations()
497525
for _, imp := range imports {
498-
importContractName := imp.Imports[0].Identifier.Identifier
526+
527+
actualContractName := imp.Imports[0].Identifier.Identifier
499528
importAddress := flowsdk.HexToAddress(imp.Location.String())
500529

530+
// Check if this import has an alias (e.g., "import FUSD as FUSD1 from 0xaddress")
531+
// If aliased, use the alias as the dependency name so "import FUSD1" resolves correctly
532+
dependencyName := actualContractName
533+
if imp.Imports[0].Alias.Identifier != "" {
534+
dependencyName = imp.Imports[0].Alias.Identifier
535+
}
536+
501537
// Create a dependency for the import
538+
// Name is the alias (or actual name if not aliased) - this is what gets resolved in imports
539+
// ContractName is the actual contract name on chain - this is what gets fetched
502540
importDependency := config.Dependency{
503-
Name: importContractName,
541+
Name: dependencyName,
504542
Source: config.Source{
505543
NetworkName: networkName,
506544
Address: importAddress,
507-
ContractName: importContractName,
545+
ContractName: actualContractName,
508546
},
509547
}
510548

@@ -567,13 +605,13 @@ func (di *DependencyInstaller) handleFoundContract(dependency config.Dependency,
567605
program.ConvertAddressImports()
568606
contractData := string(program.CodeWithUnprocessedImports())
569607

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

572610
// If a dependency by this name already exists and its remote source network or address does not match,
573611
// allow it only if an existing alias matches the incoming network+address; otherwise terminate.
574612
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))
613+
if !di.existingAliasMatches(dependency.Name, networkName, contractAddr) {
614+
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))
577615
os.Exit(0)
578616
return nil
579617
}
@@ -588,7 +626,7 @@ func (di *DependencyInstaller) handleFoundContract(dependency config.Dependency,
588626
// Find existing pending prompt for this contract or create new one
589627
found := false
590628
for i := range di.pendingPrompts {
591-
if di.pendingPrompts[i].contractName == contractName {
629+
if di.pendingPrompts[i].contractName == dependency.Name {
592630
di.pendingPrompts[i].needsUpdate = true
593631
di.pendingPrompts[i].updateHash = originalContractDataHash
594632
found = true
@@ -597,7 +635,7 @@ func (di *DependencyInstaller) handleFoundContract(dependency config.Dependency,
597635
}
598636
if !found {
599637
di.pendingPrompts = append(di.pendingPrompts, pendingPrompt{
600-
contractName: contractName,
638+
contractName: dependency.Name,
601639
networkName: networkName,
602640
needsUpdate: true,
603641
updateHash: originalContractDataHash,
@@ -607,7 +645,7 @@ func (di *DependencyInstaller) handleFoundContract(dependency config.Dependency,
607645
}
608646

609647
// Check if this is a new dependency before updating state
610-
isNewDep := di.State.Dependencies().ByName(contractName) == nil
648+
isNewDep := di.State.Dependencies().ByName(dependency.Name) == nil
611649

612650
err := di.updateDependencyState(dependency, originalContractDataHash)
613651
if err != nil {
@@ -618,7 +656,7 @@ func (di *DependencyInstaller) handleFoundContract(dependency config.Dependency,
618656
// Handle additional tasks for new dependencies or when contract file doesn't exist
619657
// This makes sure prompts are collected for new dependencies regardless of whether contract file exists
620658
if isNewDep || !di.contractFileExists(contractAddr, contractName) {
621-
err := di.handleAdditionalDependencyTasks(networkName, contractName)
659+
err := di.handleAdditionalDependencyTasks(networkName, dependency.Name)
622660
if err != nil {
623661
di.Logger.Error(fmt.Sprintf("Error handling additional dependency tasks: %v", err))
624662
return err
@@ -775,19 +813,29 @@ func (di *DependencyInstaller) updateDependencyAlias(contractName, aliasNetwork
775813
}
776814

777815
func (di *DependencyInstaller) updateDependencyState(originalDependency config.Dependency, contractHash string) error {
778-
// Create the dependency to save, preserving aliases from the original
816+
// Create the dependency to save, preserving aliases and canonical from the original
779817
dep := config.Dependency{
780-
Name: originalDependency.Name,
781-
Source: originalDependency.Source,
782-
Hash: contractHash,
783-
Aliases: originalDependency.Aliases, // Preserve aliases from the original dependency
818+
Name: originalDependency.Name,
819+
Source: originalDependency.Source,
820+
Hash: contractHash,
821+
Aliases: originalDependency.Aliases,
822+
Canonical: originalDependency.Canonical,
784823
}
785824

786825
isNewDep := di.State.Dependencies().ByName(dep.Name) == nil
787826

788827
di.State.Dependencies().AddOrUpdate(dep)
789828
di.State.Contracts().AddDependencyAsContract(dep, originalDependency.Source.NetworkName)
790829

830+
// If this is an aliased import (Name differs from ContractName), set the Canonical field on the contract
831+
// This enables flowkit to generate the correct "import X as Y from address" syntax
832+
if dep.Name != dep.Source.ContractName {
833+
contract, err := di.State.Contracts().ByName(dep.Name)
834+
if err == nil && contract != nil {
835+
contract.Canonical = dep.Source.ContractName
836+
}
837+
}
838+
791839
if isNewDep {
792840
msg := util.MessageWithEmojiPrefix("✅", fmt.Sprintf("%s added to flow.json", dep.Name))
793841
di.logs.stateUpdates = append(di.logs.stateUpdates, msg)

internal/dependencymanager/dependencyinstaller_test.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,3 +675,178 @@ 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+
}
746+
747+
func TestDependencyInstallerWithAlias(t *testing.T) {
748+
logger := output.NewStdoutLogger(output.NoneLog)
749+
_, state, _ := util.TestMocks(t)
750+
751+
serviceAcc, _ := state.EmulatorServiceAccount()
752+
serviceAddress := serviceAcc.Address
753+
754+
t.Run("AddBySourceStringWithName", func(t *testing.T) {
755+
gw := mocks.DefaultMockGateway()
756+
757+
gw.GetAccount.Run(func(args mock.Arguments) {
758+
addr := args.Get(1).(flow.Address)
759+
assert.Equal(t, addr.String(), serviceAddress.String())
760+
acc := tests.NewAccountWithAddress(addr.String())
761+
acc.Contracts = map[string][]byte{
762+
"NumberFormatter": []byte("access(all) contract NumberFormatter {}"),
763+
}
764+
gw.GetAccount.Return(acc, nil)
765+
})
766+
767+
di := &DependencyInstaller{
768+
Gateways: map[string]gateway.Gateway{
769+
config.EmulatorNetwork.Name: gw.Mock,
770+
},
771+
Logger: logger,
772+
State: state,
773+
SaveState: true,
774+
TargetDir: "",
775+
SkipDeployments: true,
776+
SkipAlias: true,
777+
Name: "NumberFormatterCustom",
778+
dependencies: make(map[string]config.Dependency),
779+
}
780+
781+
err := di.AddBySourceString(fmt.Sprintf("%s://%s.%s", config.EmulatorNetwork.Name, serviceAddress.String(), "NumberFormatter"))
782+
assert.NoError(t, err, "Failed to add dependency with import alias")
783+
784+
// Check that the dependency was added with the import alias name
785+
dep := state.Dependencies().ByName("NumberFormatterCustom")
786+
assert.NotNil(t, dep, "Dependency should exist with import alias name")
787+
assert.Equal(t, "NumberFormatter", dep.Source.ContractName, "Source ContractName should be the actual contract name")
788+
assert.Equal(t, "NumberFormatter", dep.Canonical, "Canonical should be set to the actual contract name for import aliasing")
789+
790+
// Check that the contract was added with canonical field for Cadence import aliasing
791+
contract, err := state.Contracts().ByName("NumberFormatterCustom")
792+
assert.NoError(t, err, "Contract should exist")
793+
assert.Equal(t, "NumberFormatter", contract.Canonical, "Contract Canonical should be set for import aliasing")
794+
795+
// Check that the file was created with the actual contract name
796+
filePath := fmt.Sprintf("imports/%s/NumberFormatter.cdc", serviceAddress.String())
797+
fileContent, err := state.ReaderWriter().ReadFile(filePath)
798+
assert.NoError(t, err, "Contract file should exist at imports/address/NumberFormatter.cdc")
799+
assert.NotNil(t, fileContent)
800+
})
801+
802+
t.Run("AddByCoreContractNameWithName", func(t *testing.T) {
803+
// Mock the gateway to return FlowToken contract
804+
gw := mocks.DefaultMockGateway()
805+
gw.GetAccount.Run(func(args mock.Arguments) {
806+
addr := args.Get(1).(flow.Address)
807+
acc := tests.NewAccountWithAddress(addr.String())
808+
acc.Contracts = map[string][]byte{
809+
"FlowToken": []byte("access(all) contract FlowToken {}"),
810+
}
811+
gw.GetAccount.Return(acc, nil)
812+
})
813+
814+
di := &DependencyInstaller{
815+
Gateways: map[string]gateway.Gateway{
816+
config.MainnetNetwork.Name: gw.Mock,
817+
},
818+
Logger: logger,
819+
State: state,
820+
SaveState: true,
821+
TargetDir: "",
822+
SkipDeployments: true,
823+
SkipAlias: true,
824+
Name: "FlowTokenCustom",
825+
dependencies: make(map[string]config.Dependency),
826+
}
827+
828+
err := di.AddByCoreContractName("FlowToken")
829+
assert.NoError(t, err, "Failed to add core contract with import alias")
830+
831+
// Check that the dependency was added with the import alias name
832+
dep := state.Dependencies().ByName("FlowTokenCustom")
833+
assert.NotNil(t, dep, "Dependency should exist with import alias name")
834+
assert.Equal(t, "FlowToken", dep.Source.ContractName, "Source ContractName should be FlowToken")
835+
assert.Equal(t, "FlowToken", dep.Canonical, "Canonical should be set to FlowToken for import aliasing")
836+
})
837+
838+
t.Run("AddAllByNetworkAddressWithNameError", func(t *testing.T) {
839+
// This test doesn't need gateways since it returns an error before making any gateway calls
840+
di := &DependencyInstaller{
841+
Logger: logger,
842+
State: state,
843+
SaveState: true,
844+
TargetDir: "",
845+
Name: "SomeName",
846+
}
847+
848+
err := di.AddAllByNetworkAddress(fmt.Sprintf("%s://%s", config.EmulatorNetwork.Name, serviceAddress.String()))
849+
assert.Error(t, err, "Should error when using --name with network://address format")
850+
assert.Contains(t, err.Error(), "--name flag is not supported when installing all contracts", "Error message should mention name flag limitation")
851+
})
852+
}

0 commit comments

Comments
 (0)