Skip to content

Commit c991c99

Browse files
authored
Add import alias support for Cadence contracts (#167)
* Add contract helpers * Add imports test * State changes * Update program * Update import replacer * Add comment * Remove * Remove * Remove * Update schema --------- Co-authored-by: Chase Fleming <1666730+chasefleming@users.noreply.github.com>
1 parent ffa06a6 commit c991c99

File tree

9 files changed

+399
-23
lines changed

9 files changed

+399
-23
lines changed

config/contract.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type Contract struct {
3535
Location string
3636
Aliases Aliases
3737
IsDependency bool
38+
Canonical string // Reference to canonical contract name if this is an alias
3839
}
3940

4041
// Alias defines an existing pre-deployed contract address for specific network.
@@ -74,6 +75,19 @@ func (c *Contract) IsAliased() bool {
7475
return len(c.Aliases) > 0
7576
}
7677

78+
// IsAlias checks if this contract is an alias to another contract.
79+
func (c *Contract) IsAlias() bool {
80+
return c.Canonical != ""
81+
}
82+
83+
// CanonicalName returns the canonical contract name if this is an alias, otherwise returns the contract's own name.
84+
func (c *Contract) CanonicalName() string {
85+
if c.Canonical != "" {
86+
return c.Canonical
87+
}
88+
return c.Name
89+
}
90+
7791
// ByName get contract by name or return an error if it doesn't exist.
7892
func (c *Contracts) ByName(name string) (*Contract, error) {
7993
for i, contract := range *c {
@@ -112,6 +126,30 @@ func (c *Contracts) Remove(name string) error {
112126
return nil
113127
}
114128

129+
// ValidateCanonical validates that all canonical references are valid.
130+
func (c *Contracts) ValidateCanonical() error {
131+
for _, contract := range *c {
132+
if contract.Canonical != "" {
133+
// Check self-reference
134+
if contract.Canonical == contract.Name {
135+
return fmt.Errorf("contract %s cannot have itself as canonical", contract.Name)
136+
}
137+
}
138+
}
139+
return nil
140+
}
141+
142+
// GetAliases returns all contracts that have the given contract as their canonical.
143+
func (c *Contracts) GetAliases(canonicalName string) []*Contract {
144+
var aliases []*Contract
145+
for i, contract := range *c {
146+
if contract.Canonical == canonicalName {
147+
aliases = append(aliases, &(*c)[i])
148+
}
149+
}
150+
return aliases
151+
}
152+
115153
const dependencyManagerDirectory = "imports"
116154

117155
const (

config/contract_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,122 @@ func TestContracts_AddDependencyAsContract(t *testing.T) {
109109
assert.Equal(t, "imports/0000000000abcdef/TestContract.cdc", contract.Location)
110110
assert.Len(t, contract.Aliases, 1)
111111
}
112+
113+
func TestContract_IsAlias(t *testing.T) {
114+
tests := []struct {
115+
name string
116+
contract Contract
117+
expected bool
118+
}{
119+
{
120+
name: "contract with canonical is an alias",
121+
contract: Contract{Name: "FUSD1", Canonical: "FUSD"},
122+
expected: true,
123+
},
124+
{
125+
name: "contract without canonical is not an alias",
126+
contract: Contract{Name: "FUSD"},
127+
expected: false,
128+
},
129+
}
130+
131+
for _, tt := range tests {
132+
t.Run(tt.name, func(t *testing.T) {
133+
assert.Equal(t, tt.expected, tt.contract.IsAlias())
134+
})
135+
}
136+
}
137+
138+
func TestContract_CanonicalName(t *testing.T) {
139+
tests := []struct {
140+
name string
141+
contract Contract
142+
expected string
143+
}{
144+
{
145+
name: "alias returns canonical name",
146+
contract: Contract{Name: "FUSD1", Canonical: "FUSD"},
147+
expected: "FUSD",
148+
},
149+
{
150+
name: "non-alias returns its own name",
151+
contract: Contract{Name: "FUSD"},
152+
expected: "FUSD",
153+
},
154+
}
155+
156+
for _, tt := range tests {
157+
t.Run(tt.name, func(t *testing.T) {
158+
assert.Equal(t, tt.expected, tt.contract.CanonicalName())
159+
})
160+
}
161+
}
162+
163+
func TestContracts_ValidateCanonical(t *testing.T) {
164+
tests := []struct {
165+
name string
166+
contracts Contracts
167+
wantErr bool
168+
errMsg string
169+
}{
170+
{
171+
name: "valid canonical reference",
172+
contracts: Contracts{
173+
{Name: "FUSD", Location: "FUSD.cdc"},
174+
{Name: "FUSD1", Location: "FUSD.cdc", Canonical: "FUSD"},
175+
},
176+
wantErr: false,
177+
},
178+
{
179+
name: "self-referential canonical",
180+
contracts: Contracts{
181+
{Name: "FUSD", Location: "FUSD.cdc", Canonical: "FUSD"},
182+
},
183+
wantErr: true,
184+
errMsg: "contract FUSD cannot have itself as canonical",
185+
},
186+
{
187+
name: "multiple aliases to same canonical",
188+
contracts: Contracts{
189+
{Name: "FUSD", Location: "FUSD.cdc"},
190+
{Name: "FUSD1", Location: "FUSD.cdc", Canonical: "FUSD"},
191+
{Name: "FUSD2", Location: "FUSD.cdc", Canonical: "FUSD"},
192+
},
193+
wantErr: false,
194+
},
195+
}
196+
197+
for _, tt := range tests {
198+
t.Run(tt.name, func(t *testing.T) {
199+
err := tt.contracts.ValidateCanonical()
200+
if tt.wantErr {
201+
assert.Error(t, err)
202+
assert.Contains(t, err.Error(), tt.errMsg)
203+
} else {
204+
assert.NoError(t, err)
205+
}
206+
})
207+
}
208+
}
209+
210+
func TestContracts_GetAliases(t *testing.T) {
211+
contracts := Contracts{
212+
{Name: "FUSD", Location: "FUSD.cdc"},
213+
{Name: "FUSD1", Location: "FUSD.cdc", Canonical: "FUSD"},
214+
{Name: "FUSD2", Location: "FUSD.cdc", Canonical: "FUSD"},
215+
{Name: "FT", Location: "FT.cdc"},
216+
{Name: "FT1", Location: "FT.cdc", Canonical: "FT"},
217+
}
218+
219+
fusdAliases := contracts.GetAliases("FUSD")
220+
assert.Len(t, fusdAliases, 2)
221+
assert.Equal(t, "FUSD1", fusdAliases[0].Name)
222+
assert.Equal(t, "FUSD2", fusdAliases[1].Name)
223+
224+
ftAliases := contracts.GetAliases("FT")
225+
assert.Len(t, ftAliases, 1)
226+
assert.Equal(t, "FT1", ftAliases[0].Name)
227+
228+
noAliases := contracts.GetAliases("NonExistent")
229+
assert.Len(t, noAliases, 0)
230+
}

config/json/contract.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ func (j jsonContracts) transformToConfig() (config.Contracts, error) {
4545
contracts = append(contracts, contract)
4646
} else {
4747
contract := config.Contract{
48-
Name: contractName,
49-
Location: c.Advanced.Source,
48+
Name: contractName,
49+
Location: c.Advanced.Source,
50+
Canonical: c.Advanced.Canonical,
5051
}
5152
for network, alias := range c.Advanced.Aliases {
5253
address := flow.HexToAddress(alias)
@@ -73,8 +74,8 @@ func transformContractsToJSON(contracts config.Contracts) jsonContracts {
7374
continue
7475
}
7576

76-
// if simple case
77-
if !c.IsAliased() {
77+
// if simple case (no aliases and no canonical)
78+
if !c.IsAliased() && c.Canonical == "" {
7879
jsonContracts[c.Name] = jsonContract{
7980
Simple: filepath.ToSlash(c.Location),
8081
}
@@ -87,8 +88,9 @@ func transformContractsToJSON(contracts config.Contracts) jsonContracts {
8788

8889
jsonContracts[c.Name] = jsonContract{
8990
Advanced: jsonContractAdvanced{
90-
Source: filepath.ToSlash(c.Location),
91-
Aliases: aliases,
91+
Source: filepath.ToSlash(c.Location),
92+
Aliases: aliases,
93+
Canonical: c.Canonical,
9294
},
9395
}
9496
}
@@ -99,8 +101,9 @@ func transformContractsToJSON(contracts config.Contracts) jsonContracts {
99101

100102
// jsonContractAdvanced for json parsing advanced config.
101103
type jsonContractAdvanced struct {
102-
Source string `json:"source"`
103-
Aliases map[string]string `json:"aliases"`
104+
Source string `json:"source"`
105+
Aliases map[string]string `json:"aliases"`
106+
Canonical string `json:"canonical,omitempty"`
104107
}
105108

106109
// jsonContract structure for json parsing.

flowkit.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ func (f *Flowkit) AddContract(
289289
importReplacer := project.NewImportReplacer(
290290
contracts,
291291
state.AliasesForNetwork(f.network),
292+
state.CanonicalContractMapping(),
292293
)
293294

294295
program, err = importReplacer.Replace(program)
@@ -833,6 +834,7 @@ func (f *Flowkit) ExecuteScript(ctx context.Context, script Script, query Script
833834
importReplacer := project.NewImportReplacer(
834835
contracts,
835836
state.AliasesForNetwork(f.network),
837+
state.CanonicalContractMapping(),
836838
)
837839

838840
if state == nil {
@@ -990,6 +992,7 @@ func (f *Flowkit) BuildTransaction(
990992
importReplacer := project.NewImportReplacer(
991993
contracts,
992994
state.AliasesForNetwork(f.network),
995+
state.CanonicalContractMapping(),
993996
)
994997

995998
program, err = importReplacer.Replace(program)
@@ -1122,6 +1125,7 @@ func (f *Flowkit) ReplaceImportsInScript(
11221125
importReplacer := project.NewImportReplacer(
11231126
contracts,
11241127
state.AliasesForNetwork(f.network),
1128+
state.CanonicalContractMapping(),
11251129
)
11261130

11271131
program, err := project.NewProgram(script.Code, script.Args, script.Location)

project/imports.go

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,22 @@ type Account interface {
3232

3333
// ImportReplacer implements file import replacements functionality for the project contracts with optionally included aliases.
3434
type ImportReplacer struct {
35-
contracts []*Contract
36-
aliases LocationAliases
35+
contracts []*Contract
36+
aliases LocationAliases
37+
canonicalMapping map[string]string // maps alias names to their canonical contract names
3738
}
3839

39-
func NewImportReplacer(contracts []*Contract, aliases LocationAliases) *ImportReplacer {
40+
func NewImportReplacer(contracts []*Contract, aliases LocationAliases, canonicalMapping ...map[string]string) *ImportReplacer {
41+
canonical := make(map[string]string)
42+
// If canonical mapping is provided, use it
43+
if len(canonicalMapping) > 0 && canonicalMapping[0] != nil {
44+
canonical = canonicalMapping[0]
45+
}
46+
4047
return &ImportReplacer{
41-
contracts: contracts,
42-
aliases: aliases,
48+
contracts: contracts,
49+
aliases: aliases,
50+
canonicalMapping: canonical,
4351
}
4452
}
4553

@@ -52,13 +60,17 @@ func (i *ImportReplacer) Replace(program *Program) (*Program, error) {
5260
importLocation := filepath.Clean(absolutePath(program.Location(), imp))
5361
address, isPath := contractsLocations[importLocation]
5462
if isPath {
55-
program.replaceImport(imp, address)
63+
// Check if this import is an alias
64+
canonicalName := i.getCanonicalNameForImport(imp, address)
65+
program.replaceImport(imp, address, canonicalName)
5666
continue
5767
}
5868
// check if import by identifier exists (e.g. import ["X"])
5969
address, isIdentifier := contractsLocations[imp]
6070
if isIdentifier {
61-
program.replaceImport(imp, address)
71+
// Check if this import is an alias
72+
canonicalName := i.getCanonicalNameForImport(imp, address)
73+
program.replaceImport(imp, address, canonicalName)
6274
continue
6375
}
6476

@@ -84,6 +96,25 @@ func (i *ImportReplacer) getContractsLocations() map[string]string {
8496
return locationAddress
8597
}
8698

99+
// getCanonicalNameForImport determines the canonical contract name for an import.
100+
// Returns the canonical name if the import is an alias, otherwise returns the import name.
101+
func (i *ImportReplacer) getCanonicalNameForImport(importName string, address string) string {
102+
// Extract just the contract name from the import path if it's a path
103+
contractName := importName
104+
if filepath.Ext(importName) == ".cdc" {
105+
contractName = filepath.Base(importName)
106+
contractName = contractName[:len(contractName)-4] // Remove .cdc extension
107+
}
108+
109+
// Check if this is an alias by looking up in canonical mapping
110+
if canonicalName, isAlias := i.canonicalMapping[contractName]; isAlias {
111+
return canonicalName
112+
}
113+
114+
// Not an alias, return the original contract name
115+
return contractName
116+
}
117+
87118
func absolutePath(basePath, relativePath string) string {
88119
return filepath.Join(filepath.Dir(basePath), relativePath)
89120
}

0 commit comments

Comments
 (0)