diff --git a/VERSION b/VERSION index 70b02ffc1..6ecac6812 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.9.4 \ No newline at end of file +1.9.5 \ No newline at end of file diff --git a/cmd/blockchaincmd/export_test.go b/cmd/blockchaincmd/export_test.go new file mode 100644 index 000000000..f243a061b --- /dev/null +++ b/cmd/blockchaincmd/export_test.go @@ -0,0 +1,100 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package blockchaincmd + +import ( + "encoding/json" + "io" + "os" + "path/filepath" + "testing" + + "github.com/ava-labs/avalanche-cli/internal/mocks" + "github.com/ava-labs/avalanche-cli/pkg/application" + "github.com/ava-labs/avalanche-cli/pkg/constants" + "github.com/ava-labs/avalanche-cli/pkg/prompts" + "github.com/ava-labs/avalanche-cli/pkg/ux" + "github.com/ava-labs/avalanche-cli/pkg/vm" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestExportImportSubnet(t *testing.T) { + testDir := t.TempDir() + require := require.New(t) + testSubnet := "testSubnet" + vmVersion := "v0.9.99" + testSubnetEVMCompat := []byte("{\"rpcChainVMProtocolVersion\": {\"v0.9.99\": 18}}") + + app = application.New() + + mockAppDownloader := mocks.Downloader{} + mockAppDownloader.On("Download", mock.Anything).Return(testSubnetEVMCompat, nil) + + app.Setup(testDir, logging.NoLog{}, nil, "", prompts.NewPrompter(), &mockAppDownloader, nil) + ux.NewUserLog(logging.NoLog{}, io.Discard) + + subnetEvmGenesisPath := "tests/e2e/assets/test_subnet_evm_genesis.json" + genBytes, err := os.ReadFile("../../" + subnetEvmGenesisPath) + require.NoError(err) + sc, err := vm.CreateEvmSidecar( + nil, + app, + testSubnet, + vmVersion, + "Test", + false, + true, + true, + ) + require.NoError(err) + err = app.WriteGenesisFile(testSubnet, genBytes) + require.NoError(err) + err = app.CreateSidecar(sc) + require.NoError(err) + + exportOutputDir := filepath.Join(testDir, "output") + err = os.MkdirAll(exportOutputDir, constants.DefaultPerms755) + require.NoError(err) + exportOutput = filepath.Join(exportOutputDir, testSubnet) + defer func() { + exportOutput = "" + app = nil + }() + globalNetworkFlags.UseLocal = true + err = exportSubnet(nil, []string{"this-does-not-exist-should-fail"}) + require.Error(err) + + err = exportSubnet(nil, []string{testSubnet}) + require.NoError(err) + require.FileExists(exportOutput) + sidecarFile := filepath.Join(app.GetBaseDir(), constants.SubnetDir, testSubnet, constants.SidecarFileName) + orig, err := os.ReadFile(sidecarFile) + require.NoError(err) + + var control map[string]interface{} + err = json.Unmarshal(orig, &control) + require.NoError(err) + require.Equal(control["Name"], testSubnet) + require.Equal(control["VM"], "Subnet-EVM") + require.Equal(control["VMVersion"], vmVersion) + require.Equal(control["Subnet"], testSubnet) + require.Equal(control["TokenName"], "Test Token") + require.Equal(control["TokenSymbol"], "Test") + require.Equal(control["Version"], constants.SidecarVersion) + require.Equal(control["Networks"], nil) + + err = os.Remove(sidecarFile) + require.NoError(err) + + err = importFile(nil, []string{"this-does-also-not-exist-import-should-fail"}) + require.ErrorIs(err, os.ErrNotExist) + err = importFile(nil, []string{exportOutput}) + require.ErrorContains(err, "blockchain already exists") + genFile := filepath.Join(app.GetBaseDir(), constants.SubnetDir, testSubnet, constants.GenesisFileName) + err = os.Remove(genFile) + require.NoError(err) + err = importFile(nil, []string{exportOutput}) + require.NoError(err) +} diff --git a/cmd/blockchaincmd/import.go b/cmd/blockchaincmd/import.go index 06241f203..a4d7679bf 100644 --- a/cmd/blockchaincmd/import.go +++ b/cmd/blockchaincmd/import.go @@ -19,6 +19,8 @@ or importing from blockchains running public networks (e.g. created manually or with the deprecated subnet-cli)`, RunE: cobrautils.CommandSuiteUsage, } + // blockchain import file + cmd.AddCommand(newImportFileCmd()) // blockchain import public cmd.AddCommand(newImportPublicCmd()) return cmd diff --git a/cmd/blockchaincmd/import_file.go b/cmd/blockchaincmd/import_file.go new file mode 100644 index 000000000..e0cf4bf83 --- /dev/null +++ b/cmd/blockchaincmd/import_file.go @@ -0,0 +1,147 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package blockchaincmd + +import ( + "encoding/json" + "errors" + "fmt" + "os" + + "github.com/ava-labs/avalanche-cli/pkg/cobrautils" + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/pkg/ux" + "github.com/ava-labs/avalanche-cli/pkg/vm" + "github.com/spf13/cobra" +) + +// avalanche blockchain import file +func newImportFileCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "file [blockchainPath]", + Short: "Import an existing blockchain config", + RunE: importFile, + Args: cobrautils.MaximumNArgs(1), + Long: `The blockchain import file command will import a blockchain configuration from a file. + +You can optionally provide the path as a command-line argument. +Alternatively, running the command without any arguments triggers an interactive wizard. +By default, an imported blockchain doesn't overwrite an existing blockchain with the same name. +To allow overwrites, provide the --force flag.`, + } + cmd.Flags().BoolVarP( + &overwriteImport, + "force", + "f", + false, + "overwrite the existing configuration if one exists", + ) + return cmd +} + +func importFile(_ *cobra.Command, args []string) error { + var ( + importPath string + err error + ) + if len(args) == 1 { + importPath = args[0] + } + + if importPath == "" { + promptStr := "Select the file to import your blockchain from" + importPath, err = app.Prompt.CaptureExistingFilepath(promptStr) + if err != nil { + return err + } + } + + importFileBytes, err := os.ReadFile(importPath) + if err != nil { + return err + } + + importable := models.Exportable{} + err = json.Unmarshal(importFileBytes, &importable) + if err != nil { + return err + } + + blockchainName := importable.Sidecar.Name + if blockchainName == "" { + return errors.New("export data is malformed: missing blockchain name") + } + + if app.GenesisExists(blockchainName) && !overwriteImport { + return errors.New("blockchain already exists. Use --" + forceFlag + " parameter to overwrite") + } + + if importable.Sidecar.VM == models.CustomVM { + if importable.Sidecar.CustomVMRepoURL == "" { + return fmt.Errorf("repository url must be defined for custom vm import") + } + if importable.Sidecar.CustomVMBranch == "" { + return fmt.Errorf("repository branch must be defined for custom vm import") + } + if importable.Sidecar.CustomVMBuildScript == "" { + return fmt.Errorf("build script must be defined for custom vm import") + } + + if err := vm.BuildCustomVM(app, &importable.Sidecar); err != nil { + return err + } + + vmPath := app.GetCustomVMPath(blockchainName) + rpcVersion, err := vm.GetVMBinaryProtocolVersion(vmPath) + if err != nil { + return fmt.Errorf("unable to get custom binary RPC version: %w", err) + } + if rpcVersion != importable.Sidecar.RPCVersion { + return fmt.Errorf("RPC version mismatch between sidecar and vm binary (%d vs %d)", importable.Sidecar.RPCVersion, rpcVersion) + } + } + + if err := app.WriteGenesisFile(blockchainName, importable.Genesis); err != nil { + return err + } + + if importable.NodeConfig != nil { + if err := app.WriteAvagoNodeConfigFile(blockchainName, importable.NodeConfig); err != nil { + return err + } + } else { + _ = os.RemoveAll(app.GetAvagoNodeConfigPath(blockchainName)) + } + + if importable.ChainConfig != nil { + if err := app.WriteChainConfigFile(blockchainName, importable.ChainConfig); err != nil { + return err + } + } else { + _ = os.RemoveAll(app.GetChainConfigPath(blockchainName)) + } + + if importable.SubnetConfig != nil { + if err := app.WriteAvagoSubnetConfigFile(blockchainName, importable.SubnetConfig); err != nil { + return err + } + } else { + _ = os.RemoveAll(app.GetAvagoSubnetConfigPath(blockchainName)) + } + + if importable.NetworkUpgrades != nil { + if err := app.WriteNetworkUpgradesFile(blockchainName, importable.NetworkUpgrades); err != nil { + return err + } + } else { + _ = os.RemoveAll(app.GetUpgradeBytesFilepath(blockchainName)) + } + + if err := app.CreateSidecar(&importable.Sidecar); err != nil { + return err + } + + ux.Logger.PrintToUser("Blockchain imported successfully") + + return nil +}