From 69d8de882023a0ce178eaac16aa8bad1963205f3 Mon Sep 17 00:00:00 2001 From: Chase Fleming <1666730+chasefleming@users.noreply.github.com> Date: Fri, 7 Nov 2025 13:44:34 -0800 Subject: [PATCH 01/10] Add contract helpers --- config/contract.go | 42 +++++++++++++ config/contract_test.go | 127 ++++++++++++++++++++++++++++++++++++++++ config/json/contract.go | 19 +++--- 3 files changed, 180 insertions(+), 8 deletions(-) diff --git a/config/contract.go b/config/contract.go index bcfa883d7..710d31cb7 100644 --- a/config/contract.go +++ b/config/contract.go @@ -35,6 +35,7 @@ type Contract struct { Location string Aliases Aliases IsDependency bool + Canonical string // Reference to canonical contract name if this is an alias } // Alias defines an existing pre-deployed contract address for specific network. @@ -74,6 +75,19 @@ func (c *Contract) IsAliased() bool { return len(c.Aliases) > 0 } +// IsAlias checks if this contract is an alias to another contract. +func (c *Contract) IsAlias() bool { + return c.Canonical != "" +} + +// CanonicalName returns the canonical contract name if this is an alias, otherwise returns the contract's own name. +func (c *Contract) CanonicalName() string { + if c.Canonical != "" { + return c.Canonical + } + return c.Name +} + // ByName get contract by name or return an error if it doesn't exist. func (c *Contracts) ByName(name string) (*Contract, error) { for i, contract := range *c { @@ -112,6 +126,34 @@ func (c *Contracts) Remove(name string) error { return nil } +// ValidateCanonical validates that all canonical references are valid. +func (c *Contracts) ValidateCanonical() error { + for _, contract := range *c { + if contract.Canonical != "" { + // Check self-reference + if contract.Canonical == contract.Name { + return fmt.Errorf("contract %s cannot have itself as canonical", contract.Name) + } + // Check target exists + if _, err := c.ByName(contract.Canonical); err != nil { + return fmt.Errorf("canonical contract %s referenced by %s does not exist", contract.Canonical, contract.Name) + } + } + } + return nil +} + +// GetAliases returns all contracts that have the given contract as their canonical. +func (c *Contracts) GetAliases(canonicalName string) []*Contract { + var aliases []*Contract + for i, contract := range *c { + if contract.Canonical == canonicalName { + aliases = append(aliases, &(*c)[i]) + } + } + return aliases +} + const dependencyManagerDirectory = "imports" const ( diff --git a/config/contract_test.go b/config/contract_test.go index 2b9ab21f0..0fb63b7dd 100644 --- a/config/contract_test.go +++ b/config/contract_test.go @@ -109,3 +109,130 @@ func TestContracts_AddDependencyAsContract(t *testing.T) { assert.Equal(t, "imports/0000000000abcdef/TestContract.cdc", contract.Location) assert.Len(t, contract.Aliases, 1) } + +func TestContract_IsAlias(t *testing.T) { + tests := []struct { + name string + contract Contract + expected bool + }{ + { + name: "contract with canonical is an alias", + contract: Contract{Name: "FUSD1", Canonical: "FUSD"}, + expected: true, + }, + { + name: "contract without canonical is not an alias", + contract: Contract{Name: "FUSD"}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.contract.IsAlias()) + }) + } +} + +func TestContract_CanonicalName(t *testing.T) { + tests := []struct { + name string + contract Contract + expected string + }{ + { + name: "alias returns canonical name", + contract: Contract{Name: "FUSD1", Canonical: "FUSD"}, + expected: "FUSD", + }, + { + name: "non-alias returns its own name", + contract: Contract{Name: "FUSD"}, + expected: "FUSD", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.contract.CanonicalName()) + }) + } +} + +func TestContracts_ValidateCanonical(t *testing.T) { + tests := []struct { + name string + contracts Contracts + wantErr bool + errMsg string + }{ + { + name: "valid canonical reference", + contracts: Contracts{ + {Name: "FUSD", Location: "FUSD.cdc"}, + {Name: "FUSD1", Location: "FUSD.cdc", Canonical: "FUSD"}, + }, + wantErr: false, + }, + { + name: "self-referential canonical", + contracts: Contracts{ + {Name: "FUSD", Location: "FUSD.cdc", Canonical: "FUSD"}, + }, + wantErr: true, + errMsg: "contract FUSD cannot have itself as canonical", + }, + { + name: "canonical target does not exist", + contracts: Contracts{ + {Name: "FUSD1", Location: "FUSD.cdc", Canonical: "FUSD"}, + }, + wantErr: true, + errMsg: "canonical contract FUSD referenced by FUSD1 does not exist", + }, + { + name: "multiple aliases to same canonical", + contracts: Contracts{ + {Name: "FUSD", Location: "FUSD.cdc"}, + {Name: "FUSD1", Location: "FUSD.cdc", Canonical: "FUSD"}, + {Name: "FUSD2", Location: "FUSD.cdc", Canonical: "FUSD"}, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.contracts.ValidateCanonical() + if tt.wantErr { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.errMsg) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestContracts_GetAliases(t *testing.T) { + contracts := Contracts{ + {Name: "FUSD", Location: "FUSD.cdc"}, + {Name: "FUSD1", Location: "FUSD.cdc", Canonical: "FUSD"}, + {Name: "FUSD2", Location: "FUSD.cdc", Canonical: "FUSD"}, + {Name: "FT", Location: "FT.cdc"}, + {Name: "FT1", Location: "FT.cdc", Canonical: "FT"}, + } + + fusdAliases := contracts.GetAliases("FUSD") + assert.Len(t, fusdAliases, 2) + assert.Equal(t, "FUSD1", fusdAliases[0].Name) + assert.Equal(t, "FUSD2", fusdAliases[1].Name) + + ftAliases := contracts.GetAliases("FT") + assert.Len(t, ftAliases, 1) + assert.Equal(t, "FT1", ftAliases[0].Name) + + noAliases := contracts.GetAliases("NonExistent") + assert.Len(t, noAliases, 0) +} diff --git a/config/json/contract.go b/config/json/contract.go index 804f43cb2..5f20837ed 100644 --- a/config/json/contract.go +++ b/config/json/contract.go @@ -45,8 +45,9 @@ func (j jsonContracts) transformToConfig() (config.Contracts, error) { contracts = append(contracts, contract) } else { contract := config.Contract{ - Name: contractName, - Location: c.Advanced.Source, + Name: contractName, + Location: c.Advanced.Source, + Canonical: c.Advanced.Canonical, } for network, alias := range c.Advanced.Aliases { address := flow.HexToAddress(alias) @@ -73,8 +74,8 @@ func transformContractsToJSON(contracts config.Contracts) jsonContracts { continue } - // if simple case - if !c.IsAliased() { + // if simple case (no aliases and no canonical) + if !c.IsAliased() && c.Canonical == "" { jsonContracts[c.Name] = jsonContract{ Simple: filepath.ToSlash(c.Location), } @@ -87,8 +88,9 @@ func transformContractsToJSON(contracts config.Contracts) jsonContracts { jsonContracts[c.Name] = jsonContract{ Advanced: jsonContractAdvanced{ - Source: filepath.ToSlash(c.Location), - Aliases: aliases, + Source: filepath.ToSlash(c.Location), + Aliases: aliases, + Canonical: c.Canonical, }, } } @@ -99,8 +101,9 @@ func transformContractsToJSON(contracts config.Contracts) jsonContracts { // jsonContractAdvanced for json parsing advanced config. type jsonContractAdvanced struct { - Source string `json:"source"` - Aliases map[string]string `json:"aliases"` + Source string `json:"source"` + Aliases map[string]string `json:"aliases"` + Canonical string `json:"canonical,omitempty"` } // jsonContract structure for json parsing. From 0bc2b80badb823473fd4d98938e933f23de6c7b0 Mon Sep 17 00:00:00 2001 From: Chase Fleming <1666730+chasefleming@users.noreply.github.com> Date: Fri, 7 Nov 2025 13:47:00 -0800 Subject: [PATCH 02/10] Add imports test --- project/imports_test.go | 108 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/project/imports_test.go b/project/imports_test.go index 546c2cf1a..eb621e54d 100644 --- a/project/imports_test.go +++ b/project/imports_test.go @@ -135,4 +135,112 @@ func TestResolver(t *testing.T) { assert.Equal(t, cleanCode(expected), cleanCode(replaced.Code())) }) + t.Run("Resolve imports with canonical aliases", func(t *testing.T) { + // Create contracts - FUSD1 and FUSD2 are alias deployments of FUSD + // In practice, only the canonical contract (FUSD) would be deployed, and + // FUSD1/FUSD2 would be aliases pointing to different addresses where FUSD is deployed + contracts := []*Contract{ + NewContract("FUSD", "./contracts/FUSD.cdc", nil, flow.HexToAddress("0x1"), "", nil), + NewContract("FUSD1", "", nil, flow.HexToAddress("0x2"), "", nil), // Alias deployment + NewContract("FUSD2", "", nil, flow.HexToAddress("0x3"), "", nil), // Another alias deployment + NewContract("FT", "./contracts/FT.cdc", nil, flow.HexToAddress("0x4"), "", nil), // Regular contract + } + + // Canonical mapping to simulate FUSD1 and FUSD2 having FUSD as canonical + canonicalMapping := map[string]string{ + "FUSD1": "FUSD", + "FUSD2": "FUSD", + } + + replacer := &ImportReplacer{ + contracts: contracts, + aliases: nil, + canonicalMapping: canonicalMapping, + } + + t.Run("basic alias replacement", func(t *testing.T) { + code := []byte(` + import "FUSD" + import "FUSD1" + import "FUSD2" + import "FT" + + access(all) contract Test {} + `) + + program, err := NewProgram(code, nil, "./Test.cdc") + require.NoError(t, err) + + replaced, err := replacer.Replace(program) + require.NoError(t, err) + + expected := []byte(` + import FUSD from 0x0000000000000001 + import FUSD as FUSD1 from 0x0000000000000002 + import FUSD as FUSD2 from 0x0000000000000003 + import FT from 0x0000000000000004 + + access(all) contract Test {} + `) + + assert.Equal(t, cleanCode(expected), cleanCode(replaced.Code())) + }) + }) + + t.Run("ConvertAddressImports with aliases", func(t *testing.T) { + code := []byte(` + import FUSD from 0x0000000000000001 + import FUSD as FUSD1 from 0x0000000000000002 + import FUSD as FUSD2 from 0x0000000000000003 + import FT from 0x0000000000000004 + + access(all) contract Test {} + `) + + program, err := NewProgram(code, nil, "./Test.cdc") + require.NoError(t, err) + + // ConvertAddressImports should convert both regular and alias imports back to identifier imports + // For alias imports (import X as Y from 0x...), it should use the alias name (Y) + expected := []byte(` + import "FUSD" + import "FUSD1" + import "FUSD2" + import "FT" + + access(all) contract Test {} + `) + + assert.Equal(t, cleanCode(expected), cleanCode(program.CodeWithUnprocessedImports())) + }) + + t.Run("Import replacer with no canonical mapping", func(t *testing.T) { + // Test that contracts work normally without canonical mapping + contracts := []*Contract{ + NewContract("Token", "./contracts/Token.cdc", nil, flow.HexToAddress("0x1"), "", nil), + } + + replacer := NewImportReplacer(contracts, nil) + + code := []byte(` + import "Token" + + access(all) contract Test {} + `) + + program, err := NewProgram(code, nil, "./Test.cdc") + require.NoError(t, err) + + replaced, err := replacer.Replace(program) + require.NoError(t, err) + + expected := []byte(` + import Token from 0x0000000000000001 + + access(all) contract Test {} + `) + + assert.Equal(t, cleanCode(expected), cleanCode(replaced.Code())) + }) + } From 5d1c5e112f0ff0ced0633ce38864eb0787a9a2bf Mon Sep 17 00:00:00 2001 From: Chase Fleming <1666730+chasefleming@users.noreply.github.com> Date: Fri, 7 Nov 2025 13:59:20 -0800 Subject: [PATCH 03/10] State changes --- state.go | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/state.go b/state.go index 9360fb5fc..dfae674da 100644 --- a/state.go +++ b/state.go @@ -178,7 +178,15 @@ func (p *State) DeploymentContractsByNetwork(network config.Network) ([]*project return nil, err } + // If this contract is an alias, get the canonical contract's location location := c.Location + if c.IsAlias() { + canonicalContract, err := p.conf.Contracts.ByName(c.Canonical) + if err != nil { + return nil, fmt.Errorf("canonical contract %s not found for alias %s", c.Canonical, c.Name) + } + location = canonicalContract.Location + } // if we loaded config from a single location, we should make the path of contracts defined in config relative to // config path we have provided, this will make cases where we execute loading in different path than config work if len(p.confLoader.LoadedLocations) == 1 { @@ -280,14 +288,34 @@ func (p *State) AliasesForNetwork(network config.Network) project.LocationAliase for _, contract := range p.conf.Contracts { if contract.IsAliased() && contract.Aliases.ByNetwork(network.Name) != nil { alias := contract.Aliases.ByNetwork(network.Name).Address.String() - aliases[filepath.Clean(contract.Location)] = alias // alias for import by file location - aliases[contract.Name] = alias // alias for import by name + + // For alias contracts, use their canonical contract's location as well + location := contract.Location + if contract.IsAlias() { + if canonicalContract, err := p.conf.Contracts.ByName(contract.Canonical); err == nil { + location = canonicalContract.Location + } + } + + aliases[filepath.Clean(location)] = alias // alias for import by file location + aliases[contract.Name] = alias // alias for import by name } } return aliases } +// CanonicalContractMapping returns a mapping of alias contract names to their canonical contract names. +func (p *State) CanonicalContractMapping() map[string]string { + canonicalMapping := make(map[string]string) + for _, contract := range p.conf.Contracts { + if contract.IsAlias() { + canonicalMapping[contract.Name] = contract.Canonical + } + } + return canonicalMapping +} + // Load loads a project configuration and returns the resulting project. func Load(configFilePaths []string, readerWriter ReaderWriter) (*State, error) { confLoader := config.NewLoader(readerWriter) From 1ced43d178c5a69d1d30b19048decb150cd72ad2 Mon Sep 17 00:00:00 2001 From: Chase Fleming <1666730+chasefleming@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:00:44 -0800 Subject: [PATCH 04/10] Update program --- project/program.go | 56 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/project/program.go b/project/program.go index f0fa0a796..658aa519c 100644 --- a/project/program.go +++ b/project/program.go @@ -42,13 +42,18 @@ func NewProgram(code []byte, args []cadence.Value, location string) (*Program, e return nil, err } - return &Program{ + p := &Program{ code: code, args: args, location: location, astProgram: astProgram, codeWithUnprocessedImports: code, // has converted import syntax e.g. 'import "Foo"' - }, nil + } + + // Convert address imports to identifier imports for codeWithUnprocessedImports + p.ConvertAddressImports() + + return p, nil } func (p *Program) AddressImportDeclarations() []*ast.ImportDeclaration { @@ -88,15 +93,45 @@ func (p *Program) HasImports() bool { return len(p.imports()) > 0 } -func (p *Program) replaceImport(from string, to string) *Program { +func (p *Program) replaceImport(from string, to string, canonicalName ...string) *Program { code := string(p.Code()) - pathRegex := regexp.MustCompile(fmt.Sprintf(`import\s+(\w+)\s+from\s+"%s"`, from)) - identifierRegex := regexp.MustCompile(fmt.Sprintf(`import\s+"(%s)"`, from)) + // Extract the import name from the 'from' parameter + importName := from + if regexp.MustCompile(`\.cdc$`).MatchString(from) { + // If it's a path, extract the contract name + matches := regexp.MustCompile(`([^/]+)\.cdc$`).FindStringSubmatch(from) + if len(matches) > 1 { + importName = matches[1] + } + } - replacement := fmt.Sprintf(`import $1 from 0x%s`, to) - code = pathRegex.ReplaceAllString(code, replacement) - code = identifierRegex.ReplaceAllString(code, replacement) + // Handle path imports (e.g., import X from "./X.cdc") + pathRegex := regexp.MustCompile(fmt.Sprintf(`import\s+(\w+)\s+from\s+"%s"`, regexp.QuoteMeta(from))) + // Handle identifier imports (e.g., import "X") + identifierRegex := regexp.MustCompile(fmt.Sprintf(`import\s+"%s"`, regexp.QuoteMeta(from))) + + // Determine if we need alias syntax + canonical := "" + if len(canonicalName) > 0 { + canonical = canonicalName[0] + } + + if canonical != "" && canonical != importName { + // Use alias syntax: import CanonicalName as AliasName from 0xAddress + pathReplacement := fmt.Sprintf(`import %s as $1 from 0x%s`, canonical, to) + identifierReplacement := fmt.Sprintf(`import %s as %s from 0x%s`, canonical, importName, to) + + code = pathRegex.ReplaceAllString(code, pathReplacement) + code = identifierRegex.ReplaceAllString(code, identifierReplacement) + } else { + // Use regular syntax: import Name from 0xAddress + replacement := fmt.Sprintf(`import $1 from 0x%s`, to) + code = pathRegex.ReplaceAllString(code, replacement) + + identifierReplacement := fmt.Sprintf(`import %s from 0x%s`, importName, to) + code = identifierRegex.ReplaceAllString(code, identifierReplacement) + } p.code = []byte(code) p.reload() @@ -138,8 +173,13 @@ func (p *Program) Name() (string, error) { func (p *Program) ConvertAddressImports() { code := string(p.code) + // Handle regular imports: import X from 0xAddress addressImportRegex := regexp.MustCompile(`import\s+(\w+)\s+from\s+0x[0-9a-fA-F]+`) modifiedCode := addressImportRegex.ReplaceAllString(code, `import "$1"`) + + // Handle alias imports: import X as Y from 0xAddress -> import "Y" + aliasImportRegex := regexp.MustCompile(`import\s+\w+\s+as\s+(\w+)\s+from\s+0x[0-9a-fA-F]+`) + modifiedCode = aliasImportRegex.ReplaceAllString(modifiedCode, `import "$1"`) p.codeWithUnprocessedImports = []byte(modifiedCode) } From 8f9f0b7868e3264f83c4c5611c288957c2ce30ec Mon Sep 17 00:00:00 2001 From: Chase Fleming <1666730+chasefleming@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:01:40 -0800 Subject: [PATCH 05/10] Update import replacer --- flowkit.go | 4 ++++ project/imports.go | 45 ++++++++++++++++++++++++++++++++++++++------- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/flowkit.go b/flowkit.go index 3d1f5f03b..1f4808149 100644 --- a/flowkit.go +++ b/flowkit.go @@ -289,6 +289,7 @@ func (f *Flowkit) AddContract( importReplacer := project.NewImportReplacer( contracts, state.AliasesForNetwork(f.network), + state.CanonicalContractMapping(), ) program, err = importReplacer.Replace(program) @@ -833,6 +834,7 @@ func (f *Flowkit) ExecuteScript(ctx context.Context, script Script, query Script importReplacer := project.NewImportReplacer( contracts, state.AliasesForNetwork(f.network), + state.CanonicalContractMapping(), ) if state == nil { @@ -990,6 +992,7 @@ func (f *Flowkit) BuildTransaction( importReplacer := project.NewImportReplacer( contracts, state.AliasesForNetwork(f.network), + state.CanonicalContractMapping(), ) program, err = importReplacer.Replace(program) @@ -1122,6 +1125,7 @@ func (f *Flowkit) ReplaceImportsInScript( importReplacer := project.NewImportReplacer( contracts, state.AliasesForNetwork(f.network), + state.CanonicalContractMapping(), ) program, err := project.NewProgram(script.Code, script.Args, script.Location) diff --git a/project/imports.go b/project/imports.go index 268b0e86e..6935b3b88 100644 --- a/project/imports.go +++ b/project/imports.go @@ -32,14 +32,22 @@ type Account interface { // ImportReplacer implements file import replacements functionality for the project contracts with optionally included aliases. type ImportReplacer struct { - contracts []*Contract - aliases LocationAliases + contracts []*Contract + aliases LocationAliases + canonicalMapping map[string]string // maps alias names to their canonical contract names } -func NewImportReplacer(contracts []*Contract, aliases LocationAliases) *ImportReplacer { +func NewImportReplacer(contracts []*Contract, aliases LocationAliases, canonicalMapping ...map[string]string) *ImportReplacer { + canonical := make(map[string]string) + // If canonical mapping is provided, use it + if len(canonicalMapping) > 0 && canonicalMapping[0] != nil { + canonical = canonicalMapping[0] + } + return &ImportReplacer{ - contracts: contracts, - aliases: aliases, + contracts: contracts, + aliases: aliases, + canonicalMapping: canonical, } } @@ -52,13 +60,17 @@ func (i *ImportReplacer) Replace(program *Program) (*Program, error) { importLocation := filepath.Clean(absolutePath(program.Location(), imp)) address, isPath := contractsLocations[importLocation] if isPath { - program.replaceImport(imp, address) + // Check if this import is an alias + canonicalName := i.getCanonicalNameForImport(imp, address) + program.replaceImport(imp, address, canonicalName) continue } // check if import by identifier exists (e.g. import ["X"]) address, isIdentifier := contractsLocations[imp] if isIdentifier { - program.replaceImport(imp, address) + // Check if this import is an alias + canonicalName := i.getCanonicalNameForImport(imp, address) + program.replaceImport(imp, address, canonicalName) continue } @@ -84,6 +96,25 @@ func (i *ImportReplacer) getContractsLocations() map[string]string { return locationAddress } +// getCanonicalNameForImport determines the canonical contract name for an import. +// Returns the canonical name if the import is an alias, otherwise returns the import name. +func (i *ImportReplacer) getCanonicalNameForImport(importName string, address string) string { + // Extract just the contract name from the import path if it's a path + contractName := importName + if filepath.Ext(importName) == ".cdc" { + contractName = filepath.Base(importName) + contractName = contractName[:len(contractName)-4] // Remove .cdc extension + } + + // Check if this is an alias by looking up in canonical mapping + if canonicalName, isAlias := i.canonicalMapping[contractName]; isAlias { + return canonicalName + } + + // Not an alias, return the original contract name + return contractName +} + func absolutePath(basePath, relativePath string) string { return filepath.Join(filepath.Dir(basePath), relativePath) } From 9f384de20cec665186f4e9b50bd3cd3eed76a627 Mon Sep 17 00:00:00 2001 From: Chase Fleming <1666730+chasefleming@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:24:35 -0800 Subject: [PATCH 06/10] Add comment --- state.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/state.go b/state.go index dfae674da..38ddda8f2 100644 --- a/state.go +++ b/state.go @@ -306,6 +306,25 @@ func (p *State) AliasesForNetwork(network config.Network) project.LocationAliase } // CanonicalContractMapping returns a mapping of alias contract names to their canonical contract names. +// +// Example usage with import aliases: +// +// // Given flow.json with: +// // "FUSD": { "source": "./FUSD.cdc", "aliases": {...} } +// // "FUSD_v2": { "source": "./FUSD.cdc", "canonical": "FUSD", "aliases": {...} } +// +// state, _ := flowkit.Load([]string{"flow.json"}, flowkit.NewReaderWriter()) +// +// // 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) for _, contract := range p.conf.Contracts { From dd2f58ee1a88822b768818f905244b32b52839b5 Mon Sep 17 00:00:00 2001 From: Chase Fleming <1666730+chasefleming@users.noreply.github.com> Date: Wed, 12 Nov 2025 12:47:12 -0800 Subject: [PATCH 07/10] Remove --- config/contract.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/config/contract.go b/config/contract.go index 710d31cb7..d04fd3ced 100644 --- a/config/contract.go +++ b/config/contract.go @@ -134,10 +134,6 @@ func (c *Contracts) ValidateCanonical() error { if contract.Canonical == contract.Name { return fmt.Errorf("contract %s cannot have itself as canonical", contract.Name) } - // Check target exists - if _, err := c.ByName(contract.Canonical); err != nil { - return fmt.Errorf("canonical contract %s referenced by %s does not exist", contract.Canonical, contract.Name) - } } } return nil From 8fe41eb05e26f71349c55f0167a9f4933b856b05 Mon Sep 17 00:00:00 2001 From: Chase Fleming <1666730+chasefleming@users.noreply.github.com> Date: Wed, 12 Nov 2025 12:52:56 -0800 Subject: [PATCH 08/10] Remove --- state.go | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/state.go b/state.go index 38ddda8f2..ee95b103f 100644 --- a/state.go +++ b/state.go @@ -178,15 +178,7 @@ func (p *State) DeploymentContractsByNetwork(network config.Network) ([]*project return nil, err } - // If this contract is an alias, get the canonical contract's location location := c.Location - if c.IsAlias() { - canonicalContract, err := p.conf.Contracts.ByName(c.Canonical) - if err != nil { - return nil, fmt.Errorf("canonical contract %s not found for alias %s", c.Canonical, c.Name) - } - location = canonicalContract.Location - } // if we loaded config from a single location, we should make the path of contracts defined in config relative to // config path we have provided, this will make cases where we execute loading in different path than config work if len(p.confLoader.LoadedLocations) == 1 { @@ -288,17 +280,8 @@ func (p *State) AliasesForNetwork(network config.Network) project.LocationAliase for _, contract := range p.conf.Contracts { if contract.IsAliased() && contract.Aliases.ByNetwork(network.Name) != nil { alias := contract.Aliases.ByNetwork(network.Name).Address.String() - - // For alias contracts, use their canonical contract's location as well - location := contract.Location - if contract.IsAlias() { - if canonicalContract, err := p.conf.Contracts.ByName(contract.Canonical); err == nil { - location = canonicalContract.Location - } - } - - aliases[filepath.Clean(location)] = alias // alias for import by file location - aliases[contract.Name] = alias // alias for import by name + aliases[filepath.Clean(contract.Location)] = alias // alias for import by file location + aliases[contract.Name] = alias // alias for import by name } } From 794c3b149cda40dd0ad69a4b9d5c476e1835e5a7 Mon Sep 17 00:00:00 2001 From: Chase Fleming <1666730+chasefleming@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:05:08 -0800 Subject: [PATCH 09/10] Remove --- config/contract_test.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/config/contract_test.go b/config/contract_test.go index 0fb63b7dd..74ceedfaa 100644 --- a/config/contract_test.go +++ b/config/contract_test.go @@ -183,14 +183,6 @@ func TestContracts_ValidateCanonical(t *testing.T) { wantErr: true, errMsg: "contract FUSD cannot have itself as canonical", }, - { - name: "canonical target does not exist", - contracts: Contracts{ - {Name: "FUSD1", Location: "FUSD.cdc", Canonical: "FUSD"}, - }, - wantErr: true, - errMsg: "canonical contract FUSD referenced by FUSD1 does not exist", - }, { name: "multiple aliases to same canonical", contracts: Contracts{ From 3aa916afecbab51cfbd7717236fbb5e6040ee053 Mon Sep 17 00:00:00 2001 From: Chase Fleming <1666730+chasefleming@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:05:05 -0800 Subject: [PATCH 10/10] Update schema --- schema.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/schema.json b/schema.json index 48d412c1b..4f9b24536 100644 --- a/schema.json +++ b/schema.json @@ -195,6 +195,9 @@ } }, "type": "object" + }, + "canonical": { + "type": "string" } }, "additionalProperties": false,