From 8be3a6cb3f6c625d4ea199d89b80a20d928f2af3 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 6 Oct 2025 14:28:12 -0500 Subject: [PATCH 1/4] Created a new add-alias option for flow config. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- internal/config/add-alias.go | 118 +++++++++++++++++++++++++++++++++++ internal/config/add.go | 3 +- internal/prompt/prompt.go | 28 +++++++++ 3 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 internal/config/add-alias.go 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.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 From 1d6e08fac6e5930cb09ae7da6be4211c80deb360 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 7 Oct 2025 06:06:39 -0500 Subject: [PATCH 2/4] creates a new add-alias_test --- internal/config/add-alias_test.go | 314 ++++++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 internal/config/add-alias_test.go 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) + }) +} From 35826aa4340f2a4549de26dfad4182c3291dd3eb Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 16 Dec 2025 14:57:57 -0800 Subject: [PATCH 3/4] Validate network addresses --- internal/config/add-alias.go | 10 +++-- internal/config/add-alias_test.go | 70 ++++++++++++++++++++++++------- 2 files changed, 62 insertions(+), 18 deletions(-) diff --git a/internal/config/add-alias.go b/internal/config/add-alias.go index 73bde8d22..8f18a578b 100644 --- a/internal/config/add-alias.go +++ b/internal/config/add-alias.go @@ -22,8 +22,9 @@ import ( "fmt" "github.com/onflow/flow-cli/internal/prompt" + "github.com/onflow/flow-cli/internal/util" - "github.com/onflow/flow-go-sdk" + flow "github.com/onflow/flow-go-sdk" "github.com/spf13/cobra" "github.com/onflow/flowkit/v2" @@ -106,8 +107,9 @@ func flagsToAliasData(flags flagsAddAlias) (*prompt.AliasData, bool, error) { return nil, true, fmt.Errorf("address must be provided") } - if flow.HexToAddress(flags.Address) == flow.EmptyAddress { - return nil, true, fmt.Errorf("invalid address") + // Validate address is valid for the specified network + if !util.IsAddressValidForNetwork(address, flags.Network) { + return nil, true, fmt.Errorf("address %s is not valid for network %s", flags.Address, flags.Network) } return &prompt.AliasData{ @@ -115,4 +117,4 @@ func flagsToAliasData(flags flagsAddAlias) (*prompt.AliasData, bool, error) { 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 index a97c590fb..aebf585e2 100644 --- a/internal/config/add-alias_test.go +++ b/internal/config/add-alias_test.go @@ -45,7 +45,7 @@ func Test_AddAlias(t *testing.T) { // Set flags addAliasFlags.Contract = "MyContract" addAliasFlags.Network = "testnet" - addAliasFlags.Address = "0x1234567890abcdef" + addAliasFlags.Address = "0x9a0766d93b6608b7" // Call the function result, err := addAlias( @@ -68,7 +68,7 @@ func Test_AddAlias(t *testing.T) { // 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()) + assert.Equal(t, "9a0766d93b6608b7", alias.Address.String()) // Reset flags addAliasFlags = flagsAddAlias{} @@ -95,7 +95,7 @@ func Test_AddAlias(t *testing.T) { // Add testnet alias addAliasFlags.Contract = "MultiContract" addAliasFlags.Network = "testnet" - addAliasFlags.Address = "0xabcdef1234567890" + addAliasFlags.Address = "0x631e88ae7f1d7c20" result, err := addAlias( []string{}, @@ -118,7 +118,7 @@ func Test_AddAlias(t *testing.T) { testnetAlias := updatedContract.Aliases.ByNetwork("testnet") require.NotNil(t, testnetAlias) - assert.Equal(t, "abcdef1234567890", testnetAlias.Address.String()) + assert.Equal(t, "631e88ae7f1d7c20", testnetAlias.Address.String()) // Reset flags addAliasFlags = flagsAddAlias{} @@ -129,7 +129,7 @@ func Test_AddAlias(t *testing.T) { addAliasFlags.Contract = "NonExistentContract" addAliasFlags.Network = "testnet" - addAliasFlags.Address = "0x1234567890abcdef" + addAliasFlags.Address = "0x9a0766d93b6608b7" result, err := addAlias( []string{}, @@ -159,7 +159,7 @@ func Test_AddAlias(t *testing.T) { // Set flags addAliasFlags.Contract = "TestContract" addAliasFlags.Network = "mainnet" - addAliasFlags.Address = "0xabcdef1234567890" + addAliasFlags.Address = "0xf233dcee88fe0abe" // Call the function result, err := addAlias( @@ -197,7 +197,7 @@ func Test_AddAlias(t *testing.T) { // 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) + assert.Equal(t, "f233dcee88fe0abe", mainnetAlias) // Reset flags addAliasFlags = flagsAddAlias{} @@ -209,7 +209,7 @@ func Test_FlagsToAliasData(t *testing.T) { flags := flagsAddAlias{ Contract: "TestContract", Network: "testnet", - Address: "0x1234567890abcdef", + Address: "0x9a0766d93b6608b7", } data, flagsProvided, err := flagsToAliasData(flags) @@ -218,7 +218,7 @@ func Test_FlagsToAliasData(t *testing.T) { assert.True(t, flagsProvided) assert.Equal(t, "TestContract", data.Contract) assert.Equal(t, "testnet", data.Network) - assert.Equal(t, "0x1234567890abcdef", data.Address) + assert.Equal(t, "0x9a0766d93b6608b7", data.Address) }) t.Run("No flags provided", func(t *testing.T) { @@ -234,7 +234,7 @@ func Test_FlagsToAliasData(t *testing.T) { t.Run("Fail missing contract name", func(t *testing.T) { flags := flagsAddAlias{ Network: "testnet", - Address: "0x1234567890abcdef", + Address: "0x9a0766d93b6608b7", } data, flagsProvided, err := flagsToAliasData(flags) @@ -247,7 +247,7 @@ func Test_FlagsToAliasData(t *testing.T) { t.Run("Fail missing network", func(t *testing.T) { flags := flagsAddAlias{ Contract: "TestContract", - Address: "0x1234567890abcdef", + Address: "0x9a0766d93b6608b7", } data, flagsProvided, err := flagsToAliasData(flags) @@ -301,14 +301,56 @@ func Test_FlagsToAliasData(t *testing.T) { t.Run("Success with address without 0x prefix", func(t *testing.T) { flags := flagsAddAlias{ Contract: "TestContract", - Network: "testnet", - Address: "1234567890abcdef", + Network: "mainnet", + Address: "1d7e57aa55817448", } data, flagsProvided, err := flagsToAliasData(flags) require.NoError(t, err) assert.True(t, flagsProvided) - assert.Equal(t, "1234567890abcdef", data.Address) + assert.Equal(t, "1d7e57aa55817448", data.Address) + }) + + t.Run("Fail testnet address used for mainnet", func(t *testing.T) { + flags := flagsAddAlias{ + Contract: "TestContract", + Network: "mainnet", + Address: "0x9a0766d93b6608b7", // Testnet address + } + + data, flagsProvided, err := flagsToAliasData(flags) + + assert.Nil(t, data) + assert.True(t, flagsProvided) + assert.ErrorContains(t, err, "not valid for network mainnet") + }) + + t.Run("Fail mainnet address used for testnet", func(t *testing.T) { + flags := flagsAddAlias{ + Contract: "TestContract", + Network: "testnet", + Address: "0xf233dcee88fe0abe", // Mainnet address + } + + data, flagsProvided, err := flagsToAliasData(flags) + + assert.Nil(t, data) + assert.True(t, flagsProvided) + assert.ErrorContains(t, err, "not valid for network testnet") + }) + + t.Run("Fail emulator address used for testnet", func(t *testing.T) { + flags := flagsAddAlias{ + Contract: "TestContract", + Network: "testnet", + Address: "0xf8d6e0586b0a20c7", // Emulator address + } + + data, flagsProvided, err := flagsToAliasData(flags) + + assert.Nil(t, data) + assert.True(t, flagsProvided) + assert.ErrorContains(t, err, "not valid for network testnet") }) } From e5a2e4cc3f88add384e5525c2c67c569067c06eb Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Mon, 22 Dec 2025 20:34:09 -0800 Subject: [PATCH 4/4] Switch to RPC chain ID validation --- internal/config/add-alias.go | 40 +++++++++++++++++------ internal/config/add-alias_test.go | 53 ++++++++++++++++++++++++------- internal/util/util.go | 38 +++++++++++++++++++--- 3 files changed, 105 insertions(+), 26 deletions(-) diff --git a/internal/config/add-alias.go b/internal/config/add-alias.go index 8f18a578b..833673ef1 100644 --- a/internal/config/add-alias.go +++ b/internal/config/add-alias.go @@ -56,16 +56,20 @@ func addAlias( _ []string, globalFlags command.GlobalFlags, _ output.Logger, - _ flowkit.Services, + flowServices flowkit.Services, state *flowkit.State, ) (command.Result, error) { - raw, flagsProvided, err := flagsToAliasData(addAliasFlags) + raw, flagsProvided, err := flagsToAliasData(addAliasFlags, state) if err != nil { return nil, err } if !flagsProvided { raw = prompt.NewAliasPrompt() + err = validateAliasData(raw, state) + if err != nil { + return nil, err + } } contract, err := state.Contracts().ByName(raw.Contract) @@ -90,7 +94,21 @@ func addAlias( }, nil } -func flagsToAliasData(flags flagsAddAlias) (*prompt.AliasData, bool, error) { +func validateAliasData(data *prompt.AliasData, state *flowkit.State) error { + address := flow.HexToAddress(data.Address) + if address == flow.EmptyAddress { + return fmt.Errorf("invalid address") + } + + network, err := state.Networks().ByName(data.Network) + if err != nil { + return fmt.Errorf("network %s not found in configuration", data.Network) + } + + return util.ValidateAddressForNetwork(address, network) +} + +func flagsToAliasData(flags flagsAddAlias, state *flowkit.State) (*prompt.AliasData, bool, error) { if flags.Contract == "" && flags.Network == "" && flags.Address == "" { return nil, false, nil } @@ -107,14 +125,16 @@ func flagsToAliasData(flags flagsAddAlias) (*prompt.AliasData, bool, error) { return nil, true, fmt.Errorf("address must be provided") } - // Validate address is valid for the specified network - if !util.IsAddressValidForNetwork(address, flags.Network) { - return nil, true, fmt.Errorf("address %s is not valid for network %s", flags.Address, flags.Network) - } - - return &prompt.AliasData{ + data := &prompt.AliasData{ Contract: flags.Contract, Network: flags.Network, Address: flags.Address, - }, true, nil + } + + err := validateAliasData(data, state) + if err != nil { + return nil, true, err + } + + return data, true, nil } diff --git a/internal/config/add-alias_test.go b/internal/config/add-alias_test.go index aebf585e2..fbc57380f 100644 --- a/internal/config/add-alias_test.go +++ b/internal/config/add-alias_test.go @@ -206,13 +206,18 @@ func Test_AddAlias(t *testing.T) { func Test_FlagsToAliasData(t *testing.T) { t.Run("Success with all flags", func(t *testing.T) { + _, state, _ := util.TestMocks(t) + + // Add testnet network to state + state.Networks().AddOrUpdate(config.TestnetNetwork) + flags := flagsAddAlias{ Contract: "TestContract", Network: "testnet", Address: "0x9a0766d93b6608b7", } - data, flagsProvided, err := flagsToAliasData(flags) + data, flagsProvided, err := flagsToAliasData(flags, state) require.NoError(t, err) assert.True(t, flagsProvided) @@ -222,9 +227,10 @@ func Test_FlagsToAliasData(t *testing.T) { }) t.Run("No flags provided", func(t *testing.T) { + _, state, _ := util.TestMocks(t) flags := flagsAddAlias{} - data, flagsProvided, err := flagsToAliasData(flags) + data, flagsProvided, err := flagsToAliasData(flags, state) require.NoError(t, err) assert.False(t, flagsProvided) @@ -232,12 +238,13 @@ func Test_FlagsToAliasData(t *testing.T) { }) t.Run("Fail missing contract name", func(t *testing.T) { + _, state, _ := util.TestMocks(t) flags := flagsAddAlias{ Network: "testnet", Address: "0x9a0766d93b6608b7", } - data, flagsProvided, err := flagsToAliasData(flags) + data, flagsProvided, err := flagsToAliasData(flags, state) assert.Nil(t, data) assert.True(t, flagsProvided) @@ -245,12 +252,13 @@ func Test_FlagsToAliasData(t *testing.T) { }) t.Run("Fail missing network", func(t *testing.T) { + _, state, _ := util.TestMocks(t) flags := flagsAddAlias{ Contract: "TestContract", Address: "0x9a0766d93b6608b7", } - data, flagsProvided, err := flagsToAliasData(flags) + data, flagsProvided, err := flagsToAliasData(flags, state) assert.Nil(t, data) assert.True(t, flagsProvided) @@ -258,12 +266,13 @@ func Test_FlagsToAliasData(t *testing.T) { }) t.Run("Fail missing address", func(t *testing.T) { + _, state, _ := util.TestMocks(t) flags := flagsAddAlias{ Contract: "TestContract", Network: "testnet", } - data, flagsProvided, err := flagsToAliasData(flags) + data, flagsProvided, err := flagsToAliasData(flags, state) assert.Nil(t, data) assert.True(t, flagsProvided) @@ -271,13 +280,14 @@ func Test_FlagsToAliasData(t *testing.T) { }) t.Run("Fail invalid address", func(t *testing.T) { + _, state, _ := util.TestMocks(t) flags := flagsAddAlias{ Contract: "TestContract", Network: "testnet", Address: "invalid-address", } - data, flagsProvided, err := flagsToAliasData(flags) + data, flagsProvided, err := flagsToAliasData(flags, state) assert.Nil(t, data) assert.True(t, flagsProvided) @@ -285,13 +295,14 @@ func Test_FlagsToAliasData(t *testing.T) { }) t.Run("Fail empty address", func(t *testing.T) { + _, state, _ := util.TestMocks(t) flags := flagsAddAlias{ Contract: "TestContract", Network: "testnet", Address: "0x0000000000000000", } - data, flagsProvided, err := flagsToAliasData(flags) + data, flagsProvided, err := flagsToAliasData(flags, state) assert.Nil(t, data) assert.True(t, flagsProvided) @@ -299,13 +310,18 @@ func Test_FlagsToAliasData(t *testing.T) { }) t.Run("Success with address without 0x prefix", func(t *testing.T) { + _, state, _ := util.TestMocks(t) + + // Add mainnet network to state + state.Networks().AddOrUpdate(config.MainnetNetwork) + flags := flagsAddAlias{ Contract: "TestContract", Network: "mainnet", Address: "1d7e57aa55817448", } - data, flagsProvided, err := flagsToAliasData(flags) + data, flagsProvided, err := flagsToAliasData(flags, state) require.NoError(t, err) assert.True(t, flagsProvided) @@ -313,13 +329,18 @@ func Test_FlagsToAliasData(t *testing.T) { }) t.Run("Fail testnet address used for mainnet", func(t *testing.T) { + _, state, _ := util.TestMocks(t) + + // Add mainnet network to state + state.Networks().AddOrUpdate(config.MainnetNetwork) + flags := flagsAddAlias{ Contract: "TestContract", Network: "mainnet", Address: "0x9a0766d93b6608b7", // Testnet address } - data, flagsProvided, err := flagsToAliasData(flags) + data, flagsProvided, err := flagsToAliasData(flags, state) assert.Nil(t, data) assert.True(t, flagsProvided) @@ -327,13 +348,18 @@ func Test_FlagsToAliasData(t *testing.T) { }) t.Run("Fail mainnet address used for testnet", func(t *testing.T) { + _, state, _ := util.TestMocks(t) + + // Add testnet network to state + state.Networks().AddOrUpdate(config.TestnetNetwork) + flags := flagsAddAlias{ Contract: "TestContract", Network: "testnet", Address: "0xf233dcee88fe0abe", // Mainnet address } - data, flagsProvided, err := flagsToAliasData(flags) + data, flagsProvided, err := flagsToAliasData(flags, state) assert.Nil(t, data) assert.True(t, flagsProvided) @@ -341,13 +367,18 @@ func Test_FlagsToAliasData(t *testing.T) { }) t.Run("Fail emulator address used for testnet", func(t *testing.T) { + _, state, _ := util.TestMocks(t) + + // Add testnet network to state + state.Networks().AddOrUpdate(config.TestnetNetwork) + flags := flagsAddAlias{ Contract: "TestContract", Network: "testnet", Address: "0xf8d6e0586b0a20c7", // Emulator address } - data, flagsProvided, err := flagsToAliasData(flags) + data, flagsProvided, err := flagsToAliasData(flags, state) assert.Nil(t, data) assert.True(t, flagsProvided) diff --git a/internal/util/util.go b/internal/util/util.go index 6b7685f4b..8a3048686 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -32,11 +32,12 @@ import ( "time" "github.com/onflow/flow-go-sdk" + "github.com/onflow/flow-go-sdk/access/grpc" "github.com/onflow/flow-go-sdk/crypto" "github.com/onflow/flow-go/fvm/systemcontracts" flowGo "github.com/onflow/flow-go/model/flow" flowaccess "github.com/onflow/flow/protobuf/go/flow/access" - "google.golang.org/grpc" + grpcOpts "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" emulatorUtils "github.com/onflow/flow-emulator/utils" @@ -52,7 +53,8 @@ func Exit(code int, msg string) { os.Exit(code) } -// IsAddressValidForNetwork checks if an address is valid for a specific network +// IsAddressValidForNetwork checks if an address is valid for a specific network based on address format +// This is used for address introspection only - use ValidateAddressForNetwork for actual validation func IsAddressValidForNetwork(address flow.Address, networkName string) bool { switch networkName { case "mainnet": @@ -62,11 +64,37 @@ func IsAddressValidForNetwork(address flow.Address, networkName string) bool { case "emulator", "testing": return address.IsValid(flow.Emulator) default: - // For custom networks, assume they use the same validation as emulator return address.IsValid(flow.Emulator) } } +// ValidateAddressForNetwork validates that an address is valid for the specified network +// by querying the access node to get the actual chain ID +func ValidateAddressForNetwork(address flow.Address, network *config.Network) error { + // Create a grpc client to query the network + client, err := grpc.NewBaseClient(network.Host, grpcOpts.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return fmt.Errorf("failed to connect to access node: %w", err) + } + defer client.Close() + + // Get the chain ID from the access node + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + params, err := client.GetNetworkParameters(ctx) + if err != nil { + return fmt.Errorf("failed to get chain ID from access node: %w", err) + } + + // Validate the address against the chain ID returned from the access node + if !address.IsValid(params.ChainID) { + return fmt.Errorf("address %s is not valid for network %s (chain ID: %s)", address, network.Name, params.ChainID) + } + + return nil +} + // entryExists checks if an entry already exists in the content func entryExists(content, entry string) bool { lines := strings.SplitSeq(strings.TrimSpace(content), "\n") @@ -250,9 +278,9 @@ func GetChainIDFromHost(host string) (flowGo.ChainID, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - conn, err := grpc.NewClient( + conn, err := grpcOpts.NewClient( host, - grpc.WithTransportCredentials(insecure.NewCredentials()), + grpcOpts.WithTransportCredentials(insecure.NewCredentials()), emulatorUtils.DefaultGRPCRetryInterceptor(), ) if err != nil {