From 16821c5d75c533d71c53373d67d6a0fe0b922ac6 Mon Sep 17 00:00:00 2001 From: Chase Fleming <1666730+chasefleming@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:42:45 -0800 Subject: [PATCH 1/4] Support import aliasing in dependencies --- config/config.go | 8 +++ config/contract.go | 1 + config/dependency.go | 21 ++++++-- config/dependency_test.go | 31 ++++++++++++ config/json/dependency.go | 19 +++++--- config/json/dependency_test.go | 89 ++++++++++++++++++++++++++++++++++ schema.json | 3 ++ state.go | 14 +++++- 8 files changed, 172 insertions(+), 14 deletions(-) diff --git a/config/config.go b/config/config.go index 3781ba32b..d97f0bd48 100644 --- a/config/config.go +++ b/config/config.go @@ -61,6 +61,14 @@ func (c *Config) Validate() error { } } + if err := c.Contracts.ValidateCanonical(); err != nil { + return err + } + + if err := c.Dependencies.ValidateCanonical(); err != nil { + return err + } + for _, em := range c.Emulators { if _, err := c.Accounts.ByName(em.ServiceAccount); err != nil { return fmt.Errorf("emulator %s contains nonexisting service account %s", em.Name, em.ServiceAccount) diff --git a/config/contract.go b/config/contract.go index d04fd3ced..5e781f73a 100644 --- a/config/contract.go +++ b/config/contract.go @@ -228,6 +228,7 @@ func (c *Contracts) AddDependencyAsContract(dependency Dependency, networkName s Location: filepath.ToSlash(fmt.Sprintf("%s/%s/%s.cdc", dependencyManagerDirectory, dependency.Source.Address, dependency.Source.ContractName)), Aliases: aliases, IsDependency: true, + Canonical: dependency.Canonical, } if _, err := c.ByName(contract.Name); err != nil { diff --git a/config/dependency.go b/config/dependency.go index 68c862a19..95ee75176 100644 --- a/config/dependency.go +++ b/config/dependency.go @@ -49,10 +49,11 @@ type Source struct { } type Dependency struct { - Name string - Source Source - Hash string - Aliases Aliases + Name string + Source Source + Hash string + Aliases Aliases + Canonical string } type Dependencies []Dependency @@ -77,3 +78,15 @@ func (d *Dependencies) AddOrUpdate(dep Dependency) { *d = append(*d, dep) } + +// ValidateCanonical validates that all canonical references are valid for dependencies. +func (d *Dependencies) ValidateCanonical() error { + for _, dependency := range *d { + if dependency.Canonical != "" { + if dependency.Canonical == dependency.Name { + return fmt.Errorf("dependency %s cannot have itself as canonical", dependency.Name) + } + } + } + return nil +} diff --git a/config/dependency_test.go b/config/dependency_test.go index b3f00499f..f023bc8ed 100644 --- a/config/dependency_test.go +++ b/config/dependency_test.go @@ -42,3 +42,34 @@ func TestDependencies_AddOrUpdate(t *testing.T) { dep := dependencies.ByName("mydep") assert.NotNil(t, dep) } + +func TestDependencies_ValidateCanonical(t *testing.T) { + t.Run("valid canonical reference", func(t *testing.T) { + dependencies := Dependencies{ + Dependency{Name: "NumberFormatter", Canonical: ""}, + Dependency{Name: "NumberFormatterAlias", Canonical: "NumberFormatter"}, + } + + err := dependencies.ValidateCanonical() + assert.NoError(t, err) + }) + + t.Run("self-referential canonical", func(t *testing.T) { + dependencies := Dependencies{ + Dependency{Name: "NumberFormatter", Canonical: "NumberFormatter"}, + } + + err := dependencies.ValidateCanonical() + assert.Error(t, err) + assert.Contains(t, err.Error(), "cannot have itself as canonical") + }) + + t.Run("no canonical reference", func(t *testing.T) { + dependencies := Dependencies{ + Dependency{Name: "NumberFormatter", Canonical: ""}, + } + + err := dependencies.ValidateCanonical() + assert.NoError(t, err) + }) +} diff --git a/config/json/dependency.go b/config/json/dependency.go index 8887a8841..803a9cdef 100644 --- a/config/json/dependency.go +++ b/config/json/dependency.go @@ -59,8 +59,9 @@ func (j jsonDependencies) transformToConfig() (config.Dependencies, error) { } dep = config.Dependency{ - Name: dependencyName, - Hash: dependency.Extended.Hash, + Name: dependencyName, + Hash: dependency.Extended.Hash, + Canonical: dependency.Extended.Canonical, Source: config.Source{ NetworkName: depNetwork, Address: flow.HexToAddress(depAddress), @@ -99,9 +100,10 @@ func transformDependenciesToJSON(configDependencies config.Dependencies, configC jsonDeps[dep.Name] = jsonDependency{ Extended: jsonDependencyExtended{ - Source: buildSourceString(dep.Source), - Hash: dep.Hash, - Aliases: aliases, + Source: buildSourceString(dep.Source), + Hash: dep.Hash, + Aliases: aliases, + Canonical: dep.Canonical, }, } } @@ -123,9 +125,10 @@ func buildSourceString(source config.Source) string { // jsonDependencyExtended for json parsing advanced config. type jsonDependencyExtended struct { - Source string `json:"source"` - Hash string `json:"hash"` - Aliases map[string]string `json:"aliases"` + Source string `json:"source"` + Hash string `json:"hash"` + Aliases map[string]string `json:"aliases"` + Canonical string `json:"canonical,omitempty"` } // jsonDependency structure for json parsing. diff --git a/config/json/dependency_test.go b/config/json/dependency_test.go index 99467bbd9..1d493e6ca 100644 --- a/config/json/dependency_test.go +++ b/config/json/dependency_test.go @@ -76,3 +76,92 @@ func Test_TransformDependenciesToJSON(t *testing.T) { assert.Equal(t, cleanSpecialChars(bOut), cleanSpecialChars(x)) } + +func Test_ConfigDependenciesWithCanonical(t *testing.T) { + b := []byte(`{ + "NumberFormatter": { + "source": "testnet://8a4dce54554b225d.NumberFormatter", + "hash": "dc7043832da46dbcc8242a53fa95b37f020bc374df42586a62703b2651979fb9", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "testnet": "8a4dce54554b225d" + } + }, + "NumberFormatterAlias": { + "source": "testnet://8a4dce54554b225d.NumberFormatter", + "hash": "dc7043832da46dbcc8242a53fa95b37f020bc374df42586a62703b2651979fb9", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "testnet": "8a4dce54554b225d" + }, + "canonical": "NumberFormatter" + } + }`) + + var jsonDependencies jsonDependencies + err := json.Unmarshal(b, &jsonDependencies) + assert.NoError(t, err) + + dependencies, err := jsonDependencies.transformToConfig() + assert.NoError(t, err) + + assert.Len(t, dependencies, 2) + + // Check canonical dependency + canonicalDep := dependencies.ByName("NumberFormatter") + assert.NotNil(t, canonicalDep) + assert.Equal(t, "", canonicalDep.Canonical) + + // Check alias dependency + aliasDep := dependencies.ByName("NumberFormatterAlias") + assert.NotNil(t, aliasDep) + assert.Equal(t, "NumberFormatter", aliasDep.Canonical) +} + +func Test_TransformDependenciesWithCanonicalToJSON(t *testing.T) { + b := []byte(`{ + "NumberFormatter": { + "source": "testnet://8a4dce54554b225d.NumberFormatter", + "hash": "dc7043832da46dbcc8242a53fa95b37f020bc374df42586a62703b2651979fb9", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "testnet": "8a4dce54554b225d" + } + }, + "NumberFormatterAlias": { + "source": "testnet://8a4dce54554b225d.NumberFormatter", + "hash": "dc7043832da46dbcc8242a53fa95b37f020bc374df42586a62703b2651979fb9", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "testnet": "8a4dce54554b225d" + }, + "canonical": "NumberFormatter" + } + }`) + + var jsonContracts jsonContracts + errContracts := json.Unmarshal(b, &jsonContracts) + assert.NoError(t, errContracts) + + var jsonDependencies jsonDependencies + err := json.Unmarshal(b, &jsonDependencies) + assert.NoError(t, err) + + contracts, err := jsonContracts.transformToConfig() + assert.NoError(t, err) + dependencies, err := jsonDependencies.transformToConfig() + assert.NoError(t, err) + + j := transformDependenciesToJSON(dependencies, contracts) + + // Marshal and check + x, _ := json.Marshal(j) + + // Parse back and check canonical field + var result map[string]jsonDependency + err = json.Unmarshal(x, &result) + assert.NoError(t, err) + + assert.Equal(t, "", result["NumberFormatter"].Extended.Canonical) + assert.Equal(t, "NumberFormatter", result["NumberFormatterAlias"].Extended.Canonical) +} diff --git a/schema.json b/schema.json index 4f9b24536..ef122f511 100644 --- a/schema.json +++ b/schema.json @@ -248,6 +248,9 @@ } }, "type": "object" + }, + "canonical": { + "type": "string" } }, "additionalProperties": false, diff --git a/state.go b/state.go index ee95b103f..00245caa1 100644 --- a/state.go +++ b/state.go @@ -301,20 +301,30 @@ func (p *State) AliasesForNetwork(network config.Network) project.LocationAliase // // Get canonical mappings // mapping := state.CanonicalContractMapping() // // Returns: {"FUSD_v2": "FUSD"} -// +// // // Use with import replacer // contracts, _ := state.DeploymentContractsByNetwork(network) // aliases := state.AliasesForNetwork(network) // replacer := project.NewImportReplacer(contracts, aliases, mapping) -// +// // // Process imports: "FUSD_v2" → "import FUSD as FUSD_v2 from 0x..." func (p *State) CanonicalContractMapping() map[string]string { canonicalMapping := make(map[string]string) + + // Include canonical mappings from contracts for _, contract := range p.conf.Contracts { if contract.IsAlias() { canonicalMapping[contract.Name] = contract.Canonical } } + + // Include canonical mappings from dependencies + for _, dependency := range p.conf.Dependencies { + if dependency.Canonical != "" { + canonicalMapping[dependency.Name] = dependency.Canonical + } + } + return canonicalMapping } From b65655c6649248ba1b6429b6b6f2f6abcb55c45c Mon Sep 17 00:00:00 2001 From: Chase Fleming <1666730+chasefleming@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:23:46 -0800 Subject: [PATCH 2/4] Remove comments --- config/json/dependency_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/config/json/dependency_test.go b/config/json/dependency_test.go index 1d493e6ca..f68050386 100644 --- a/config/json/dependency_test.go +++ b/config/json/dependency_test.go @@ -107,12 +107,10 @@ func Test_ConfigDependenciesWithCanonical(t *testing.T) { assert.Len(t, dependencies, 2) - // Check canonical dependency canonicalDep := dependencies.ByName("NumberFormatter") assert.NotNil(t, canonicalDep) assert.Equal(t, "", canonicalDep.Canonical) - // Check alias dependency aliasDep := dependencies.ByName("NumberFormatterAlias") assert.NotNil(t, aliasDep) assert.Equal(t, "NumberFormatter", aliasDep.Canonical) From 40d33aa28bd8121c3b008c69897e1ca5b431f8ed Mon Sep 17 00:00:00 2001 From: Chase Fleming <1666730+chasefleming@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:25:01 -0800 Subject: [PATCH 3/4] Remove --- config/json/dependency_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/config/json/dependency_test.go b/config/json/dependency_test.go index f68050386..bac31f56f 100644 --- a/config/json/dependency_test.go +++ b/config/json/dependency_test.go @@ -152,7 +152,6 @@ func Test_TransformDependenciesWithCanonicalToJSON(t *testing.T) { j := transformDependenciesToJSON(dependencies, contracts) - // Marshal and check x, _ := json.Marshal(j) // Parse back and check canonical field From 1c26ac19ea82260fd766d38f525b4b6a79678c2c Mon Sep 17 00:00:00 2001 From: Chase Fleming <1666730+chasefleming@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:33:33 -0800 Subject: [PATCH 4/4] Add contract tests --- config/json/contract_test.go | 101 +++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/config/json/contract_test.go b/config/json/contract_test.go index bd9ab0800..b87a00498 100644 --- a/config/json/contract_test.go +++ b/config/json/contract_test.go @@ -185,3 +185,104 @@ func Test_TransformComplexContractToJSON(t *testing.T) { assert.JSONEq(t, string(b), string(x)) } + +func Test_ConfigContractsWithCanonical(t *testing.T) { + b := []byte(`{ + "FUSD": { + "source": "./contracts/FUSD.cdc", + "aliases": { + "testnet": "9a0766d93b6608b7", + "mainnet": "3c5959b568896393" + } + }, + "FUSD1": { + "source": "./contracts/FUSD.cdc", + "aliases": { + "testnet": "e223d8a629e49c68", + "mainnet": "8d0e87b65159ae63" + }, + "canonical": "FUSD" + }, + "FUSD2": { + "source": "./contracts/FUSD.cdc", + "aliases": { + "testnet": "0f9df91c9121c460", + "mainnet": "754a90d51a1c8e1b" + }, + "canonical": "FUSD" + } + }`) + + var jsonContracts jsonContracts + err := json.Unmarshal(b, &jsonContracts) + assert.NoError(t, err) + + contracts, err := jsonContracts.transformToConfig() + assert.NoError(t, err) + + assert.Len(t, contracts, 3) + + fusd, _ := contracts.ByName("FUSD") + assert.NotNil(t, fusd) + assert.Equal(t, "", fusd.Canonical) + assert.False(t, fusd.IsAlias()) + + fusd1, _ := contracts.ByName("FUSD1") + assert.NotNil(t, fusd1) + assert.Equal(t, "FUSD", fusd1.Canonical) + assert.True(t, fusd1.IsAlias()) + assert.Equal(t, "FUSD", fusd1.CanonicalName()) + + fusd2, _ := contracts.ByName("FUSD2") + assert.NotNil(t, fusd2) + assert.Equal(t, "FUSD", fusd2.Canonical) + assert.True(t, fusd2.IsAlias()) + assert.Equal(t, "FUSD", fusd2.CanonicalName()) +} + +func Test_TransformContractsWithCanonicalToJSON(t *testing.T) { + b := []byte(`{ + "FUSD": { + "source": "./contracts/FUSD.cdc", + "aliases": { + "testnet": "9a0766d93b6608b7", + "mainnet": "3c5959b568896393" + } + }, + "FUSD1": { + "source": "./contracts/FUSD.cdc", + "aliases": { + "testnet": "e223d8a629e49c68", + "mainnet": "8d0e87b65159ae63" + }, + "canonical": "FUSD" + }, + "FUSD2": { + "source": "./contracts/FUSD.cdc", + "aliases": { + "testnet": "0f9df91c9121c460", + "mainnet": "754a90d51a1c8e1b" + }, + "canonical": "FUSD" + } + }`) + + var jsonContracts jsonContracts + err := json.Unmarshal(b, &jsonContracts) + assert.NoError(t, err) + + contracts, err := jsonContracts.transformToConfig() + assert.NoError(t, err) + + j := transformContractsToJSON(contracts) + + x, _ := json.Marshal(j) + + var result map[string]jsonContract + err = json.Unmarshal(x, &result) + assert.NoError(t, err) + + assert.Equal(t, "", result["FUSD"].Advanced.Canonical) + assert.Equal(t, "FUSD", result["FUSD1"].Advanced.Canonical) + assert.Equal(t, "FUSD", result["FUSD2"].Advanced.Canonical) +}