diff --git a/go.mod b/go.mod index 37679987a..a7e66f9e2 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 bbc90c261..7bfe19861 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 fa3265a76..20e0b0d86 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 000000000..e444ca5cb --- /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 +} diff --git a/internal/migrate/state_test.go b/internal/migrate/state_test.go new file mode 100644 index 000000000..1377aef19 --- /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) + }) +}