diff --git a/internal/config/add-alias.go b/internal/config/add-alias.go new file mode 100644 index 000000000..73bde8d22 --- /dev/null +++ b/internal/config/add-alias.go @@ -0,0 +1,118 @@ +/* + * Flow CLI + * + * Copyright Flow Foundation + * + * 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" + + "github.com/onflow/flow-cli/internal/prompt" + + "github.com/onflow/flow-go-sdk" + "github.com/spf13/cobra" + + "github.com/onflow/flowkit/v2" + "github.com/onflow/flowkit/v2/output" + + "github.com/onflow/flow-cli/internal/command" +) + +type flagsAddAlias struct { + Contract string `flag:"contract" info:"Name of the contract to add alias for"` + Network string `flag:"network" info:"Network name for the alias"` + Address string `flag:"address" info:"Address for the alias"` +} + +var addAliasFlags = flagsAddAlias{} + +var addAliasCommand = &command.Command{ + Cmd: &cobra.Command{ + Use: "alias", + Short: "Add alias to contract configuration", + Example: "flow config add alias --contract MyContract --network testnet --address 0x1234567890abcdef", + Args: cobra.NoArgs, + }, + Flags: &addAliasFlags, + RunS: addAlias, +} + +func addAlias( + _ []string, + globalFlags command.GlobalFlags, + _ output.Logger, + _ flowkit.Services, + state *flowkit.State, +) (command.Result, error) { + raw, flagsProvided, err := flagsToAliasData(addAliasFlags) + if err != nil { + return nil, err + } + + if !flagsProvided { + raw = prompt.NewAliasPrompt() + } + + contract, err := state.Contracts().ByName(raw.Contract) + if err != nil { + return nil, fmt.Errorf("contract %s not found in configuration: %w", raw.Contract, err) + } + + contract.Aliases.Add( + raw.Network, + flow.HexToAddress(raw.Address), + ) + + state.Contracts().AddOrUpdate(*contract) + + err = state.SaveEdited(globalFlags.ConfigPaths) + if err != nil { + return nil, err + } + + return &result{ + result: fmt.Sprintf("Alias for contract %s on network %s added to the configuration", raw.Contract, raw.Network), + }, nil +} + +func flagsToAliasData(flags flagsAddAlias) (*prompt.AliasData, bool, error) { + if flags.Contract == "" && flags.Network == "" && flags.Address == "" { + return nil, false, nil + } + + if flags.Contract == "" { + return nil, true, fmt.Errorf("contract name must be provided") + } + + if flags.Network == "" { + return nil, true, fmt.Errorf("network name must be provided") + } + + if flags.Address == "" { + return nil, true, fmt.Errorf("address must be provided") + } + + if flow.HexToAddress(flags.Address) == flow.EmptyAddress { + return nil, true, fmt.Errorf("invalid address") + } + + return &prompt.AliasData{ + Contract: flags.Contract, + Network: flags.Network, + Address: flags.Address, + }, true, nil +} \ No newline at end of file diff --git a/internal/config/add-alias_test.go b/internal/config/add-alias_test.go new file mode 100644 index 000000000..a97c590fb --- /dev/null +++ b/internal/config/add-alias_test.go @@ -0,0 +1,314 @@ +/* + * Flow CLI + * + * Copyright Flow Foundation + * + * 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 ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/onflow/flowkit/v2/config" + + "github.com/onflow/flow-cli/internal/command" + "github.com/onflow/flow-cli/internal/util" +) + +func Test_AddAlias(t *testing.T) { + t.Run("Success", func(t *testing.T) { + srv, state, _ := util.TestMocks(t) + + // Setup: Add a contract to the state first + contract := config.Contract{ + Name: "MyContract", + Location: "contracts/MyContract.cdc", + } + state.Contracts().AddOrUpdate(contract) + + // Set flags + addAliasFlags.Contract = "MyContract" + addAliasFlags.Network = "testnet" + addAliasFlags.Address = "0x1234567890abcdef" + + // Call the function + result, err := addAlias( + []string{}, + command.GlobalFlags{ConfigPaths: []string{"flow.json"}}, + util.NoLogger, + srv.Mock, + state, + ) + + // Verify no errors + require.NoError(t, err) + assert.NotNil(t, result) + assert.Contains(t, result.String(), "Alias for contract MyContract on network testnet added") + + // Verify the state was modified correctly + updatedContract, err := state.Contracts().ByName("MyContract") + require.NoError(t, err) + + // Verify the alias was added for the specified network + alias := updatedContract.Aliases.ByNetwork("testnet") + require.NotNil(t, alias) + assert.Equal(t, "1234567890abcdef", alias.Address.String()) + + // Reset flags + addAliasFlags = flagsAddAlias{} + }) + + t.Run("Success with multiple aliases", func(t *testing.T) { + srv, state, _ := util.TestMocks(t) + + // Get the emulator service account address + serviceAcc, err := state.EmulatorServiceAccount() + require.NoError(t, err) + + // Setup: Add a contract with an existing alias + contract := config.Contract{ + Name: "MultiContract", + Location: "contracts/MultiContract.cdc", + Aliases: config.Aliases{{ + Network: "emulator", + Address: serviceAcc.Address, + }}, + } + state.Contracts().AddOrUpdate(contract) + + // Add testnet alias + addAliasFlags.Contract = "MultiContract" + addAliasFlags.Network = "testnet" + addAliasFlags.Address = "0xabcdef1234567890" + + result, err := addAlias( + []string{}, + command.GlobalFlags{ConfigPaths: []string{"flow.json"}}, + util.NoLogger, + srv.Mock, + state, + ) + + require.NoError(t, err) + assert.NotNil(t, result) + + // Verify both aliases exist + updatedContract, err := state.Contracts().ByName("MultiContract") + require.NoError(t, err) + + emulatorAlias := updatedContract.Aliases.ByNetwork("emulator") + require.NotNil(t, emulatorAlias) + assert.Equal(t, serviceAcc.Address.String(), emulatorAlias.Address.String()) + + testnetAlias := updatedContract.Aliases.ByNetwork("testnet") + require.NotNil(t, testnetAlias) + assert.Equal(t, "abcdef1234567890", testnetAlias.Address.String()) + + // Reset flags + addAliasFlags = flagsAddAlias{} + }) + + t.Run("Fail contract not found", func(t *testing.T) { + srv, state, _ := util.TestMocks(t) + + addAliasFlags.Contract = "NonExistentContract" + addAliasFlags.Network = "testnet" + addAliasFlags.Address = "0x1234567890abcdef" + + result, err := addAlias( + []string{}, + command.GlobalFlags{ConfigPaths: []string{"flow.json"}}, + util.NoLogger, + srv.Mock, + state, + ) + + assert.Nil(t, result) + assert.ErrorContains(t, err, "contract NonExistentContract not found in configuration") + + // Reset flags + addAliasFlags = flagsAddAlias{} + }) + + t.Run("Verify flow.json is modified correctly", func(t *testing.T) { + srv, state, rw := util.TestMocks(t) + + // Setup: Add a contract to the state + contract := config.Contract{ + Name: "TestContract", + Location: "contracts/TestContract.cdc", + } + state.Contracts().AddOrUpdate(contract) + + // Set flags + addAliasFlags.Contract = "TestContract" + addAliasFlags.Network = "mainnet" + addAliasFlags.Address = "0xabcdef1234567890" + + // Call the function + result, err := addAlias( + []string{}, + command.GlobalFlags{ConfigPaths: []string{"flow.json"}}, + util.NoLogger, + srv.Mock, + state, + ) + + require.NoError(t, err) + assert.NotNil(t, result) + + // Read the flow.json file + flowJSON, err := rw.ReadFile("flow.json") + require.NoError(t, err) + + // Unmarshal and verify the JSON structure + var flowConfig map[string]interface{} + err = json.Unmarshal(flowJSON, &flowConfig) + require.NoError(t, err) + + // Verify contracts section exists + contracts, ok := flowConfig["contracts"].(map[string]interface{}) + require.True(t, ok, "contracts section should exist in flow.json") + + // Verify TestContract exists + testContract, ok := contracts["TestContract"].(map[string]interface{}) + require.True(t, ok, "TestContract should exist in flow.json") + + // Verify aliases section exists in the contract + aliases, ok := testContract["aliases"].(map[string]interface{}) + require.True(t, ok, "aliases section should exist in TestContract") + + // Verify mainnet alias exists with correct address (stored without 0x prefix) + mainnetAlias, ok := aliases["mainnet"].(string) + require.True(t, ok, "mainnet alias should exist") + assert.Equal(t, "abcdef1234567890", mainnetAlias) + + // Reset flags + addAliasFlags = flagsAddAlias{} + }) +} + +func Test_FlagsToAliasData(t *testing.T) { + t.Run("Success with all flags", func(t *testing.T) { + flags := flagsAddAlias{ + Contract: "TestContract", + Network: "testnet", + Address: "0x1234567890abcdef", + } + + data, flagsProvided, err := flagsToAliasData(flags) + + require.NoError(t, err) + assert.True(t, flagsProvided) + assert.Equal(t, "TestContract", data.Contract) + assert.Equal(t, "testnet", data.Network) + assert.Equal(t, "0x1234567890abcdef", data.Address) + }) + + t.Run("No flags provided", func(t *testing.T) { + flags := flagsAddAlias{} + + data, flagsProvided, err := flagsToAliasData(flags) + + require.NoError(t, err) + assert.False(t, flagsProvided) + assert.Nil(t, data) + }) + + t.Run("Fail missing contract name", func(t *testing.T) { + flags := flagsAddAlias{ + Network: "testnet", + Address: "0x1234567890abcdef", + } + + data, flagsProvided, err := flagsToAliasData(flags) + + assert.Nil(t, data) + assert.True(t, flagsProvided) + assert.EqualError(t, err, "contract name must be provided") + }) + + t.Run("Fail missing network", func(t *testing.T) { + flags := flagsAddAlias{ + Contract: "TestContract", + Address: "0x1234567890abcdef", + } + + data, flagsProvided, err := flagsToAliasData(flags) + + assert.Nil(t, data) + assert.True(t, flagsProvided) + assert.EqualError(t, err, "network name must be provided") + }) + + t.Run("Fail missing address", func(t *testing.T) { + flags := flagsAddAlias{ + Contract: "TestContract", + Network: "testnet", + } + + data, flagsProvided, err := flagsToAliasData(flags) + + assert.Nil(t, data) + assert.True(t, flagsProvided) + assert.EqualError(t, err, "address must be provided") + }) + + t.Run("Fail invalid address", func(t *testing.T) { + flags := flagsAddAlias{ + Contract: "TestContract", + Network: "testnet", + Address: "invalid-address", + } + + data, flagsProvided, err := flagsToAliasData(flags) + + assert.Nil(t, data) + assert.True(t, flagsProvided) + assert.EqualError(t, err, "invalid address") + }) + + t.Run("Fail empty address", func(t *testing.T) { + flags := flagsAddAlias{ + Contract: "TestContract", + Network: "testnet", + Address: "0x0000000000000000", + } + + data, flagsProvided, err := flagsToAliasData(flags) + + assert.Nil(t, data) + assert.True(t, flagsProvided) + assert.EqualError(t, err, "invalid address") + }) + + t.Run("Success with address without 0x prefix", func(t *testing.T) { + flags := flagsAddAlias{ + Contract: "TestContract", + Network: "testnet", + Address: "1234567890abcdef", + } + + data, flagsProvided, err := flagsToAliasData(flags) + + require.NoError(t, err) + assert.True(t, flagsProvided) + assert.Equal(t, "1234567890abcdef", data.Address) + }) +} diff --git a/internal/config/add.go b/internal/config/add.go index 23ed0c5c6..104b62efa 100644 --- a/internal/config/add.go +++ b/internal/config/add.go @@ -23,7 +23,7 @@ import ( ) var addCmd = &cobra.Command{ - Use: "add ", + Use: "add ", Short: "Add resource to configuration", Example: "flow config add account", Args: cobra.ExactArgs(1), @@ -32,6 +32,7 @@ var addCmd = &cobra.Command{ func init() { addAccountCommand.AddToParent(addCmd) + addAliasCommand.AddToParent(addCmd) addContractCommand.AddToParent(addCmd) addDeploymentCommand.AddToParent(addCmd) addNetworkCommand.AddToParent(addCmd) diff --git a/internal/prompt/prompt.go b/internal/prompt/prompt.go index 376a20a25..4fbb089e8 100644 --- a/internal/prompt/prompt.go +++ b/internal/prompt/prompt.go @@ -374,6 +374,34 @@ func NewNetworkPrompt() map[string]string { return networkData } +func NewAliasPrompt() *AliasData { + alias := &AliasData{ + Contract: NamePrompt(), + } + + alias.Network, _ = RunTextInputWithValidation( + "Enter network name", + "testnet", + "", + func(s string) error { + if len(s) < 1 { + return fmt.Errorf("network name cannot be empty") + } + return nil + }, + ) + + alias.Address = addressPrompt("Enter address for alias", "invalid address", false) + + return alias +} + +type AliasData struct { + Contract string + Network string + Address string +} + type DeploymentData struct { Network string Account string