Repository: onflow/flow-cli (go)

Issue:
Emulator command for migrating contracts/state to Cadence 1.0

### Issue To Be Solved

The CLI should have a command to take in an Emulator snapshot created using a recent Emulator version with Cadence v0.42, run the Cadence 1.0 contract and state migrations on it, and produce a new Emulator snapshot for the latest version  of the Emulator with Cadence v1.0.

### Suggest A Solution

- Add a command like `flow migrate`
- Call the Emulator's Cadence 1.0 migration function, added in https://github.com/onflow/flow-emulator/pull/557


Pull request description:
Add `flow migrate state` command
Closes #1433

## Description

<!--
Add a description of the changes that this PR introduces and the files that
are the most critical to review.
-->

______

For contributor use:

- [ ] Targeted PR against `master` branch
- [x] Linked to Github issue with discussion and accepted design OR link to spec that describes this work
- [x] Code follows the [standards mentioned here](https://github.com/onflow/flow-cli/blob/master/CONTRIBUTING.md#styleguides)
- [x] Updated relevant documentation
- [x] Re-reviewed `Files changed` in the Github PR explorer
- [x] Added appropriate labels


Patch:
diff --git a/go.mod b/go.mod
index 37679987..a7e66f9e 100644
--- a/go.mod
+++ b/go.mod
@@ -26,6 +26,7 @@ require (
 	github.com/pkg/errors v0.9.1
 	github.com/psiemens/sconfig v0.1.0
 	github.com/radovskyb/watcher v1.0.7
+	github.com/rs/zerolog v1.31.0
 	github.com/sergi/go-diff v1.3.1
 	github.com/spf13/afero v1.10.0
 	github.com/spf13/cobra v1.8.0
@@ -168,6 +169,7 @@ require (
 	github.com/mattn/go-tty v0.0.4 // indirect
 	github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
 	github.com/minio/sha256-simd v1.0.1 // indirect
+	github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/mmcloughlin/addchain v0.4.0 // indirect
 	github.com/mr-tron/base58 v1.2.0 // indirect
@@ -207,7 +209,7 @@ require (
 	github.com/rivo/uniseg v0.4.4 // indirect
 	github.com/rogpeppe/go-internal v1.11.0 // indirect
 	github.com/rs/cors v1.8.0 // indirect
-	github.com/rs/zerolog v1.31.0 // indirect
+	github.com/schollz/progressbar/v3 v3.13.1 // indirect
 	github.com/sethvargo/go-retry v0.2.3 // indirect
 	github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
 	github.com/skeema/knownhosts v1.2.1 // indirect
@@ -253,6 +255,7 @@ require (
 	golang.org/x/oauth2 v0.16.0 // indirect
 	golang.org/x/sync v0.6.0 // indirect
 	golang.org/x/sys v0.17.0 // indirect
+	golang.org/x/term v0.17.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
 	golang.org/x/time v0.3.0 // indirect
 	golang.org/x/tools v0.18.0 // indirect
diff --git a/go.sum b/go.sum
index bbc90c26..7bfe1986 100644
--- a/go.sum
+++ b/go.sum
@@ -2940,6 +2940,7 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
 golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
 golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
 golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
diff --git a/internal/migrate/migrate.go b/internal/migrate/migrate.go
index fa3265a7..20e0b0d8 100644
--- a/internal/migrate/migrate.go
+++ b/internal/migrate/migrate.go
@@ -36,6 +36,7 @@ func init() {
 	listStagedContractsCommand.AddToParent(Cmd)
 	stageContractCommand.AddToParent(Cmd)
 	unstageContractCommand.AddToParent(Cmd)
+	stateCommand.AddToParent(Cmd)
 }
 
 var Cmd = &cobra.Command{
diff --git a/internal/migrate/state.go b/internal/migrate/state.go
new file mode 100644
index 00000000..e444ca5c
--- /dev/null
+++ b/internal/migrate/state.go
@@ -0,0 +1,143 @@
+/*
+ * 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 migrate
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/onflow/cadence/runtime/common"
+	"github.com/onflow/flow-emulator/storage/migration"
+	emulatorMigrate "github.com/onflow/flow-emulator/storage/migration"
+	"github.com/onflow/flow-emulator/storage/sqlite"
+	"github.com/onflow/flow-go-sdk"
+	"github.com/onflow/flow-go/cmd/util/ledger/migrations"
+	"github.com/onflow/flow-go/cmd/util/ledger/reporters"
+	"github.com/onflow/flowkit/v2"
+	"github.com/onflow/flowkit/v2/config"
+	"github.com/onflow/flowkit/v2/output"
+	"github.com/rs/zerolog"
+	"github.com/spf13/cobra"
+
+	"github.com/onflow/flow-cli/internal/command"
+)
+
+var stateFlags struct {
+	Contracts  []string `default:"" flag:"contracts" info:"contract names to migrate"`
+	SaveReport string   `default:"" flag:"save-report" info:"save migration report to a given directory if provided"`
+	DBPath     string   `default:"./flowdb" flag:"db-path" info:"path to the sqlite database file"`
+}
+
+var stateCommand = &command.Command{
+	Cmd: &cobra.Command{
+		Use:     "state",
+		Short:   "Migrate the state of a SQLite Flow Emulator database",
+		Example: `flow migrate state`,
+		Args:    cobra.MinimumNArgs(0),
+	},
+	Flags: &stateFlags,
+	RunS:  migrateState,
+}
+
+func migrateState(
+	args []string,
+	globalFlags command.GlobalFlags,
+	_ output.Logger,
+	flow flowkit.Services,
+	state *flowkit.State,
+) (command.Result, error) {
+	if globalFlags.Network != config.EmulatorNetwork.Name {
+		return nil, fmt.Errorf("state migration is only supported for the emulator network")
+	}
+
+	contracts, err := resolveStagedContracts(state, stateFlags.Contracts)
+	if err != nil {
+		return nil, fmt.Errorf("failed to resolve staged contracts: %w", err)
+	}
+
+	store, err := sqlite.New(stateFlags.DBPath)
+	if err != nil {
+		return nil, fmt.Errorf("failed to open database: %w", err)
+	}
+
+	logger := zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger()
+
+	// Create a report writer factory if a report path is provided
+	var rwf reporters.ReportWriterFactory
+	if stateFlags.SaveReport != "" {
+		rwf = reporters.NewReportFileWriterFactory(stateFlags.SaveReport, logger)
+	} else {
+		rwf = &migration.NOOPReportWriterFactory{}
+	}
+
+	err = emulatorMigrate.MigrateCadence1(
+		store,
+		migrations.EVMContractChangeNone,
+		migrations.BurnerContractChangeDeploy,
+		contracts,
+		rwf,
+		logger,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("failed to migrate database: %w", err)
+	}
+
+	return nil, nil
+}
+
+func resolveStagedContracts(state *flowkit.State, contractNames []string) ([]migrations.StagedContract, error) {
+	contracts := make([]migrations.StagedContract, len(contractNames))
+
+	for i, contractName := range contractNames {
+		// First try to get contract address from aliases
+		contract, err := state.Contracts().ByName(contractName)
+		if err != nil {
+			return nil, fmt.Errorf("failed to get contract by name: %w", err)
+		}
+
+		var address flow.Address
+		alias := contract.Aliases.ByNetwork(config.EmulatorNetwork.Name)
+		if alias != nil {
+			address = alias.Address
+		}
+
+		code, err := state.ReadFile(contract.Location)
+		if err != nil {
+			return nil, fmt.Errorf("failed to read contract file: %w", err)
+		}
+
+		// If contract is not aliased, try to get address by deployment account
+		if address == flow.EmptyAddress {
+			address, err = getAddressByContractName(state, contractName, config.EmulatorNetwork)
+			if err != nil {
+				return nil, fmt.Errorf("failed to get address by contract name: %w", err)
+			}
+		}
+
+		contracts[i] = migrations.StagedContract{
+			Contract: migrations.Contract{
+				Name: contractName,
+				Code: code,
+			},
+			Address: common.Address(address),
+		}
+	}
+
+	return contracts, nil
+}


Test patch:
diff --git a/internal/migrate/state_test.go b/internal/migrate/state_test.go
new file mode 100644
index 00000000..1377aef1
--- /dev/null
+++ b/internal/migrate/state_test.go
@@ -0,0 +1,101 @@
+/*
+ * 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 migrate
+
+import (
+	"testing"
+
+	"github.com/onflow/cadence/runtime/common"
+	"github.com/onflow/flow-go-sdk"
+	"github.com/onflow/flow-go/cmd/util/ledger/migrations"
+	"github.com/onflow/flowkit/v2/config"
+	"github.com/onflow/flowkit/v2/tests"
+	"github.com/stretchr/testify/assert"
+
+	"github.com/onflow/flow-cli/internal/util"
+)
+
+func Test_MigrateState(t *testing.T) {
+	_, state, _ := util.TestMocks(t)
+
+	testContractAliased := tests.ContractSimple
+	testContractDeployed := tests.ContractA
+
+	t.Run("resolves staged contracts by name", func(t *testing.T) {
+		// Add an aliased contract to state
+		state.Contracts().AddOrUpdate(
+			config.Contract{
+				Name:     testContractAliased.Name,
+				Location: testContractAliased.Filename,
+				Aliases: config.Aliases{
+					{
+						Network: "emulator",
+						Address: flow.HexToAddress("0x1"),
+					},
+				},
+			},
+		)
+
+		state.Contracts().AddOrUpdate(
+			config.Contract{
+				Name:     testContractDeployed.Name,
+				Location: testContractDeployed.Filename,
+			},
+		)
+
+		// Add deployment to state
+		state.Deployments().AddOrUpdate(
+			config.Deployment{
+				Network: "emulator",
+				Account: "emulator-account",
+				Contracts: []config.ContractDeployment{
+					{
+						Name: testContractDeployed.Name,
+					},
+				},
+			},
+		)
+
+		account, err := state.EmulatorServiceAccount()
+		assert.NoError(t, err)
+
+		contracts, err := resolveStagedContracts(
+			state,
+			[]string{testContractAliased.Name, testContractDeployed.Name},
+		)
+		assert.NoError(t, err)
+
+		assert.Equal(t, []migrations.StagedContract{
+			{
+				Contract: migrations.Contract{
+					Name: testContractAliased.Name,
+					Code: testContractAliased.Source,
+				},
+				Address: common.Address(flow.HexToAddress("0x1")),
+			},
+			{
+				Contract: migrations.Contract{
+					Name: testContractDeployed.Name,
+					Code: testContractDeployed.Source,
+				},
+				Address: common.Address(account.Address),
+			},
+		}, contracts)
+	})
+}