diff --git a/.gitignore b/.gitignore index 2a9200fa7..b8403f505 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ git # Ignore built CLI main *.pkey + +# Local development file generation folder +imports \ No newline at end of file diff --git a/cmd/flow/main.go b/cmd/flow/main.go index 35c312aee..79fd78961 100644 --- a/cmd/flow/main.go +++ b/cmd/flow/main.go @@ -24,6 +24,8 @@ import ( "os" "strings" + "github.com/onflow/flow-cli/internal/dependencymanager" + "github.com/spf13/cobra" "github.com/onflow/flow-cli/internal/accounts" @@ -117,6 +119,7 @@ func main() { cmd.AddCommand(snapshot.Cmd) cmd.AddCommand(super.FlixCmd) cmd.AddCommand(super.GenerateCommand) + cmd.AddCommand(dependencymanager.Cmd) command.InitFlags(cmd) cmd.AddGroup(&cobra.Group{ @@ -143,6 +146,10 @@ func main() { ID: "security", Title: "🔒 Flow Security", }) + cmd.AddGroup(&cobra.Group{ + ID: "manager", + Title: "🔗 Dependency Manager", + }) cmd.SetUsageTemplate(command.UsageTemplate) diff --git a/flowkit/config/config.go b/flowkit/config/config.go index fb75c7bdf..3781ba32b 100644 --- a/flowkit/config/config.go +++ b/flowkit/config/config.go @@ -33,11 +33,12 @@ import ( // Accounts defines Flow accounts and their addresses, private key and more properties // Deployments describes which contracts should be deployed to which accounts type Config struct { - Emulators Emulators - Contracts Contracts - Networks Networks - Accounts Accounts - Deployments Deployments + Emulators Emulators + Contracts Contracts + Dependencies Dependencies + Networks Networks + Accounts Accounts + Deployments Deployments } type KeyType string diff --git a/flowkit/config/contract.go b/flowkit/config/contract.go index bdc8d41ef..bcfa883d7 100644 --- a/flowkit/config/contract.go +++ b/flowkit/config/contract.go @@ -20,6 +20,10 @@ package config import ( "fmt" + "path/filepath" + + "github.com/onflow/flow-go/fvm/systemcontracts" + flowGo "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go-sdk" "golang.org/x/exp/slices" @@ -27,9 +31,10 @@ import ( // Contract defines the configuration for a Cadence contract. type Contract struct { - Name string - Location string - Aliases Aliases + Name string + Location string + Aliases Aliases + IsDependency bool } // Alias defines an existing pre-deployed contract address for specific network. @@ -106,3 +111,98 @@ func (c *Contracts) Remove(name string) error { return nil } + +const dependencyManagerDirectory = "imports" + +const ( + networkEmulator = "emulator" + networkTestnet = "testnet" + networkMainnet = "mainnet" +) + +func supportedNetworks() []string { + return []string{ + networkEmulator, + networkTestnet, + networkMainnet, + } +} + +var networkToChainID = map[string]flowGo.ChainID{ + networkEmulator: flowGo.Emulator, + networkTestnet: flowGo.Testnet, + networkMainnet: flowGo.Mainnet, +} + +func isCoreContract(networkName, contractName, contractAddress string) bool { + sc := systemcontracts.SystemContractsForChain(networkToChainID[networkName]) + + for _, coreContract := range sc.All() { + if coreContract.Name == contractName && coreContract.Address.String() == contractAddress { + return true + } + } + + return false +} + +func getCoreContractByName(networkName, contractName string) *systemcontracts.SystemContract { + sc := systemcontracts.SystemContractsForChain(networkToChainID[networkName]) + + for i, coreContract := range sc.All() { + if coreContract.Name == contractName { + return &sc.All()[i] + } + } + + return nil +} + +// AddDependencyAsContract adds a dependency as a contract if it doesn't already exist. +func (c *Contracts) AddDependencyAsContract(dependency Dependency, networkName string) { + var aliases []Alias + + // If core contract found by name and address matches, then use all core contract aliases across networks + if isCoreContract(networkName, dependency.Source.ContractName, dependency.Source.Address.String()) { + for _, networkStr := range supportedNetworks() { + coreContract := getCoreContractByName(networkStr, dependency.Source.ContractName) + if coreContract != nil { + aliases = append(aliases, Alias{ + Network: networkStr, + Address: flow.HexToAddress(coreContract.Address.String()), + }) + } + } + } else { + aliases = append(aliases, dependency.Aliases...) + } + + // If no core contract match, then use the address in source as alias + if len(aliases) == 0 { + aliases = append(aliases, Alias{ + Network: dependency.Source.NetworkName, + Address: dependency.Source.Address, + }) + } + + contract := Contract{ + Name: dependency.Name, + Location: filepath.ToSlash(fmt.Sprintf("%s/%s/%s.cdc", dependencyManagerDirectory, dependency.Source.Address, dependency.Source.ContractName)), + Aliases: aliases, + IsDependency: true, + } + + if _, err := c.ByName(contract.Name); err != nil { + c.AddOrUpdate(contract) + } +} + +func (c *Contracts) DependencyContractByName(name string) *Contract { + for i, contract := range *c { + if contract.Name == name && contract.IsDependency { + return &(*c)[i] + } + } + + return nil +} diff --git a/flowkit/config/contract_test.go b/flowkit/config/contract_test.go index d2389f8d9..2b9ab21f0 100644 --- a/flowkit/config/contract_test.go +++ b/flowkit/config/contract_test.go @@ -90,3 +90,22 @@ func TestContracts_Remove_NotFound(t *testing.T) { _, err = contracts.ByName("mycontract2") assert.EqualError(t, err, "contract mycontract2 does not exist") } + +func TestContracts_AddDependencyAsContract(t *testing.T) { + contracts := Contracts{} + contracts.AddDependencyAsContract(Dependency{ + Name: "testcontract", + Source: Source{ + NetworkName: "testnet", + Address: flow.HexToAddress("0x0000000000abcdef"), + ContractName: "TestContract", + }, + }, "testnet") + + assert.Len(t, contracts, 1) + + contract, err := contracts.ByName("testcontract") + assert.NoError(t, err) + assert.Equal(t, "imports/0000000000abcdef/TestContract.cdc", contract.Location) + assert.Len(t, contract.Aliases, 1) +} diff --git a/flowkit/config/dependency.go b/flowkit/config/dependency.go new file mode 100644 index 000000000..68c862a19 --- /dev/null +++ b/flowkit/config/dependency.go @@ -0,0 +1,79 @@ +/* + * Flow CLI + * + * Copyright 2019 Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "fmt" + "strings" + + "github.com/onflow/flow-go-sdk" +) + +func ParseSourceString(s string) (network, address, contractName string, err error) { + parts := strings.Split(s, "://") + if len(parts) != 2 { + return "", "", "", fmt.Errorf("invalid dependency source format: %s", s) + } + network = parts[0] + + subParts := strings.Split(parts[1], ".") + if len(subParts) != 2 { + return "", "", "", fmt.Errorf("invalid dependency source format: %s", s) + } + address = subParts[0] + contractName = subParts[1] + + return network, address, contractName, nil +} + +type Source struct { + NetworkName string + Address flow.Address + ContractName string +} + +type Dependency struct { + Name string + Source Source + Hash string + Aliases Aliases +} + +type Dependencies []Dependency + +func (d *Dependencies) ByName(name string) *Dependency { + for i, dep := range *d { + if dep.Name == name { + return &(*d)[i] + } + } + + return nil +} + +func (d *Dependencies) AddOrUpdate(dep Dependency) { + for i, dependency := range *d { + if dependency.Name == dep.Name { + (*d)[i] = dep + return + } + } + + *d = append(*d, dep) +} diff --git a/flowkit/config/dependency_test.go b/flowkit/config/dependency_test.go new file mode 100644 index 000000000..b3f00499f --- /dev/null +++ b/flowkit/config/dependency_test.go @@ -0,0 +1,44 @@ +/* + * Flow CLI + * + * Copyright 2019 Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDependencies_ByName(t *testing.T) { + dependencies := Dependencies{ + Dependency{Name: "mydep"}, + } + + dep := dependencies.ByName("mydep") + assert.NotNil(t, dep) +} + +func TestDependencies_AddOrUpdate(t *testing.T) { + dependencies := Dependencies{} + dependencies.AddOrUpdate(Dependency{Name: "mydep"}) + + assert.Len(t, dependencies, 1) + + dep := dependencies.ByName("mydep") + assert.NotNil(t, dep) +} diff --git a/flowkit/config/json/config.go b/flowkit/config/json/config.go index 0611b622f..af2f96c8e 100644 --- a/flowkit/config/json/config.go +++ b/flowkit/config/json/config.go @@ -27,11 +27,12 @@ import ( // jsonConfig implements JSON format for persisting and parsing configuration. type jsonConfig struct { - Emulators jsonEmulators `json:"emulators,omitempty"` - Contracts jsonContracts `json:"contracts,omitempty"` - Networks jsonNetworks `json:"networks,omitempty"` - Accounts jsonAccounts `json:"accounts,omitempty"` - Deployments jsonDeployments `json:"deployments,omitempty"` + Emulators jsonEmulators `json:"emulators,omitempty"` + Contracts jsonContracts `json:"contracts,omitempty"` + Dependencies jsonDependencies `json:"dependencies,omitempty"` + Networks jsonNetworks `json:"networks,omitempty"` + Accounts jsonAccounts `json:"accounts,omitempty"` + Deployments jsonDeployments `json:"deployments,omitempty"` } func (j *jsonConfig) transformToConfig() (*config.Config, error) { @@ -45,6 +46,11 @@ func (j *jsonConfig) transformToConfig() (*config.Config, error) { return nil, err } + dependencies, err := j.Dependencies.transformToConfig() + if err != nil { + return nil, err + } + networks, err := j.Networks.transformToConfig() if err != nil { return nil, err @@ -61,11 +67,17 @@ func (j *jsonConfig) transformToConfig() (*config.Config, error) { } conf := &config.Config{ - Emulators: emulators, - Contracts: contracts, - Networks: networks, - Accounts: accounts, - Deployments: deployments, + Emulators: emulators, + Contracts: contracts, + Dependencies: dependencies, + Networks: networks, + Accounts: accounts, + Deployments: deployments, + } + + // Add dependencies as contracts so they can be used in the project just like any other contract. + for _, dep := range dependencies { + conf.Contracts.AddDependencyAsContract(dep, dep.Source.NetworkName) } return conf, nil @@ -73,11 +85,12 @@ func (j *jsonConfig) transformToConfig() (*config.Config, error) { func transformConfigToJSON(config *config.Config) jsonConfig { return jsonConfig{ - Emulators: transformEmulatorsToJSON(config.Emulators), - Contracts: transformContractsToJSON(config.Contracts), - Networks: transformNetworksToJSON(config.Networks), - Accounts: transformAccountsToJSON(config.Accounts), - Deployments: transformDeploymentsToJSON(config.Deployments), + Emulators: transformEmulatorsToJSON(config.Emulators), + Contracts: transformContractsToJSON(config.Contracts), + Dependencies: transformDependenciesToJSON(config.Dependencies, config.Contracts), + Networks: transformNetworksToJSON(config.Networks), + Accounts: transformAccountsToJSON(config.Accounts), + Deployments: transformDeploymentsToJSON(config.Deployments), } } diff --git a/flowkit/config/json/contract.go b/flowkit/config/json/contract.go index b1a12b6b1..0cc90a160 100644 --- a/flowkit/config/json/contract.go +++ b/flowkit/config/json/contract.go @@ -68,6 +68,11 @@ func transformContractsToJSON(contracts config.Contracts) jsonContracts { jsonContracts := jsonContracts{} for _, c := range contracts { + // If it's a dependency, skip. These are used under the hood and should not be saved. + if c.IsDependency { + continue + } + // if simple case if !c.IsAliased() { jsonContracts[c.Name] = jsonContract{ diff --git a/flowkit/config/json/dependency.go b/flowkit/config/json/dependency.go new file mode 100644 index 000000000..d724ff6a5 --- /dev/null +++ b/flowkit/config/json/dependency.go @@ -0,0 +1,181 @@ +/* + * Flow CLI + * + * Copyright 2019 Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package json + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/invopop/jsonschema" + + "github.com/onflow/flow-go-sdk" + + "github.com/onflow/flow-cli/flowkit/config" +) + +type jsonDependencies map[string]jsonDependency + +func (j jsonDependencies) transformToConfig() (config.Dependencies, error) { + deps := make(config.Dependencies, 0) + + for dependencyName, dependency := range j { + var dep config.Dependency + + if dependency.Simple != "" { + depNetwork, depAddress, depContractName, err := config.ParseSourceString(dependency.Simple) + if err != nil { + return nil, fmt.Errorf("error parsing source for dependency %s: %w", dependencyName, err) + } + + dep = config.Dependency{ + Name: dependencyName, + Source: config.Source{ + NetworkName: depNetwork, + Address: flow.HexToAddress(depAddress), + ContractName: depContractName, + }, + } + } else { + depNetwork, depAddress, depContractName, err := config.ParseSourceString(dependency.Extended.Source) + if err != nil { + return nil, fmt.Errorf("error parsing source for dependency %s: %w", dependencyName, err) + } + + dep = config.Dependency{ + Name: dependencyName, + Hash: dependency.Extended.Hash, + Source: config.Source{ + NetworkName: depNetwork, + Address: flow.HexToAddress(depAddress), + ContractName: depContractName, + }, + } + + for network, alias := range dependency.Extended.Aliases { + address := flow.HexToAddress(alias) + if address == flow.EmptyAddress { + return nil, fmt.Errorf("invalid alias address for a contract") + } + + dep.Aliases.Add(network, address) + } + } + + deps = append(deps, dep) + } + + return deps, nil +} + +func transformDependenciesToJSON(configDependencies config.Dependencies, configContracts config.Contracts) jsonDependencies { + jsonDeps := jsonDependencies{} + + for _, dep := range configDependencies { + aliases := make(map[string]string) + + depContract := configContracts.DependencyContractByName(dep.Name) + if depContract != nil { + for _, alias := range depContract.Aliases { + aliases[alias.Network] = alias.Address.String() + } + } + + jsonDeps[dep.Name] = jsonDependency{ + Extended: jsonDependencyExtended{ + Source: buildSourceString(dep.Source), + Hash: dep.Hash, + Aliases: aliases, + }, + } + } + + return jsonDeps +} + +func buildSourceString(source config.Source) string { + var builder strings.Builder + + builder.WriteString(source.NetworkName) + builder.WriteString("://") + builder.WriteString(source.Address.String()) + builder.WriteString(".") + builder.WriteString(source.ContractName) + + return builder.String() +} + +// jsonDependencyExtended for json parsing advanced config. +type jsonDependencyExtended struct { + Source string `json:"source"` + Hash string `json:"hash"` + Aliases map[string]string `json:"aliases"` +} + +// jsonDependency structure for json parsing. +type jsonDependency struct { + Simple string + Extended jsonDependencyExtended +} + +func (j *jsonDependency) UnmarshalJSON(b []byte) error { + var source string + var extendedFormat jsonDependencyExtended + + // simple + err := json.Unmarshal(b, &source) + if err == nil { + j.Simple = source + return nil + } + + // advanced + err = json.Unmarshal(b, &extendedFormat) + if err == nil { + j.Extended = extendedFormat + } else { + return err + } + + return nil +} + +func (j jsonDependency) MarshalJSON() ([]byte, error) { + if j.Simple != "" { + return json.Marshal(j.Simple) + } else { + return json.Marshal(j.Extended) + } +} + +func (j jsonDependency) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + OneOf: []*jsonschema.Schema{ + { + Type: "string", + }, + { + Ref: "#/$defs/jsonDependencyExtended", + }, + }, + Definitions: map[string]*jsonschema.Schema{ + "jsonDependencyExtended": jsonschema.Reflect(jsonDependencyExtended{}), + }, + } +} diff --git a/flowkit/config/json/dependency_test.go b/flowkit/config/json/dependency_test.go new file mode 100644 index 000000000..3467b130c --- /dev/null +++ b/flowkit/config/json/dependency_test.go @@ -0,0 +1,77 @@ +/* + * Flow CLI + * + * Copyright 2019 Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package json + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_ConfigDependencies(t *testing.T) { + b := []byte(`{ + "HelloWorld": "testnet://877931736ee77cff.HelloWorld" + }`) + + var jsonDependencies jsonDependencies + err := json.Unmarshal(b, &jsonDependencies) + assert.NoError(t, err) + + dependencies, err := jsonDependencies.transformToConfig() + assert.NoError(t, err) + + assert.Len(t, dependencies, 1) + + dependencyOne := dependencies.ByName("HelloWorld") + assert.NotNil(t, dependencyOne) + + assert.NotNil(t, dependencyOne) +} + +func Test_TransformDependenciesToJSON(t *testing.T) { + b := []byte(`{ + "HelloWorld": "testnet://877931736ee77cff.HelloWorld" + }`) + + bOut := []byte(`{ + "HelloWorld": { + "source": "testnet://877931736ee77cff.HelloWorld", + "hash": "", + "aliases": {} + } + }`) + + 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() + dependencies, err := jsonDependencies.transformToConfig() + assert.NoError(t, err) + + j := transformDependenciesToJSON(dependencies, contracts) + x, _ := json.Marshal(j) + + assert.Equal(t, cleanSpecialChars(bOut), cleanSpecialChars(x)) +} diff --git a/flowkit/config/loader.go b/flowkit/config/loader.go index d46f7b1e7..6e1176bee 100644 --- a/flowkit/config/loader.go +++ b/flowkit/config/loader.go @@ -201,6 +201,9 @@ func (l *Loader) composeConfig(baseConf *Config, conf *Config) { for _, contract := range conf.Contracts { baseConf.Contracts.AddOrUpdate(contract) } + for _, dependency := range conf.Dependencies { + baseConf.Dependencies.AddOrUpdate(dependency) + } for _, deployment := range conf.Deployments { baseConf.Deployments.AddOrUpdate(deployment) } diff --git a/flowkit/config/processor.go b/flowkit/config/processor.go index cdcc6924a..883bafade 100644 --- a/flowkit/config/processor.go +++ b/flowkit/config/processor.go @@ -26,11 +26,12 @@ import ( // processorRun all pre-processors. func processorRun(raw []byte) ([]byte, error) { type config struct { - Accounts map[string]map[string]any `json:"accounts,omitempty"` - Contracts any `json:"contracts,omitempty"` - Networks any `json:"networks,omitempty"` - Deployments any `json:"deployments,omitempty"` - Emulators any `json:"emulators,omitempty"` + Accounts map[string]map[string]any `json:"accounts,omitempty"` + Contracts any `json:"contracts,omitempty"` + Dependencies any `json:"dependencies,omitempty"` + Networks any `json:"networks,omitempty"` + Deployments any `json:"deployments,omitempty"` + Emulators any `json:"emulators,omitempty"` } var conf config diff --git a/flowkit/gateway/mocks/Gateway.go b/flowkit/gateway/mocks/Gateway.go index 5fd4ba569..73453438e 100644 --- a/flowkit/gateway/mocks/Gateway.go +++ b/flowkit/gateway/mocks/Gateway.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.36.0. DO NOT EDIT. +// Code generated by mockery v2.38.0. DO NOT EDIT. package mocks @@ -18,6 +18,10 @@ type Gateway struct { func (_m *Gateway) ExecuteScript(_a0 []byte, _a1 []cadence.Value) (cadence.Value, error) { ret := _m.Called(_a0, _a1) + if len(ret) == 0 { + panic("no return value specified for ExecuteScript") + } + var r0 cadence.Value var r1 error if rf, ok := ret.Get(0).(func([]byte, []cadence.Value) (cadence.Value, error)); ok { @@ -44,6 +48,10 @@ func (_m *Gateway) ExecuteScript(_a0 []byte, _a1 []cadence.Value) (cadence.Value func (_m *Gateway) ExecuteScriptAtHeight(_a0 []byte, _a1 []cadence.Value, _a2 uint64) (cadence.Value, error) { ret := _m.Called(_a0, _a1, _a2) + if len(ret) == 0 { + panic("no return value specified for ExecuteScriptAtHeight") + } + var r0 cadence.Value var r1 error if rf, ok := ret.Get(0).(func([]byte, []cadence.Value, uint64) (cadence.Value, error)); ok { @@ -70,6 +78,10 @@ func (_m *Gateway) ExecuteScriptAtHeight(_a0 []byte, _a1 []cadence.Value, _a2 ui func (_m *Gateway) ExecuteScriptAtID(_a0 []byte, _a1 []cadence.Value, _a2 flow.Identifier) (cadence.Value, error) { ret := _m.Called(_a0, _a1, _a2) + if len(ret) == 0 { + panic("no return value specified for ExecuteScriptAtID") + } + var r0 cadence.Value var r1 error if rf, ok := ret.Get(0).(func([]byte, []cadence.Value, flow.Identifier) (cadence.Value, error)); ok { @@ -96,6 +108,10 @@ func (_m *Gateway) ExecuteScriptAtID(_a0 []byte, _a1 []cadence.Value, _a2 flow.I func (_m *Gateway) GetAccount(_a0 flow.Address) (*flow.Account, error) { ret := _m.Called(_a0) + if len(ret) == 0 { + panic("no return value specified for GetAccount") + } + var r0 *flow.Account var r1 error if rf, ok := ret.Get(0).(func(flow.Address) (*flow.Account, error)); ok { @@ -122,6 +138,10 @@ func (_m *Gateway) GetAccount(_a0 flow.Address) (*flow.Account, error) { func (_m *Gateway) GetBlockByHeight(_a0 uint64) (*flow.Block, error) { ret := _m.Called(_a0) + if len(ret) == 0 { + panic("no return value specified for GetBlockByHeight") + } + var r0 *flow.Block var r1 error if rf, ok := ret.Get(0).(func(uint64) (*flow.Block, error)); ok { @@ -148,6 +168,10 @@ func (_m *Gateway) GetBlockByHeight(_a0 uint64) (*flow.Block, error) { func (_m *Gateway) GetBlockByID(_a0 flow.Identifier) (*flow.Block, error) { ret := _m.Called(_a0) + if len(ret) == 0 { + panic("no return value specified for GetBlockByID") + } + var r0 *flow.Block var r1 error if rf, ok := ret.Get(0).(func(flow.Identifier) (*flow.Block, error)); ok { @@ -174,6 +198,10 @@ func (_m *Gateway) GetBlockByID(_a0 flow.Identifier) (*flow.Block, error) { func (_m *Gateway) GetCollection(_a0 flow.Identifier) (*flow.Collection, error) { ret := _m.Called(_a0) + if len(ret) == 0 { + panic("no return value specified for GetCollection") + } + var r0 *flow.Collection var r1 error if rf, ok := ret.Get(0).(func(flow.Identifier) (*flow.Collection, error)); ok { @@ -200,6 +228,10 @@ func (_m *Gateway) GetCollection(_a0 flow.Identifier) (*flow.Collection, error) func (_m *Gateway) GetEvents(_a0 string, _a1 uint64, _a2 uint64) ([]flow.BlockEvents, error) { ret := _m.Called(_a0, _a1, _a2) + if len(ret) == 0 { + panic("no return value specified for GetEvents") + } + var r0 []flow.BlockEvents var r1 error if rf, ok := ret.Get(0).(func(string, uint64, uint64) ([]flow.BlockEvents, error)); ok { @@ -226,6 +258,10 @@ func (_m *Gateway) GetEvents(_a0 string, _a1 uint64, _a2 uint64) ([]flow.BlockEv func (_m *Gateway) GetLatestBlock() (*flow.Block, error) { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for GetLatestBlock") + } + var r0 *flow.Block var r1 error if rf, ok := ret.Get(0).(func() (*flow.Block, error)); ok { @@ -252,6 +288,10 @@ func (_m *Gateway) GetLatestBlock() (*flow.Block, error) { func (_m *Gateway) GetLatestProtocolStateSnapshot() ([]byte, error) { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for GetLatestProtocolStateSnapshot") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func() ([]byte, error)); ok { @@ -278,6 +318,10 @@ func (_m *Gateway) GetLatestProtocolStateSnapshot() ([]byte, error) { func (_m *Gateway) GetTransaction(_a0 flow.Identifier) (*flow.Transaction, error) { ret := _m.Called(_a0) + if len(ret) == 0 { + panic("no return value specified for GetTransaction") + } + var r0 *flow.Transaction var r1 error if rf, ok := ret.Get(0).(func(flow.Identifier) (*flow.Transaction, error)); ok { @@ -304,6 +348,10 @@ func (_m *Gateway) GetTransaction(_a0 flow.Identifier) (*flow.Transaction, error func (_m *Gateway) GetTransactionResult(_a0 flow.Identifier, _a1 bool) (*flow.TransactionResult, error) { ret := _m.Called(_a0, _a1) + if len(ret) == 0 { + panic("no return value specified for GetTransactionResult") + } + var r0 *flow.TransactionResult var r1 error if rf, ok := ret.Get(0).(func(flow.Identifier, bool) (*flow.TransactionResult, error)); ok { @@ -330,6 +378,10 @@ func (_m *Gateway) GetTransactionResult(_a0 flow.Identifier, _a1 bool) (*flow.Tr func (_m *Gateway) GetTransactionResultsByBlockID(blockID flow.Identifier) ([]*flow.TransactionResult, error) { ret := _m.Called(blockID) + if len(ret) == 0 { + panic("no return value specified for GetTransactionResultsByBlockID") + } + var r0 []*flow.TransactionResult var r1 error if rf, ok := ret.Get(0).(func(flow.Identifier) ([]*flow.TransactionResult, error)); ok { @@ -356,6 +408,10 @@ func (_m *Gateway) GetTransactionResultsByBlockID(blockID flow.Identifier) ([]*f func (_m *Gateway) GetTransactionsByBlockID(blockID flow.Identifier) ([]*flow.Transaction, error) { ret := _m.Called(blockID) + if len(ret) == 0 { + panic("no return value specified for GetTransactionsByBlockID") + } + var r0 []*flow.Transaction var r1 error if rf, ok := ret.Get(0).(func(flow.Identifier) ([]*flow.Transaction, error)); ok { @@ -382,6 +438,10 @@ func (_m *Gateway) GetTransactionsByBlockID(blockID flow.Identifier) ([]*flow.Tr func (_m *Gateway) Ping() error { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for Ping") + } + var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() @@ -396,6 +456,10 @@ func (_m *Gateway) Ping() error { func (_m *Gateway) SecureConnection() bool { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for SecureConnection") + } + var r0 bool if rf, ok := ret.Get(0).(func() bool); ok { r0 = rf() @@ -410,6 +474,10 @@ func (_m *Gateway) SecureConnection() bool { func (_m *Gateway) SendSignedTransaction(_a0 *flow.Transaction) (*flow.Transaction, error) { ret := _m.Called(_a0) + if len(ret) == 0 { + panic("no return value specified for SendSignedTransaction") + } + var r0 *flow.Transaction var r1 error if rf, ok := ret.Get(0).(func(*flow.Transaction) (*flow.Transaction, error)); ok { diff --git a/flowkit/mocks/Services.go b/flowkit/mocks/Services.go index 7839bf22b..b63714420 100644 --- a/flowkit/mocks/Services.go +++ b/flowkit/mocks/Services.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.36.0. DO NOT EDIT. +// Code generated by mockery v2.38.0. DO NOT EDIT. package mocks @@ -37,6 +37,10 @@ type Services struct { func (_m *Services) AddContract(_a0 context.Context, _a1 *accounts.Account, _a2 flowkit.Script, _a3 flowkit.UpdateContract) (flow.Identifier, bool, error) { ret := _m.Called(_a0, _a1, _a2, _a3) + if len(ret) == 0 { + panic("no return value specified for AddContract") + } + var r0 flow.Identifier var r1 bool var r2 error @@ -70,6 +74,10 @@ func (_m *Services) AddContract(_a0 context.Context, _a1 *accounts.Account, _a2 func (_m *Services) BuildTransaction(_a0 context.Context, _a1 transactions.AddressesRoles, _a2 int, _a3 flowkit.Script, _a4 uint64) (*transactions.Transaction, error) { ret := _m.Called(_a0, _a1, _a2, _a3, _a4) + if len(ret) == 0 { + panic("no return value specified for BuildTransaction") + } + var r0 *transactions.Transaction var r1 error if rf, ok := ret.Get(0).(func(context.Context, transactions.AddressesRoles, int, flowkit.Script, uint64) (*transactions.Transaction, error)); ok { @@ -96,6 +104,10 @@ func (_m *Services) BuildTransaction(_a0 context.Context, _a1 transactions.Addre func (_m *Services) CreateAccount(_a0 context.Context, _a1 *accounts.Account, _a2 []accounts.PublicKey) (*flow.Account, flow.Identifier, error) { ret := _m.Called(_a0, _a1, _a2) + if len(ret) == 0 { + panic("no return value specified for CreateAccount") + } + var r0 *flow.Account var r1 flow.Identifier var r2 error @@ -131,6 +143,10 @@ func (_m *Services) CreateAccount(_a0 context.Context, _a1 *accounts.Account, _a func (_m *Services) DeployProject(_a0 context.Context, _a1 flowkit.UpdateContract) ([]*project.Contract, error) { ret := _m.Called(_a0, _a1) + if len(ret) == 0 { + panic("no return value specified for DeployProject") + } + var r0 []*project.Contract var r1 error if rf, ok := ret.Get(0).(func(context.Context, flowkit.UpdateContract) ([]*project.Contract, error)); ok { @@ -157,6 +173,10 @@ func (_m *Services) DeployProject(_a0 context.Context, _a1 flowkit.UpdateContrac func (_m *Services) DerivePrivateKeyFromMnemonic(_a0 context.Context, _a1 string, _a2 crypto.SigningAlgorithm, _a3 string) (crypto.PrivateKey, error) { ret := _m.Called(_a0, _a1, _a2, _a3) + if len(ret) == 0 { + panic("no return value specified for DerivePrivateKeyFromMnemonic") + } + var r0 crypto.PrivateKey var r1 error if rf, ok := ret.Get(0).(func(context.Context, string, crypto.SigningAlgorithm, string) (crypto.PrivateKey, error)); ok { @@ -183,6 +203,10 @@ func (_m *Services) DerivePrivateKeyFromMnemonic(_a0 context.Context, _a1 string func (_m *Services) ExecuteScript(_a0 context.Context, _a1 flowkit.Script, _a2 flowkit.ScriptQuery) (cadence.Value, error) { ret := _m.Called(_a0, _a1, _a2) + if len(ret) == 0 { + panic("no return value specified for ExecuteScript") + } + var r0 cadence.Value var r1 error if rf, ok := ret.Get(0).(func(context.Context, flowkit.Script, flowkit.ScriptQuery) (cadence.Value, error)); ok { @@ -209,6 +233,10 @@ func (_m *Services) ExecuteScript(_a0 context.Context, _a1 flowkit.Script, _a2 f func (_m *Services) Gateway() gateway.Gateway { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for Gateway") + } + var r0 gateway.Gateway if rf, ok := ret.Get(0).(func() gateway.Gateway); ok { r0 = rf() @@ -225,6 +253,10 @@ func (_m *Services) Gateway() gateway.Gateway { func (_m *Services) GenerateKey(_a0 context.Context, _a1 crypto.SigningAlgorithm, _a2 string) (crypto.PrivateKey, error) { ret := _m.Called(_a0, _a1, _a2) + if len(ret) == 0 { + panic("no return value specified for GenerateKey") + } + var r0 crypto.PrivateKey var r1 error if rf, ok := ret.Get(0).(func(context.Context, crypto.SigningAlgorithm, string) (crypto.PrivateKey, error)); ok { @@ -251,6 +283,10 @@ func (_m *Services) GenerateKey(_a0 context.Context, _a1 crypto.SigningAlgorithm func (_m *Services) GenerateMnemonicKey(_a0 context.Context, _a1 crypto.SigningAlgorithm, _a2 string) (crypto.PrivateKey, string, error) { ret := _m.Called(_a0, _a1, _a2) + if len(ret) == 0 { + panic("no return value specified for GenerateMnemonicKey") + } + var r0 crypto.PrivateKey var r1 string var r2 error @@ -284,6 +320,10 @@ func (_m *Services) GenerateMnemonicKey(_a0 context.Context, _a1 crypto.SigningA func (_m *Services) GetAccount(_a0 context.Context, _a1 flow.Address) (*flow.Account, error) { ret := _m.Called(_a0, _a1) + if len(ret) == 0 { + panic("no return value specified for GetAccount") + } + var r0 *flow.Account var r1 error if rf, ok := ret.Get(0).(func(context.Context, flow.Address) (*flow.Account, error)); ok { @@ -310,6 +350,10 @@ func (_m *Services) GetAccount(_a0 context.Context, _a1 flow.Address) (*flow.Acc func (_m *Services) GetBlock(_a0 context.Context, _a1 flowkit.BlockQuery) (*flow.Block, error) { ret := _m.Called(_a0, _a1) + if len(ret) == 0 { + panic("no return value specified for GetBlock") + } + var r0 *flow.Block var r1 error if rf, ok := ret.Get(0).(func(context.Context, flowkit.BlockQuery) (*flow.Block, error)); ok { @@ -336,6 +380,10 @@ func (_m *Services) GetBlock(_a0 context.Context, _a1 flowkit.BlockQuery) (*flow func (_m *Services) GetCollection(_a0 context.Context, _a1 flow.Identifier) (*flow.Collection, error) { ret := _m.Called(_a0, _a1) + if len(ret) == 0 { + panic("no return value specified for GetCollection") + } + var r0 *flow.Collection var r1 error if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier) (*flow.Collection, error)); ok { @@ -362,6 +410,10 @@ func (_m *Services) GetCollection(_a0 context.Context, _a1 flow.Identifier) (*fl func (_m *Services) GetEvents(_a0 context.Context, _a1 []string, _a2 uint64, _a3 uint64, _a4 *flowkit.EventWorker) ([]flow.BlockEvents, error) { ret := _m.Called(_a0, _a1, _a2, _a3, _a4) + if len(ret) == 0 { + panic("no return value specified for GetEvents") + } + var r0 []flow.BlockEvents var r1 error if rf, ok := ret.Get(0).(func(context.Context, []string, uint64, uint64, *flowkit.EventWorker) ([]flow.BlockEvents, error)); ok { @@ -388,6 +440,10 @@ func (_m *Services) GetEvents(_a0 context.Context, _a1 []string, _a2 uint64, _a3 func (_m *Services) GetTransactionByID(_a0 context.Context, _a1 flow.Identifier, _a2 bool) (*flow.Transaction, *flow.TransactionResult, error) { ret := _m.Called(_a0, _a1, _a2) + if len(ret) == 0 { + panic("no return value specified for GetTransactionByID") + } + var r0 *flow.Transaction var r1 *flow.TransactionResult var r2 error @@ -423,6 +479,10 @@ func (_m *Services) GetTransactionByID(_a0 context.Context, _a1 flow.Identifier, func (_m *Services) GetTransactionsByBlockID(_a0 context.Context, _a1 flow.Identifier) ([]*flow.Transaction, []*flow.TransactionResult, error) { ret := _m.Called(_a0, _a1) + if len(ret) == 0 { + panic("no return value specified for GetTransactionsByBlockID") + } + var r0 []*flow.Transaction var r1 []*flow.TransactionResult var r2 error @@ -458,6 +518,10 @@ func (_m *Services) GetTransactionsByBlockID(_a0 context.Context, _a1 flow.Ident func (_m *Services) Network() config.Network { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for Network") + } + var r0 config.Network if rf, ok := ret.Get(0).(func() config.Network); ok { r0 = rf() @@ -472,6 +536,10 @@ func (_m *Services) Network() config.Network { func (_m *Services) Ping() error { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for Ping") + } + var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() @@ -486,6 +554,10 @@ func (_m *Services) Ping() error { func (_m *Services) RemoveContract(_a0 context.Context, _a1 *accounts.Account, _a2 string) (flow.Identifier, error) { ret := _m.Called(_a0, _a1, _a2) + if len(ret) == 0 { + panic("no return value specified for RemoveContract") + } + var r0 flow.Identifier var r1 error if rf, ok := ret.Get(0).(func(context.Context, *accounts.Account, string) (flow.Identifier, error)); ok { @@ -512,6 +584,10 @@ func (_m *Services) RemoveContract(_a0 context.Context, _a1 *accounts.Account, _ func (_m *Services) SendSignedTransaction(_a0 context.Context, _a1 *transactions.Transaction) (*flow.Transaction, *flow.TransactionResult, error) { ret := _m.Called(_a0, _a1) + if len(ret) == 0 { + panic("no return value specified for SendSignedTransaction") + } + var r0 *flow.Transaction var r1 *flow.TransactionResult var r2 error @@ -547,6 +623,10 @@ func (_m *Services) SendSignedTransaction(_a0 context.Context, _a1 *transactions func (_m *Services) SendTransaction(_a0 context.Context, _a1 transactions.AccountRoles, _a2 flowkit.Script, _a3 uint64) (*flow.Transaction, *flow.TransactionResult, error) { ret := _m.Called(_a0, _a1, _a2, _a3) + if len(ret) == 0 { + panic("no return value specified for SendTransaction") + } + var r0 *flow.Transaction var r1 *flow.TransactionResult var r2 error @@ -587,6 +667,10 @@ func (_m *Services) SetLogger(_a0 output.Logger) { func (_m *Services) SignTransactionPayload(_a0 context.Context, _a1 *accounts.Account, _a2 []byte) (*transactions.Transaction, error) { ret := _m.Called(_a0, _a1, _a2) + if len(ret) == 0 { + panic("no return value specified for SignTransactionPayload") + } + var r0 *transactions.Transaction var r1 error if rf, ok := ret.Get(0).(func(context.Context, *accounts.Account, []byte) (*transactions.Transaction, error)); ok { diff --git a/flowkit/project/program.go b/flowkit/project/program.go index 0b77bba69..66d0f793c 100644 --- a/flowkit/project/program.go +++ b/flowkit/project/program.go @@ -29,10 +29,11 @@ import ( ) type Program struct { - code []byte - args []cadence.Value - location string - astProgram *ast.Program + code []byte + args []cadence.Value + location string + astProgram *ast.Program + codeWithUnprocessedImports []byte } func NewProgram(code []byte, args []cadence.Value, location string) (*Program, error) { @@ -42,15 +43,31 @@ func NewProgram(code []byte, args []cadence.Value, location string) (*Program, e } return &Program{ - code: code, - args: args, - location: location, - astProgram: astProgram, + code: code, + args: args, + location: location, + astProgram: astProgram, + codeWithUnprocessedImports: code, // has converted import syntax e.g. 'import "Foo"' }, nil } -// imports builds an array of all the import locations -// +func (p *Program) AddressImportDeclarations() []*ast.ImportDeclaration { + addressImports := make([]*ast.ImportDeclaration, 0) + + for _, importDeclaration := range p.astProgram.ImportDeclarations() { + if len(importDeclaration.Identifiers) > 0 && len(importDeclaration.Location.String()) > 0 { + addressImports = append(addressImports, importDeclaration) + } + } + + return addressImports +} + +func (p *Program) HasAddressImports() bool { + return len(p.AddressImportDeclarations()) > 0 +} + +// Imports builds an array of all the import locations // It currently supports getting import locations as identifiers or as strings. Strings locations // can represent a file or an account name, whereas identifiers represent contract names. func (p *Program) imports() []string { @@ -94,6 +111,10 @@ func (p *Program) Code() []byte { return p.code } +func (p *Program) CodeWithUnprocessedImports() []byte { + return p.codeWithUnprocessedImports +} + func (p *Program) Name() (string, error) { if len(p.astProgram.CompositeDeclarations()) > 1 || len(p.astProgram.InterfaceDeclarations()) > 1 || len(p.astProgram.CompositeDeclarations())+len(p.astProgram.InterfaceDeclarations()) > 1 { @@ -115,6 +136,14 @@ func (p *Program) Name() (string, error) { return "", fmt.Errorf("unable to determine contract name") } +func (p *Program) ConvertAddressImports() { + code := string(p.code) + addressImportRegex := regexp.MustCompile(`import\s+(\w+)\s+from\s+0x[0-9a-fA-F]+`) + modifiedCode := addressImportRegex.ReplaceAllString(code, `import "$1"`) + + p.codeWithUnprocessedImports = []byte(modifiedCode) +} + func (p *Program) reload() { astProgram, err := parser.ParseProgram(nil, p.code, parser.Config{}) if err != nil { @@ -122,4 +151,5 @@ func (p *Program) reload() { } p.astProgram = astProgram + p.ConvertAddressImports() } diff --git a/flowkit/project/program_test.go b/flowkit/project/program_test.go index 161999114..171f99328 100644 --- a/flowkit/project/program_test.go +++ b/flowkit/project/program_test.go @@ -28,6 +28,32 @@ import ( func TestProgram(t *testing.T) { + t.Run("AddressImports", func(t *testing.T) { + tests := []struct { + code []byte + expectedCount int + }{ + { + code: []byte(` + import Foo from 0x123 + import "Bar" + import FooSpace from 0x124 + import "BarSpace" + + pub contract Foo {} + `), + expectedCount: 2, + }, + } + + for i, test := range tests { + program, err := NewProgram(test.code, nil, "") + require.NoError(t, err, fmt.Sprintf("AddressImports test %d failed", i)) + addressImports := program.AddressImportDeclarations() + assert.Len(t, addressImports, test.expectedCount, fmt.Sprintf("AddressImports test %d failed", i)) + } + }) + t.Run("Imports", func(t *testing.T) { tests := []struct { code []byte @@ -160,4 +186,31 @@ func TestProgram(t *testing.T) { assert.Equal(t, string(replaced), string(program.Code())) }) + t.Run("Convert to Import Syntax", func(t *testing.T) { + code := []byte(` + import Foo from 0x123 + import "Bar" + import FooSpace from 0x124 + import "BarSpace" + + pub contract Foo {} + `) + + expected := []byte(` + import "Foo" + import "Bar" + import "FooSpace" + import "BarSpace" + + pub contract Foo {} + `) + + program, err := NewProgram(code, nil, "") + require.NoError(t, err) + + program.ConvertAddressImports() + + assert.Equal(t, string(expected), string(program.CodeWithUnprocessedImports())) + }) + } diff --git a/flowkit/schema.json b/flowkit/schema.json index 082b31434..ad6767830 100644 --- a/flowkit/schema.json +++ b/flowkit/schema.json @@ -159,6 +159,9 @@ "contracts": { "$ref": "#/$defs/jsonContracts" }, + "dependencies": { + "$ref": "#/$defs/jsonDependencies" + }, "networks": { "$ref": "#/$defs/jsonNetworks" }, @@ -211,6 +214,49 @@ }, "type": "object" }, + "jsonDependencies": { + "patternProperties": { + ".*": { + "$ref": "#/$defs/jsonDependency" + } + }, + "type": "object" + }, + "jsonDependency": { + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/jsonDependencyExtended" + } + ] + }, + "jsonDependencyExtended": { + "properties": { + "source": { + "type": "string" + }, + "hash": { + "type": "string" + }, + "aliases": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "source", + "hash", + "aliases" + ] + }, "jsonDeployment": { "patternProperties": { ".*": { diff --git a/flowkit/state.go b/flowkit/state.go index 4832ab87f..0f9c975a8 100644 --- a/flowkit/state.go +++ b/flowkit/state.go @@ -38,6 +38,7 @@ type ReaderWriter interface { ReadFile(source string) ([]byte, error) WriteFile(filename string, data []byte, perm os.FileMode) error MkdirAll(path string, perm os.FileMode) error + Stat(path string) (os.FileInfo, error) } // State manages the state for a Flow project. @@ -109,6 +110,10 @@ func (p *State) Contracts() *config.Contracts { return &p.conf.Contracts } +func (p *State) Dependencies() *config.Dependencies { + return &p.conf.Dependencies +} + // Accounts get accounts. func (p *State) Accounts() *accounts.Accounts { return p.accounts diff --git a/flowkit/state_test.go b/flowkit/state_test.go index af39e923b..0be2fc511 100644 --- a/flowkit/state_test.go +++ b/flowkit/state_test.go @@ -697,8 +697,9 @@ func Test_DefaultEmulatorNotPresentInConfig(t *testing.T) { Port: 3569, ServiceAccount: "emulator-account", }}, - Contracts: config.Contracts{}, - Deployments: config.Deployments{}, + Contracts: config.Contracts{}, + Dependencies: config.Dependencies{}, + Deployments: config.Deployments{}, Accounts: config.Accounts{{ Name: "emulator-account", Address: flow.ServiceAddress(flow.Emulator), @@ -805,8 +806,9 @@ func Test_DefaultEmulatorPresentInConfig(t *testing.T) { Port: 3569, ServiceAccount: "emulator-account", }}, - Contracts: config.Contracts{}, - Deployments: config.Deployments{}, + Contracts: config.Contracts{}, + Dependencies: config.Dependencies{}, + Deployments: config.Deployments{}, Accounts: config.Accounts{{ Name: "emulator-account", Address: flow.ServiceAddress(flow.Emulator), @@ -861,8 +863,9 @@ func Test_CustomEmulatorValuesInConfig(t *testing.T) { Port: 2000, ServiceAccount: "emulator-account", }}, - Contracts: config.Contracts{}, - Deployments: config.Deployments{}, + Contracts: config.Contracts{}, + Dependencies: config.Dependencies{}, + Deployments: config.Deployments{}, Accounts: config.Accounts{{ Name: "emulator-account", Address: flow.ServiceAddress(flow.Emulator), diff --git a/flowkit/tests/resources.go b/flowkit/tests/resources.go index bfeaf5128..49808b6c2 100644 --- a/flowkit/tests/resources.go +++ b/flowkit/tests/resources.go @@ -581,6 +581,21 @@ func NewAccountWithAddress(address string) *flow.Account { return account } +func NewAccountWithContracts(address string, contracts ...Resource) *flow.Account { + account := accounts.New() + account.Address = flow.HexToAddress(address) + + if account.Contracts == nil { + account.Contracts = make(map[string][]byte) + } + + for _, contract := range contracts { + account.Contracts[contract.Name] = contract.Source + } + + return account +} + func NewTransaction() *flow.Transaction { return transactions.New() } diff --git a/internal/dependencymanager/add.go b/internal/dependencymanager/add.go new file mode 100644 index 000000000..b972e99a1 --- /dev/null +++ b/internal/dependencymanager/add.go @@ -0,0 +1,75 @@ +/* + * Flow CLI + * + * Copyright 2019 Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dependencymanager + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/onflow/flow-cli/flowkit" + "github.com/onflow/flow-cli/flowkit/output" + "github.com/onflow/flow-cli/internal/command" +) + +type addFlagsCollection struct { + name string `default:"" flag:"name" info:"Name of the dependency"` +} + +var addFlags = addFlagsCollection{} + +var addCommand = &command.Command{ + Cmd: &cobra.Command{ + Use: "add ", + Short: "Add a single contract and its dependencies.", + Example: "flow dependencies add testnet://0afe396ebc8eee65.FlowToken", + Args: cobra.ExactArgs(1), + }, + Flags: &addFlags, + RunS: add, +} + +func add( + args []string, + _ command.GlobalFlags, + logger output.Logger, + flow flowkit.Services, + state *flowkit.State, +) (result command.Result, err error) { + logger.Info(fmt.Sprintf("🔄 Installing dependencies for %s...", args[0])) + + dep := args[0] + + installer, err := NewDependencyInstaller(logger, state) + if err != nil { + logger.Error(fmt.Sprintf("Error: %v", err)) + return nil, err + } + + if err := installer.Add(dep, addFlags.name); err != nil { + logger.Error(fmt.Sprintf("Error: %v", err)) + return nil, err + } + + logger.Info("✅ Dependency installation complete. Check your flow.json") + logger.Info("Ensure you add any required dependencies to your 'deployments' section. This can be done using the 'flow add config deployment' command.") + logger.Info("Note: Core contracts do not need to be added to deployments. For reference, see this URL: https://github.com/onflow/flow-core-contracts") + + return nil, nil +} diff --git a/internal/dependencymanager/dependencies.go b/internal/dependencymanager/dependencies.go new file mode 100644 index 000000000..7175b7e9b --- /dev/null +++ b/internal/dependencymanager/dependencies.go @@ -0,0 +1,36 @@ +/* + * Flow CLI + * + * Copyright 2019 Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dependencymanager + +import ( + "github.com/spf13/cobra" +) + +var Cmd = &cobra.Command{ + Use: "dependencies", + Short: "Manage contracts and dependencies", + TraverseChildren: true, + GroupID: "manager", + Aliases: []string{"deps"}, +} + +func init() { + addCommand.AddToParent(Cmd) + installCommand.AddToParent(Cmd) +} diff --git a/internal/dependencymanager/dependencyinstaller.go b/internal/dependencymanager/dependencyinstaller.go new file mode 100644 index 000000000..0be79d74b --- /dev/null +++ b/internal/dependencymanager/dependencyinstaller.go @@ -0,0 +1,309 @@ +/* + * Flow CLI + * + * Copyright 2019 Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dependencymanager + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "os" + "path/filepath" + "sync" + + "github.com/onflow/flow-cli/internal/util" + + "github.com/onflow/flow-cli/flowkit/gateway" + + "github.com/onflow/flow-cli/flowkit/project" + + flowsdk "github.com/onflow/flow-go-sdk" + + "github.com/onflow/flow-cli/flowkit/config" + + "github.com/onflow/flow-cli/flowkit" + "github.com/onflow/flow-cli/flowkit/output" +) + +type DependencyInstaller struct { + Gateways map[string]gateway.Gateway + Logger output.Logger + State *flowkit.State + Mutex sync.Mutex +} + +// NewDependencyInstaller creates a new instance of DependencyInstaller +func NewDependencyInstaller(logger output.Logger, state *flowkit.State) (*DependencyInstaller, error) { + emulatorGateway, err := gateway.NewGrpcGateway(config.EmulatorNetwork) + if err != nil { + return nil, fmt.Errorf("error creating emulator gateway: %v", err) + } + + testnetGateway, err := gateway.NewGrpcGateway(config.TestnetNetwork) + if err != nil { + return nil, fmt.Errorf("error creating testnet gateway: %v", err) + } + + mainnetGateway, err := gateway.NewGrpcGateway(config.MainnetNetwork) + if err != nil { + return nil, fmt.Errorf("error creating mainnet gateway: %v", err) + } + + gateways := map[string]gateway.Gateway{ + config.EmulatorNetwork.Name: emulatorGateway, + config.TestnetNetwork.Name: testnetGateway, + config.MainnetNetwork.Name: mainnetGateway, + } + + return &DependencyInstaller{ + Gateways: gateways, + Logger: logger, + State: state, + }, nil +} + +// Install processes all the dependencies in the state and installs them and any dependencies they have +func (di *DependencyInstaller) Install() error { + for _, dependency := range *di.State.Dependencies() { + if err := di.processDependency(dependency); err != nil { + di.Logger.Error(fmt.Sprintf("Error processing dependency: %v", err)) + return err + } + } + return nil +} + +// Add processes a single dependency and installs it and any dependencies it has, as well as adding it to the state +func (di *DependencyInstaller) Add(depSource, customName string) error { + depNetwork, depAddress, depContractName, err := config.ParseSourceString(depSource) + if err != nil { + return fmt.Errorf("error parsing source: %w", err) + } + + name := depContractName + + if customName != "" { + name = customName + } + + dep := config.Dependency{ + Name: name, + Source: config.Source{ + NetworkName: depNetwork, + Address: flowsdk.HexToAddress(depAddress), + ContractName: depContractName, + }, + } + + if err := di.processDependency(dep); err != nil { + return fmt.Errorf("error processing dependency: %w", err) + } + + return nil +} + +func (di *DependencyInstaller) processDependency(dependency config.Dependency) error { + depAddress := flowsdk.HexToAddress(dependency.Source.Address.String()) + return di.fetchDependencies(dependency.Source.NetworkName, depAddress, dependency.Name, dependency.Source.ContractName) +} + +func (di *DependencyInstaller) fetchDependencies(networkName string, address flowsdk.Address, assignedName, contractName string) error { + account, err := di.Gateways[networkName].GetAccount(address) + if err != nil { + return fmt.Errorf("failed to get account: %w", err) + } + if account == nil { + return fmt.Errorf("account is nil for address: %s", address) + } + + if account.Contracts == nil { + return fmt.Errorf("contracts are nil for account: %s", address) + } + + var wg sync.WaitGroup + errCh := make(chan error, len(account.Contracts)) + + // Create a max number of goroutines so that we don't rate limit the access node + maxGoroutines := 5 + semaphore := make(chan struct{}, maxGoroutines) + + found := false + + for _, contract := range account.Contracts { + program, err := project.NewProgram(contract, nil, "") + if err != nil { + return fmt.Errorf("failed to parse program: %w", err) + } + + parsedContractName, err := program.Name() + if err != nil { + return fmt.Errorf("failed to parse contract name: %w", err) + } + + if parsedContractName == contractName { + found = true + + if err := di.handleFoundContract(networkName, address.String(), assignedName, parsedContractName, program); err != nil { + return fmt.Errorf("failed to handle found contract: %w", err) + } + + if program.HasAddressImports() { + imports := program.AddressImportDeclarations() + for _, imp := range imports { + wg.Add(1) + go func(importAddress flowsdk.Address, contractName string) { + semaphore <- struct{}{} + defer func() { + <-semaphore + wg.Done() + }() + err := di.fetchDependencies(networkName, importAddress, contractName, contractName) + if err != nil { + errCh <- err + } + }(flowsdk.HexToAddress(imp.Location.String()), imp.Identifiers[0].String()) + } + } + } + } + + if !found { + errMsg := fmt.Sprintf("contract %s not found for account %s on network %s", contractName, address, networkName) + di.Logger.Error(errMsg) + } + + wg.Wait() + close(errCh) + close(semaphore) + + for err := range errCh { + if err != nil { + return err + } + } + + return nil +} + +func (di *DependencyInstaller) contractFileExists(address, contractName string) bool { + fileName := fmt.Sprintf("%s.cdc", contractName) + path := filepath.Join("imports", address, fileName) + + _, err := di.State.ReaderWriter().Stat(path) + + return err == nil +} + +func (di *DependencyInstaller) createContractFile(address, contractName, data string) error { + fileName := fmt.Sprintf("%s.cdc", contractName) + path := filepath.Join("imports", address, fileName) + dir := filepath.Dir(path) + + if err := di.State.ReaderWriter().MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("error creating directories: %w", err) + } + + if err := di.State.ReaderWriter().WriteFile(path, []byte(data), 0644); err != nil { + return fmt.Errorf("error writing file: %w", err) + } + + return nil +} + +func (di *DependencyInstaller) handleFileSystem(contractAddr, contractName, contractData, networkName string) error { + di.Mutex.Lock() + defer di.Mutex.Unlock() + + if !di.contractFileExists(contractAddr, contractName) { + if err := di.createContractFile(contractAddr, contractName, contractData); err != nil { + return fmt.Errorf("failed to create contract file: %w", err) + } + + di.Logger.Info(fmt.Sprintf("Dependency Manager: %s from %s on %s installed", contractName, contractAddr, networkName)) + } + + return nil +} + +func (di *DependencyInstaller) handleFoundContract(networkName, contractAddr, assignedName, contractName string, program *project.Program) error { + hash := sha256.New() + hash.Write(program.CodeWithUnprocessedImports()) + originalContractDataHash := hex.EncodeToString(hash.Sum(nil)) + + program.ConvertAddressImports() + contractData := string(program.CodeWithUnprocessedImports()) + + dependency := di.State.Dependencies().ByName(assignedName) + + // If a dependency by this name already exists and its remote source network or address does not match, then give option to stop or continue + if dependency != nil && (dependency.Source.NetworkName != networkName || dependency.Source.Address.String() != contractAddr) { + di.Logger.Info(fmt.Sprintf("🚫 A dependency named %s already exists with a different remote source. Please fix the conflict and retry.", assignedName)) + os.Exit(0) + return nil + } + + // Check if remote source version is different from local version + // If it is, ask if they want to update + // If no hash, ignore + if dependency != nil && dependency.Hash != "" && dependency.Hash != originalContractDataHash { + msg := fmt.Sprintf("The latest version of %s is different from the one you have locally. Do you want to update it?", contractName) + if !util.GenericBoolPrompt(msg) { + return nil + } + } + + err := di.handleFileSystem(contractAddr, contractName, contractData, networkName) + if err != nil { + return fmt.Errorf("error handling file system: %w", err) + } + + err = di.updateState(networkName, contractAddr, assignedName, contractName, originalContractDataHash) + if err != nil { + di.Logger.Error(fmt.Sprintf("Error updating state: %v", err)) + return err + } + + return nil +} + +func (di *DependencyInstaller) updateState(networkName, contractAddress, assignedName, contractName, contractHash string) error { + dep := config.Dependency{ + Name: assignedName, + Source: config.Source{ + NetworkName: networkName, + Address: flowsdk.HexToAddress(contractAddress), + ContractName: contractName, + }, + Hash: contractHash, + } + + isNewDep := di.State.Dependencies().ByName(dep.Name) == nil + + di.State.Dependencies().AddOrUpdate(dep) + di.State.Contracts().AddDependencyAsContract(dep, networkName) + err := di.State.SaveDefault() + if err != nil { + return err + } + + if isNewDep { + di.Logger.Info(fmt.Sprintf("Dependency Manager: %s added to flow.json", dep.Name)) + } + + return nil +} diff --git a/internal/dependencymanager/dependencyinstaller_test.go b/internal/dependencymanager/dependencyinstaller_test.go new file mode 100644 index 000000000..9a7e1ac59 --- /dev/null +++ b/internal/dependencymanager/dependencyinstaller_test.go @@ -0,0 +1,131 @@ +/* + * Flow CLI + * + * Copyright 2019 Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dependencymanager + +import ( + "fmt" + "testing" + + "github.com/onflow/flow-go-sdk" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/onflow/flow-cli/flowkit/config" + "github.com/onflow/flow-cli/flowkit/gateway" + "github.com/onflow/flow-cli/flowkit/gateway/mocks" + "github.com/onflow/flow-cli/flowkit/output" + "github.com/onflow/flow-cli/flowkit/tests" + "github.com/onflow/flow-cli/internal/util" +) + +func TestDependencyInstallerInstall(t *testing.T) { + + logger := output.NewStdoutLogger(output.NoneLog) + _, state, _ := util.TestMocks(t) + + serviceAcc, _ := state.EmulatorServiceAccount() + serviceAddress := serviceAcc.Address + + dep := config.Dependency{ + Name: "Hello", + Source: config.Source{ + NetworkName: "emulator", + Address: serviceAddress, + ContractName: "Hello", + }, + } + + state.Dependencies().AddOrUpdate(dep) + + t.Run("Success", func(t *testing.T) { + gw := mocks.DefaultMockGateway() + + gw.GetAccount.Run(func(args mock.Arguments) { + addr := args.Get(0).(flow.Address) + assert.Equal(t, addr.String(), serviceAcc.Address.String()) + acc := tests.NewAccountWithAddress(addr.String()) + acc.Contracts = map[string][]byte{ + tests.ContractHelloString.Name: tests.ContractHelloString.Source, + } + + gw.GetAccount.Return(acc, nil) + }) + + di := &DependencyInstaller{ + Gateways: map[string]gateway.Gateway{ + config.EmulatorNetwork.Name: gw.Mock, + config.TestnetNetwork.Name: gw.Mock, + config.MainnetNetwork.Name: gw.Mock, + }, + Logger: logger, + State: state, + } + + err := di.Install() + assert.NoError(t, err, "Failed to install dependencies") + + filePath := fmt.Sprintf("imports/%s/%s.cdc", serviceAddress.String(), tests.ContractHelloString.Name) + fileContent, err := state.ReaderWriter().ReadFile(filePath) + assert.NoError(t, err, "Failed to read generated file") + assert.NotNil(t, fileContent) + }) +} + +func TestDependencyInstallerAdd(t *testing.T) { + + logger := output.NewStdoutLogger(output.NoneLog) + _, state, _ := util.TestMocks(t) + + serviceAcc, _ := state.EmulatorServiceAccount() + serviceAddress := serviceAcc.Address + + t.Run("Success", func(t *testing.T) { + gw := mocks.DefaultMockGateway() + + gw.GetAccount.Run(func(args mock.Arguments) { + addr := args.Get(0).(flow.Address) + assert.Equal(t, addr.String(), serviceAcc.Address.String()) + acc := tests.NewAccountWithAddress(addr.String()) + acc.Contracts = map[string][]byte{ + tests.ContractHelloString.Name: tests.ContractHelloString.Source, + } + + gw.GetAccount.Return(acc, nil) + }) + + di := &DependencyInstaller{ + Gateways: map[string]gateway.Gateway{ + config.EmulatorNetwork.Name: gw.Mock, + config.TestnetNetwork.Name: gw.Mock, + config.MainnetNetwork.Name: gw.Mock, + }, + Logger: logger, + State: state, + } + + sourceStr := fmt.Sprintf("emulator://%s.%s", serviceAddress.String(), tests.ContractHelloString.Name) + err := di.Add(sourceStr, "") + assert.NoError(t, err, "Failed to install dependencies") + + filePath := fmt.Sprintf("imports/%s/%s.cdc", serviceAddress.String(), tests.ContractHelloString.Name) + fileContent, err := state.ReaderWriter().ReadFile(filePath) + assert.NoError(t, err, "Failed to read generated file") + assert.NotNil(t, fileContent) + }) +} diff --git a/internal/dependencymanager/install.go b/internal/dependencymanager/install.go new file mode 100644 index 000000000..cd16effb9 --- /dev/null +++ b/internal/dependencymanager/install.go @@ -0,0 +1,68 @@ +/* + * Flow CLI + * + * Copyright 2019 Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dependencymanager + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/onflow/flow-cli/flowkit" + "github.com/onflow/flow-cli/flowkit/output" + "github.com/onflow/flow-cli/internal/command" +) + +type installFlagsCollection struct{} + +var installFlags = installFlagsCollection{} + +var installCommand = &command.Command{ + Cmd: &cobra.Command{ + Use: "install", + Short: "Install contract and dependencies.", + Example: "flow dependencies install", + }, + Flags: &installFlags, + RunS: install, +} + +func install( + _ []string, + _ command.GlobalFlags, + logger output.Logger, + flow flowkit.Services, + state *flowkit.State, +) (result command.Result, err error) { + logger.Info("🔄 Installing dependencies from flow.json...") + + installer, err := NewDependencyInstaller(logger, state) + if err != nil { + logger.Error(fmt.Sprintf("Error: %v", err)) + return nil, err + } + + if err := installer.Install(); err != nil { + logger.Error(fmt.Sprintf("Error: %v", err)) + return nil, err + } + + logger.Info("✅ Dependency installation complete. Check your flow.json") + + return nil, nil +} diff --git a/internal/util/prompt.go b/internal/util/prompt.go index deb194d73..392aa8d34 100644 --- a/internal/util/prompt.go +++ b/internal/util/prompt.go @@ -756,3 +756,13 @@ func ScaffoldPrompt(logger output.Logger, scaffoldItems []ScaffoldItem) int { return 0 } + +func GenericBoolPrompt(msg string) bool { + prompt := promptui.Select{ + Label: msg, + Items: []string{"Yes", "No"}, + } + _, result, _ := prompt.Run() + + return result == "Yes" +}