Repository: onflow/flow-cli (go)

Issue:
Stage contracts through flow CLI
- Command: **`flow stage-contract <NAME> <CONTRACT_PATH> --network <NETWORK> --signer <HOST_ACCOUNT>`**
- Description: Stages the specified contract code for migration. If the contract is already staged, its code will be replaced.
- Output: Success message indicating the contract has been staged, with any relevant details (e.g., staging ID).

Pull request:
staging contract CLI
Closes #1387  #1386 #1397 #1398 #1399 

## Description

create function commands for
getStagedCode
isStaged
listStagedContracts
StageContract
UnstageContract

include unit tests for each to test working behavior

______

For contributor use:

- [x] 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/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6bc4f647..14d62a67 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -48,4 +48,6 @@ jobs:
       - uses: golangci/golangci-lint-action@v3.7.0
         with:
           version: v1.52.2
+          only-new-issues: true
+          skip-pkg-cache: true
           args: --timeout=3m
diff --git a/cmd/flow/main.go b/cmd/flow/main.go
index 9ccf802b..3dbf7809 100644
--- a/cmd/flow/main.go
+++ b/cmd/flow/main.go
@@ -37,6 +37,7 @@ import (
 	"github.com/onflow/flow-cli/internal/emulator"
 	"github.com/onflow/flow-cli/internal/events"
 	"github.com/onflow/flow-cli/internal/keys"
+	"github.com/onflow/flow-cli/internal/migrate"
 	"github.com/onflow/flow-cli/internal/project"
 	"github.com/onflow/flow-cli/internal/quick"
 	"github.com/onflow/flow-cli/internal/scripts"
@@ -120,6 +121,7 @@ func main() {
 	cmd.AddCommand(super.FlixCmd)
 	cmd.AddCommand(super.GenerateCommand)
 	cmd.AddCommand(dependencymanager.Cmd)
+	cmd.AddCommand(migrate.Cmd)
 
 	command.InitFlags(cmd)
 	cmd.AddGroup(&cobra.Group{
@@ -150,6 +152,10 @@ func main() {
 		ID:    "manager",
 		Title: "🔗 Dependency Manager",
 	})
+	cmd.AddGroup(&cobra.Group{
+		ID:    "migrate",
+		Title: "📦 Migration to 1.0",
+	})
 
 	cmd.SetUsageTemplate(command.UsageTemplate)
 
diff --git a/go.mod b/go.mod
index 29863ca1..4407c1a1 100644
--- a/go.mod
+++ b/go.mod
@@ -11,6 +11,7 @@ require (
 	github.com/onflow/cadence v1.0.0-M7
 	github.com/onflow/cadence-tools/languageserver v1.0.0-M2
 	github.com/onflow/cadence-tools/test v1.0.0-M3
+	github.com/onflow/contract-updater/lib/go/templates v1.0.1
 	github.com/onflow/fcl-dev-wallet v0.8.0-stable-cadence.1
 	github.com/onflow/flixkit-go v1.1.1-0.20240214222351-03b90f7d32ef
 	github.com/onflow/flow-core-contracts/lib/go/templates v0.15.2-0.20240206003101-928bf99024d7
diff --git a/go.sum b/go.sum
index 61e829ea..9fa6bc63 100644
--- a/go.sum
+++ b/go.sum
@@ -2055,6 +2055,8 @@ github.com/onflow/cadence-tools/lint v1.0.0-M4 h1:XeBX+/2TKbe04nyMjUbSD9aHcF5+H6
 github.com/onflow/cadence-tools/lint v1.0.0-M4/go.mod h1:vgKOF14/NwWktjsRKHlWEz8z/3uQu5nuqDzQOm+u2qY=
 github.com/onflow/cadence-tools/test v1.0.0-M3 h1:ZbKoMhXsvafJIOO/MVE7lV5INXfq2IL0Z+W2MsJnKwY=
 github.com/onflow/cadence-tools/test v1.0.0-M3/go.mod h1:Yk6OyiRMNseM6C13FpNUxTJ7GekYZMzdX5D2B10y4N8=
+github.com/onflow/contract-updater/lib/go/templates v1.0.1 h1:xPj898Y8OgLLbXH8+JeKVBV6J+nqPZjiLgGM3Abucto=
+github.com/onflow/contract-updater/lib/go/templates v1.0.1/go.mod h1:OXO6s0X7OW4Q6QTfAfnjoOmibEPgs0psOfMi+tPyzQE=
 github.com/onflow/crypto v0.24.9 h1:jYP1qdwid0qCineFzBFlxBchg710A7RuSWpTqxaOdog=
 github.com/onflow/crypto v0.24.9/go.mod h1:J/V7IEVaqjDajvF8K0B/SJPJDgAOP2G+LVLeb0hgmbg=
 github.com/onflow/fcl-dev-wallet v0.8.0-stable-cadence.1 h1:IqdUzdqFCSW0klWmA3J9c17ZyQTab9SWcWSLouX6o0Q=
diff --git a/internal/migrate/get_staged_code.go b/internal/migrate/get_staged_code.go
new file mode 100644
index 00000000..7ac0be17
--- /dev/null
+++ b/internal/migrate/get_staged_code.go
@@ -0,0 +1,83 @@
+/*
+ * 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 (
+	"context"
+	"fmt"
+
+	"github.com/onflow/cadence"
+	"github.com/onflow/flowkit/v2"
+	"github.com/onflow/flowkit/v2/output"
+	"github.com/spf13/cobra"
+
+	"github.com/onflow/contract-updater/lib/go/templates"
+
+	"github.com/onflow/flow-cli/internal/command"
+	"github.com/onflow/flow-cli/internal/scripts"
+)
+
+var getStagedCodeflags struct{}
+
+var getStagedCodeCommand = &command.Command{
+	Cmd: &cobra.Command{
+		Use:     "staged-code <CONTRACT_NAME>",
+		Short:   "returns back the staged code for a contract",
+		Example: `flow migrate staged-code HelloWorld`,
+		Args:    cobra.MinimumNArgs(1),
+	},
+	Flags: &getStagedCodeflags,
+	RunS:  getStagedCode,
+}
+
+func getStagedCode(
+	args []string,
+	globalFlags command.GlobalFlags,
+	_ output.Logger,
+	flow flowkit.Services,
+	state *flowkit.State,
+) (command.Result, error) {
+	contractName := args[0]
+
+	addr, err := getAddressByContractName(state, contractName, flow.Network())
+	if err != nil {
+		return nil, fmt.Errorf("error getting address by contract name: %w", err)
+	}
+
+	cName, err := cadence.NewString(contractName)
+	if err != nil {
+		return nil, fmt.Errorf("error creating cadence string: %w", err)
+	}
+
+	caddr := cadence.NewAddress(addr)
+
+	value, err := flow.ExecuteScript(
+		context.Background(),
+		flowkit.Script{
+			Code: templates.GenerateGetStagedContractCodeScript(MigrationContractStagingAddress(flow.Network().Name)),
+			Args: []cadence.Value{caddr, cName},
+		},
+		flowkit.LatestScriptQuery,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("error executing script: %w", err)
+	}
+
+	return scripts.NewScriptResult(value), nil
+}
diff --git a/internal/migrate/is_staged.go b/internal/migrate/is_staged.go
new file mode 100644
index 00000000..5dbecf2b
--- /dev/null
+++ b/internal/migrate/is_staged.go
@@ -0,0 +1,82 @@
+/*
+ * 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 (
+	"context"
+	"fmt"
+
+	"github.com/onflow/cadence"
+	"github.com/onflow/contract-updater/lib/go/templates"
+	"github.com/onflow/flowkit/v2"
+	"github.com/onflow/flowkit/v2/output"
+	"github.com/spf13/cobra"
+
+	"github.com/onflow/flow-cli/internal/command"
+	"github.com/onflow/flow-cli/internal/scripts"
+)
+
+var isStagedflags struct{}
+
+var IsStagedCommand = &command.Command{
+	Cmd: &cobra.Command{
+		Use:     "is-staged <CONTRACT_NAME>",
+		Short:   "checks to see if the contract is staged for migration",
+		Example: `flow migrate is-staged HelloWorld`,
+		Args:    cobra.MinimumNArgs(1),
+	},
+	Flags: &isStagedflags,
+	RunS:  isStaged,
+}
+
+func isStaged(
+	args []string,
+	globalFlags command.GlobalFlags,
+	_ output.Logger,
+	flow flowkit.Services,
+	state *flowkit.State,
+) (command.Result, error) {
+	contractName := args[0]
+
+	addr, err := getAddressByContractName(state, contractName, flow.Network())
+	if err != nil {
+		return nil, fmt.Errorf("error getting address by contract name: %w", err)
+	}
+
+	caddr := cadence.NewAddress(addr)
+
+	cname, err := cadence.NewString(contractName)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get cadence string from contract name: %w", err)
+	}
+
+	value, err := flow.ExecuteScript(
+		context.Background(),
+		flowkit.Script{
+			Code: templates.GenerateIsStagedScript(MigrationContractStagingAddress(flow.Network().Name)),
+			Args: []cadence.Value{caddr, cname},
+		},
+		flowkit.LatestScriptQuery,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("error executing script: %w", err)
+	}
+
+	return scripts.NewScriptResult(value), nil
+}
diff --git a/internal/migrate/list_staged_contracts.go b/internal/migrate/list_staged_contracts.go
new file mode 100644
index 00000000..55bd7881
--- /dev/null
+++ b/internal/migrate/list_staged_contracts.go
@@ -0,0 +1,77 @@
+/*
+ * 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 (
+	"context"
+	"fmt"
+
+	"github.com/onflow/cadence"
+	"github.com/onflow/contract-updater/lib/go/templates"
+	"github.com/onflow/flowkit/v2"
+	"github.com/onflow/flowkit/v2/output"
+	"github.com/spf13/cobra"
+
+	"github.com/onflow/flow-cli/internal/command"
+	"github.com/onflow/flow-cli/internal/scripts"
+)
+
+var listStagedContractsflags struct{}
+
+var listStagedContractsCommand = &command.Command{
+	Cmd: &cobra.Command{
+		Use:     "list-staged <ACCOUNT_NAME>",
+		Short:   "returns back the a list of staged contracts given an account name",
+		Example: `flow migrate list-staged test-account`,
+		Args:    cobra.MinimumNArgs(1),
+	},
+	Flags: &listStagedContractsflags,
+	RunS:  listStagedContracts,
+}
+
+func listStagedContracts(
+	args []string,
+	globalFlags command.GlobalFlags,
+	_ output.Logger,
+	flow flowkit.Services,
+	state *flowkit.State,
+) (command.Result, error) {
+	accountName := args[0]
+
+	account, err := state.Accounts().ByName(accountName)
+	if err != nil {
+		return nil, fmt.Errorf("error getting account by name: %w", err)
+	}
+
+	caddr := cadence.NewAddress(account.Address)
+
+	value, err := flow.ExecuteScript(
+		context.Background(),
+		flowkit.Script{
+			Code: templates.GenerateGetStagedContractNamesForAddressScript(MigrationContractStagingAddress(flow.Network().Name)),
+			Args: []cadence.Value{caddr},
+		},
+		flowkit.LatestScriptQuery,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("error executing script: %w", err)
+	}
+
+	return scripts.NewScriptResult(value), nil
+}
diff --git a/internal/migrate/migrate.go b/internal/migrate/migrate.go
new file mode 100644
index 00000000..fa3265a7
--- /dev/null
+++ b/internal/migrate/migrate.go
@@ -0,0 +1,132 @@
+/*
+ * 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"
+
+	"github.com/onflow/cadence"
+	"github.com/onflow/flow-go-sdk"
+	"github.com/onflow/flowkit/v2"
+	"github.com/onflow/flowkit/v2/accounts"
+	"github.com/onflow/flowkit/v2/config"
+	"github.com/onflow/flowkit/v2/project"
+	"github.com/spf13/cobra"
+)
+
+func init() {
+	getStagedCodeCommand.AddToParent(Cmd)
+	IsStagedCommand.AddToParent(Cmd)
+	listStagedContractsCommand.AddToParent(Cmd)
+	stageContractCommand.AddToParent(Cmd)
+	unstageContractCommand.AddToParent(Cmd)
+}
+
+var Cmd = &cobra.Command{
+	Use:              "migrate",
+	Short:            "Migrate your Cadence project to 1.0",
+	TraverseChildren: true,
+	GroupID:          "migrate",
+}
+
+// address of the migration contract on each network
+var migrationContractStagingAddress = map[string]string{
+	"testnet":   "0x2ceae959ed1a7e7a",
+	"crescendo": "0x27b2302520211b67",
+}
+
+// MigrationContractStagingAddress returns the address of the migration contract on the given network
+func MigrationContractStagingAddress(network string) flow.Address {
+	return flow.HexToAddress(migrationContractStagingAddress[network])
+}
+
+// replaceImportsIfExists replaces imports in the given contract file with the actual contract code
+func replaceImportsIfExists(state *flowkit.State, flow flowkit.Services, location string) ([]byte, error) {
+	code, err := state.ReadFile(location)
+	if err != nil {
+		return nil, fmt.Errorf("error loading contract file: %w", err)
+	}
+
+	contracts, err := state.DeploymentContractsByNetwork(flow.Network())
+	if err != nil {
+		return nil, err
+	}
+
+	importReplacer := project.NewImportReplacer(
+		contracts,
+		state.AliasesForNetwork(flow.Network()),
+	)
+
+	program, err := project.NewProgram(code, []cadence.Value{}, location)
+	if err != nil {
+		return nil, err
+	}
+
+	if program.HasImports() {
+		program, err = importReplacer.Replace(program)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return program.Code(), nil
+}
+
+func getAccountByContractName(state *flowkit.State, contractName string, network config.Network) (*accounts.Account, error) {
+	deployments := state.Deployments().ByNetwork(network.Name)
+	var accountName string
+	for _, d := range deployments {
+		for _, c := range d.Contracts {
+			if c.Name == contractName {
+				accountName = d.Account
+				break
+			}
+		}
+	}
+	if accountName == "" {
+		return nil, fmt.Errorf("contract not found in state")
+	}
+
+	accs := state.Accounts()
+	if accs == nil {
+		return nil, fmt.Errorf("no accounts found in state")
+	}
+
+	var account *accounts.Account
+	for _, a := range *accs {
+		if accountName == a.Name {
+			account = &a
+			break
+		}
+	}
+	if account == nil {
+		return nil, fmt.Errorf("account %s not found in state", accountName)
+	}
+
+	return account, nil
+}
+
+func getAddressByContractName(state *flowkit.State, contractName string, network config.Network) (flow.Address, error) {
+	account, err := getAccountByContractName(state, contractName, network)
+	if err != nil {
+		return flow.Address{}, err
+	}
+
+	return flow.HexToAddress(account.Address.Hex()), nil
+}
diff --git a/internal/migrate/stage_contract.go b/internal/migrate/stage_contract.go
new file mode 100644
index 00000000..e9b2bed1
--- /dev/null
+++ b/internal/migrate/stage_contract.go
@@ -0,0 +1,100 @@
+/*
+ * 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 (
+	"context"
+	"fmt"
+
+	"github.com/onflow/cadence"
+	"github.com/onflow/contract-updater/lib/go/templates"
+	flowsdk "github.com/onflow/flow-go-sdk"
+	"github.com/onflow/flowkit/v2"
+	"github.com/onflow/flowkit/v2/output"
+	"github.com/onflow/flowkit/v2/transactions"
+	"github.com/spf13/cobra"
+
+	internaltx "github.com/onflow/flow-cli/internal/transactions"
+
+	"github.com/onflow/flow-cli/internal/command"
+)
+
+var stageContractflags struct{}
+
+var stageContractCommand = &command.Command{
+	Cmd: &cobra.Command{
+		Use:     "stage-contract <CONTRACT_NAME>",
+		Short:   "stage a contract for migration",
+		Example: `flow migrate stage-contract HelloWorld`,
+		Args:    cobra.MinimumNArgs(1),
+	},
+	Flags: &stageContractflags,
+	RunS:  stageContract,
+}
+
+func stageContract(
+	args []string,
+	globalFlags command.GlobalFlags,
+	_ output.Logger,
+	flow flowkit.Services,
+	state *flowkit.State,
+) (command.Result, error) {
+	contractName := args[0]
+
+	contract, err := state.Contracts().ByName(contractName)
+	if err != nil {
+		return nil, fmt.Errorf("no contracts found in state")
+	}
+
+	replacedCode, err := replaceImportsIfExists(state, flow, contract.Location)
+	if err != nil {
+		return nil, fmt.Errorf("failed to replace imports: %w", err)
+	}
+
+	cName, err := cadence.NewString(contractName)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get cadence string from contract name: %w", err)
+	}
+
+	cCode, err := cadence.NewString(string(replacedCode))
+	if err != nil {
+		return nil, fmt.Errorf("failed to get cadence string from contract code: %w", err)
+	}
+
+	account, err := getAccountByContractName(state, contractName, flow.Network())
+	if err != nil {
+		return nil, fmt.Errorf("failed to get account by contract name: %w", err)
+	}
+
+	tx, res, err := flow.SendTransaction(
+		context.Background(),
+		transactions.SingleAccountRole(*account),
+		flowkit.Script{
+			Code: templates.GenerateStageContractScript(MigrationContractStagingAddress(flow.Network().Name)),
+			Args: []cadence.Value{cName, cCode},
+		},
+		flowsdk.DefaultTransactionGasLimit,
+	)
+
+	if err != nil {
+		return nil, fmt.Errorf("failed to send transaction: %w", err)
+	}
+
+	return internaltx.NewTransactionResult(tx, res), nil
+}
diff --git a/internal/migrate/unstage_contract.go b/internal/migrate/unstage_contract.go
new file mode 100644
index 00000000..2de4d8f9
--- /dev/null
+++ b/internal/migrate/unstage_contract.go
@@ -0,0 +1,84 @@
+/*
+ * 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 (
+	"context"
+	"fmt"
+
+	"github.com/onflow/cadence"
+	"github.com/onflow/contract-updater/lib/go/templates"
+	flowsdk "github.com/onflow/flow-go-sdk"
+	"github.com/onflow/flowkit/v2"
+	"github.com/onflow/flowkit/v2/output"
+	"github.com/onflow/flowkit/v2/transactions"
+	"github.com/spf13/cobra"
+
+	"github.com/onflow/flow-cli/internal/command"
+	internaltx "github.com/onflow/flow-cli/internal/transactions"
+)
+
+var unstageContractflags struct{}
+
+var unstageContractCommand = &command.Command{
+	Cmd: &cobra.Command{
+		Use:     "unstage-contract <CONTRACT_NAME>",
+		Short:   "unstage a contract for migration",
+		Example: `flow migrate unstage-contract HelloWorld`,
+		Args:    cobra.MinimumNArgs(1),
+	},
+	Flags: &unstageContractflags,
+	RunS:  unstageContract,
+}
+
+func unstageContract(
+	args []string,
+	globalFlags command.GlobalFlags,
+	_ output.Logger,
+	flow flowkit.Services,
+	state *flowkit.State,
+) (command.Result, error) {
+	contractName := args[0]
+
+	account, err := getAccountByContractName(state, contractName, flow.Network())
+	if err != nil {
+		return nil, fmt.Errorf("failed to get account by contract name: %w", err)
+	}
+
+	cName, err := cadence.NewString(contractName)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get cadence string from contract name: %w", err)
+	}
+
+	tx, res, err := flow.SendTransaction(
+		context.Background(),
+		transactions.SingleAccountRole(*account),
+		flowkit.Script{
+			Code: templates.GenerateUnstageContractScript(MigrationContractStagingAddress(flow.Network().Name)),
+			Args: []cadence.Value{cName},
+		},
+		flowsdk.DefaultTransactionGasLimit,
+	)
+
+	if err != nil {
+		return nil, fmt.Errorf("failed to send transaction: %w", err)
+	}
+
+	return internaltx.NewTransactionResult(tx, res), nil
+}
diff --git a/internal/scripts/scripts.go b/internal/scripts/scripts.go
index b074d28e..d6c54771 100644
--- a/internal/scripts/scripts.go
+++ b/internal/scripts/scripts.go
@@ -45,6 +45,10 @@ type scriptResult struct {
 	cadence.Value
 }
 
+func NewScriptResult(value cadence.Value) *scriptResult {
+	return &scriptResult{Value: value}
+}
+
 func (r *scriptResult) JSON() any {
 	return json.RawMessage(
 		jsoncdc.MustEncode(r.Value),
diff --git a/internal/transactions/transactions.go b/internal/transactions/transactions.go
index 556cab20..8046ba29 100644
--- a/internal/transactions/transactions.go
+++ b/internal/transactions/transactions.go
@@ -57,6 +57,13 @@ type transactionResult struct {
 	exclude []string
 }
 
+func NewTransactionResult(tx *flow.Transaction, result *flow.TransactionResult) *transactionResult {
+	return &transactionResult{
+		result: result,
+		tx:     tx,
+	}
+}
+
 func (r *transactionResult) JSON() any {
 	result := make(map[string]any)
 	result["id"] = r.tx.ID().String()


Test patch:
diff --git a/internal/migrate/get_staged_code_test.go b/internal/migrate/get_staged_code_test.go
new file mode 100644
index 00000000..a80f7fd9
--- /dev/null
+++ b/internal/migrate/get_staged_code_test.go
@@ -0,0 +1,96 @@
+/*
+ * 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"
+	"github.com/onflow/contract-updater/lib/go/templates"
+	"github.com/onflow/flowkit/v2"
+	"github.com/onflow/flowkit/v2/config"
+	"github.com/onflow/flowkit/v2/tests"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+
+	"github.com/onflow/flow-cli/internal/command"
+	"github.com/onflow/flow-cli/internal/util"
+)
+
+func Test_GetStagedCode(t *testing.T) {
+	srv, state, _ := util.TestMocks(t)
+
+	t.Run("Success", func(t *testing.T) {
+		testContract := tests.ContractSimple
+
+		// Add contract to state
+		state.Contracts().AddOrUpdate(
+			config.Contract{
+				Name:     testContract.Name,
+				Location: testContract.Filename,
+			},
+		)
+
+		// Add deployment to state
+		state.Deployments().AddOrUpdate(
+			config.Deployment{
+				Network: "testnet",
+				Account: "emulator-account",
+				Contracts: []config.ContractDeployment{
+					{
+						Name: testContract.Name,
+					},
+				},
+			},
+		)
+		account, err := state.EmulatorServiceAccount()
+		assert.NoError(t, err)
+
+		srv.Network.Return(config.Network{
+			Name: "testnet",
+		}, nil)
+
+		srv.ExecuteScript.Run(func(args mock.Arguments) {
+			script := args.Get(1).(flowkit.Script)
+
+			actualContractAddressArg, actualContractNameArg := script.Args[0], script.Args[1]
+
+			assert.Equal(t, templates.GenerateGetStagedContractCodeScript(MigrationContractStagingAddress("testnet")), script.Code)
+
+			assert.Equal(t, 2, len(script.Args))
+
+			assert.EqualValues(t, testContract.Name, actualContractNameArg)
+
+			contractAddr := cadence.NewAddress(account.Address)
+			assert.Equal(t, contractAddr.String(), actualContractAddressArg.String())
+		}).Return(cadence.NewString(string(testContract.Source)))
+
+		result, err := getStagedCode(
+			[]string{testContract.Name},
+			command.GlobalFlags{
+				Network: "testnet",
+			},
+			util.NoLogger,
+			srv.Mock,
+			state,
+		)
+		assert.NoError(t, err)
+		assert.NotNil(t, result)
+	})
+}
diff --git a/internal/migrate/is_staged_test.go b/internal/migrate/is_staged_test.go
new file mode 100644
index 00000000..e85b09ba
--- /dev/null
+++ b/internal/migrate/is_staged_test.go
@@ -0,0 +1,97 @@
+/*
+ * 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"
+	"github.com/onflow/contract-updater/lib/go/templates"
+	"github.com/onflow/flowkit/v2"
+	"github.com/onflow/flowkit/v2/config"
+	"github.com/onflow/flowkit/v2/tests"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+
+	"github.com/onflow/flow-cli/internal/command"
+	"github.com/onflow/flow-cli/internal/util"
+)
+
+func Test_IsStaged(t *testing.T) {
+	srv, state, _ := util.TestMocks(t)
+
+	testContract := tests.ContractSimple
+
+	t.Run("Success", func(t *testing.T) {
+
+		state.Contracts().AddOrUpdate(
+			config.Contract{
+				Name:     testContract.Name,
+				Location: testContract.Filename,
+			},
+		)
+
+		// Add deployment to state
+		state.Deployments().AddOrUpdate(
+			config.Deployment{
+				Network: "testnet",
+				Account: "emulator-account",
+				Contracts: []config.ContractDeployment{
+					{
+						Name: testContract.Name,
+					},
+				},
+			},
+		)
+
+		srv.Network.Return(config.Network{
+			Name: "testnet",
+		}, nil)
+
+		account, err := state.EmulatorServiceAccount()
+		assert.NoError(t, err)
+
+		srv.ExecuteScript.Run(func(args mock.Arguments) {
+			script := args.Get(1).(flowkit.Script)
+
+			assert.Equal(t, templates.GenerateIsStagedScript(MigrationContractStagingAddress("testnet")), script.Code)
+
+			assert.Equal(t, 2, len(script.Args))
+			actualContractAddressArg, actualContractNameArg := script.Args[0], script.Args[1]
+
+			contractName, _ := cadence.NewString(testContract.Name)
+			contractAddr := cadence.NewAddress(account.Address)
+			assert.Equal(t, contractName, actualContractNameArg)
+			assert.Equal(t, contractAddr, actualContractAddressArg)
+		}).Return(cadence.NewBool(true), nil)
+
+		result, err := isStaged(
+			[]string{testContract.Name},
+			command.GlobalFlags{
+				Network: "testnet",
+			},
+			util.NoLogger,
+			srv.Mock,
+			state,
+		)
+		assert.NoError(t, err)
+		// TODO: fix this
+		assert.NotNil(t, result)
+	})
+}
diff --git a/internal/migrate/list_staged_contracts_test.go b/internal/migrate/list_staged_contracts_test.go
new file mode 100644
index 00000000..5100f40f
--- /dev/null
+++ b/internal/migrate/list_staged_contracts_test.go
@@ -0,0 +1,73 @@
+/*
+ * 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"
+	"github.com/onflow/contract-updater/lib/go/templates"
+	"github.com/onflow/flowkit/v2"
+	"github.com/onflow/flowkit/v2/config"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+
+	"github.com/onflow/flow-cli/internal/command"
+	"github.com/onflow/flow-cli/internal/util"
+)
+
+func Test_ListStagedContracts(t *testing.T) {
+	srv, state, _ := util.TestMocks(t)
+
+	t.Run("Success", func(t *testing.T) {
+
+		account, err := state.EmulatorServiceAccount()
+		assert.NoError(t, err)
+
+		srv.Network.Return(config.Network{
+			Name: "testnet",
+		}, nil)
+
+		srv.ExecuteScript.Run(func(args mock.Arguments) {
+			script := args.Get(1).(flowkit.Script)
+
+			assert.Equal(t, templates.GenerateGetStagedContractNamesForAddressScript(MigrationContractStagingAddress("testnet")), script.Code)
+			assert.Equal(t, 1, len(script.Args))
+
+			actualContractAddressArg := script.Args[0]
+
+			contractAddr := cadence.NewAddress(account.Address)
+			assert.Equal(t, contractAddr, actualContractAddressArg)
+		}).Return(cadence.NewArray([]cadence.Value{
+			cadence.String("some_staged_contract_code"),
+		}), nil)
+
+		result, err := listStagedContracts(
+			[]string{"emulator-account"},
+			command.GlobalFlags{
+				Network: "testnet",
+			},
+			util.NoLogger,
+			srv.Mock,
+			state,
+		)
+		assert.NoError(t, err)
+		assert.NotNil(t, result)
+	})
+}
diff --git a/internal/migrate/stage_contract_test.go b/internal/migrate/stage_contract_test.go
new file mode 100644
index 00000000..9881ad0c
--- /dev/null
+++ b/internal/migrate/stage_contract_test.go
@@ -0,0 +1,118 @@
+/*
+ * 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"
+	"github.com/onflow/contract-updater/lib/go/templates"
+	"github.com/onflow/flow-go-sdk"
+	"github.com/onflow/flowkit/v2"
+	"github.com/onflow/flowkit/v2/config"
+	"github.com/onflow/flowkit/v2/tests"
+	"github.com/onflow/flowkit/v2/transactions"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+
+	"github.com/onflow/flow-cli/internal/command"
+	"github.com/onflow/flow-cli/internal/util"
+)
+
+func Test_StageContract(t *testing.T) {
+	testContract := tests.ContractSimple
+
+	t.Run("Success", func(t *testing.T) {
+		srv, state, _ := util.TestMocks(t)
+
+		// Add contract to state
+		state.Contracts().AddOrUpdate(
+			config.Contract{
+				Name:     testContract.Name,
+				Location: testContract.Filename,
+			},
+		)
+
+		// Add deployment to state
+		state.Deployments().AddOrUpdate(
+			config.Deployment{
+				Network: "testnet",
+				Account: "emulator-account",
+				Contracts: []config.ContractDeployment{
+					{
+						Name: testContract.Name,
+					},
+				},
+			},
+		)
+
+		srv.Network.Return(config.Network{
+			Name: "testnet",
+		}, nil)
+
+		srv.SendTransaction.Run(func(args mock.Arguments) {
+			accountRoles := args.Get(1).(transactions.AccountRoles)
+			script := args.Get(2).(flowkit.Script)
+
+			assert.Equal(t, templates.GenerateStageContractScript(MigrationContractStagingAddress("testnet")), script.Code)
+
+			assert.Equal(t, 1, len(accountRoles.Signers()))
+			assert.Equal(t, "emulator-account", accountRoles.Signers()[0].Name)
+			assert.Equal(t, 2, len(script.Args))
+
+			actualContractNameArg, actualContractCodeArg := script.Args[0], script.Args[1]
+
+			contractName, _ := cadence.NewString(testContract.Name)
+			contractBody, _ := cadence.NewString(string(testContract.Source))
+			assert.Equal(t, contractName, actualContractNameArg)
+			assert.Equal(t, contractBody, actualContractCodeArg)
+		}).Return(flow.NewTransaction(), &flow.TransactionResult{
+			Status:      flow.TransactionStatusSealed,
+			Error:       nil,
+			BlockHeight: 1,
+		}, nil)
+
+		result, err := stageContract(
+			[]string{testContract.Name},
+			command.GlobalFlags{
+				Network: "testnet",
+			},
+			util.NoLogger,
+			srv.Mock,
+			state,
+		)
+		assert.NoError(t, err)
+		assert.NotNil(t, result)
+	})
+
+	t.Run("missing contract", func(t *testing.T) {
+		srv, state, _ := util.TestMocks(t)
+		result, err := stageContract(
+			[]string{testContract.Name},
+			command.GlobalFlags{
+				Network: "testnet",
+			},
+			util.NoLogger,
+			srv.Mock,
+			state,
+		)
+		assert.Error(t, err)
+		assert.Nil(t, result)
+	})
+}
diff --git a/internal/migrate/unstage_contract_test.go b/internal/migrate/unstage_contract_test.go
new file mode 100644
index 00000000..846947af
--- /dev/null
+++ b/internal/migrate/unstage_contract_test.go
@@ -0,0 +1,116 @@
+/*
+ * 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"
+	"github.com/onflow/contract-updater/lib/go/templates"
+	"github.com/onflow/flow-go-sdk"
+	"github.com/onflow/flowkit/v2"
+	"github.com/onflow/flowkit/v2/config"
+	"github.com/onflow/flowkit/v2/tests"
+	"github.com/onflow/flowkit/v2/transactions"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+
+	"github.com/onflow/flow-cli/internal/command"
+	"github.com/onflow/flow-cli/internal/util"
+)
+
+func Test_UnstageContract(t *testing.T) {
+	testContract := tests.ContractSimple
+
+	t.Run("Success", func(t *testing.T) {
+		srv, state, _ := util.TestMocks(t)
+
+		// Add contract to state
+		state.Contracts().AddOrUpdate(
+			config.Contract{
+				Name:     testContract.Name,
+				Location: testContract.Filename,
+			},
+		)
+
+		// Add deployment to state
+		state.Deployments().AddOrUpdate(
+			config.Deployment{
+				Network: "testnet",
+				Account: "emulator-account",
+				Contracts: []config.ContractDeployment{
+					{
+						Name: testContract.Name,
+					},
+				},
+			},
+		)
+
+		srv.Network.Return(config.Network{
+			Name: "testnet",
+		}, nil)
+
+		srv.SendTransaction.Run(func(args mock.Arguments) {
+			accountRoles := args.Get(1).(transactions.AccountRoles)
+			script := args.Get(2).(flowkit.Script)
+
+			assert.Equal(t, templates.GenerateUnstageContractScript(MigrationContractStagingAddress("testnet")), script.Code)
+
+			assert.Equal(t, 1, len(accountRoles.Signers()))
+			assert.Equal(t, "emulator-account", accountRoles.Signers()[0].Name)
+			assert.Equal(t, 1, len(script.Args))
+
+			actualContractNameArg := script.Args[0]
+
+			contractName, _ := cadence.NewString(testContract.Name)
+			assert.Equal(t, contractName, actualContractNameArg)
+		}).Return(flow.NewTransaction(), &flow.TransactionResult{
+			Status:      flow.TransactionStatusSealed,
+			Error:       nil,
+			BlockHeight: 1,
+		}, nil)
+
+		result, err := unstageContract(
+			[]string{testContract.Name},
+			command.GlobalFlags{
+				Network: "testnet",
+			},
+			util.NoLogger,
+			srv.Mock,
+			state,
+		)
+		assert.NoError(t, err)
+		assert.NotNil(t, result)
+	})
+
+	t.Run("missing contract file", func(t *testing.T) {
+		srv, state, _ := util.TestMocks(t)
+		result, err := stageContract(
+			[]string{testContract.Name},
+			command.GlobalFlags{
+				Network: "testnet",
+			},
+			util.NoLogger,
+			srv.Mock,
+			state,
+		)
+		assert.Error(t, err)
+		assert.Nil(t, result)
+	})
+}