Repository: pokt-network/poktroll
Pull request #1153: [Morse -> Shannon Migration] CLI: claim Morse supplier
Author: login: bryanchriswhite
Labels: name: supplier
description: Changes related to the Supplier actor
name: on-chain
description: On-chain business logic
name: push-image
description: CI related - pushes images to ghcr.io
name: tooling
description: Tooling - CLI, scripts, helpers, off-chain, etc...
name: devnet
name: devnet-test-e2e
name: consensus-breaking
description: IMPORTANT! If the PR with this tag is merged, next release WILL HAVE TO BE an upgrade.
name: migration
description: Morse to Shannon migration related work
Created: 2025-03-25T14:06:54Z | Merged: 2025-04-04T10:44:04Z

## Summary

Implement Morse supplier claiming CLI.

Changes:
- Implement `pocketd tx migration claim-supplier ...`
- ~~Refactor existing supplier stake config parsing logic~~ 
  - Extracted to #1163 
- Add E2E test coverage
- Update E2E param helpers

## Issues

- #1034

## Type of change

Select one or more from the following:

- [x] New feature, functionality or library
- [ ] Consensus breaking; add the `consensus-breaking` label if so. See #791 for details
- [ ] Bug fix
- [ ] Code health or cleanup
- [ ] Documentation
- [ ] Other (specify)

## Sanity Checklist

- [x] I have updated the GitHub Issue `assignees`, `reviewers`, `labels`, `project`, `iteration` and `milestone`
- [ ] For docs, I have run `make docusaurus_start`
- [x] For code, I have run `make go_develop_and_test` and `make test_e2e`
- [x] For code, I have added the `devnet-test-e2e` label to run E2E tests in CI
- [ ] For configurations, I have update the documentation
- [ ] I added TODOs where applicable



Diff:
diff --git a/api/pocket/application/types.pulsar.go b/api/pocket/application/types.pulsar.go
index 04e26ada1d..7d6ea5587e 100644
--- a/api/pocket/application/types.pulsar.go
+++ b/api/pocket/application/types.pulsar.go
@@ -2199,7 +2199,7 @@ type Application struct {
 	//   - https://github.com/pokt-network/poktroll/pull/750#discussion_r1735025033
 	//   - https://www.notion.so/buildwithgrove/Off-chain-Application-Stake-Tracking-6a8bebb107db4f7f9dc62cbe7ba555f7
 	ServiceConfigs []*shared.ApplicationServiceConfig `protobuf:"bytes,3,rep,name=service_configs,json=serviceConfigs,proto3" json:"service_configs,omitempty"`
-	// TODO_BETA(@bryanchriswhite): Rename `delegatee_gateway_addresses` to `gateway_addresses_delegated_to`.
+	// TODO_MAINNET_MIGRATION(@bryanchriswhite): Rename `delegatee_gateway_addresses` to `gateway_addresses_delegated_to`.
 	// Ensure to rename all relevant configs, comments, variables, function names, etc as well.
 	// Non-nullable list of Bech32 encoded delegatee Gateway addresses
 	DelegateeGatewayAddresses []string `protobuf:"bytes,4,rep,name=delegatee_gateway_addresses,json=delegateeGatewayAddresses,proto3" json:"delegatee_gateway_addresses,omitempty"`
diff --git a/api/pocket/migration/morse_offchain.pulsar.go b/api/pocket/migration/morse_offchain.pulsar.go
index 0ffd8f7a9e..04fb591393 100644
--- a/api/pocket/migration/morse_offchain.pulsar.go
+++ b/api/pocket/migration/morse_offchain.pulsar.go
@@ -5774,7 +5774,7 @@ type MorseApplication struct {
 	Address []byte `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
 	// PublicKey is the binary representation of a Morse application's ed25519 public key.
 	PublicKey []byte `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
-	// TODO_MAINNET(@Olshansk):  Should status and/or jailed be considered during the migration, and if so, how?
+	// TODO_MAINNET_MIGRATION(@Olshansk):  Should status and/or jailed be considered during the migration, and if so, how?
 	Jailed bool  `protobuf:"varint,3,opt,name=jailed,proto3" json:"jailed,omitempty"`
 	Status int32 `protobuf:"varint,4,opt,name=status,proto3" json:"status,omitempty"`
 	// The string representation of the BigInt amount of upokt.
@@ -5849,7 +5849,7 @@ type MorseValidator struct {
 	Address []byte `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
 	// The binary representation of a Morse application's ed25519 public key.
 	PublicKey []byte `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
-	// TODO_MAINNET(@Olshansk):  Should status and/or jailed be considered during the migration, and if so, how?
+	// TODO_MAINNET_MIGRATION(@Olshansk):  Should status and/or jailed be considered during the migration, and if so, how?
 	Jailed bool  `protobuf:"varint,3,opt,name=jailed,proto3" json:"jailed,omitempty"`
 	Status int32 `protobuf:"varint,4,opt,name=status,proto3" json:"status,omitempty"`
 	// The string representation of the BigInt amount of upokt.
diff --git a/api/pocket/proof/params.pulsar.go b/api/pocket/proof/params.pulsar.go
index b82ffa420e..9b01a23594 100644
--- a/api/pocket/proof/params.pulsar.go
+++ b/api/pocket/proof/params.pulsar.go
@@ -678,18 +678,18 @@ type Params struct {
 	// is equal to or above the threshold. This is in contrast to the this requirement
 	// being determined probabilistically via ProofRequestProbability.
 	//
-	// TODO_MAINNET: Consider renaming this to `proof_requirement_threshold_upokt`.
+	// TODO_MAINNET_MIGRATION: Consider renaming this to `proof_requirement_threshold_upokt`.
 	ProofRequirementThreshold *v1beta1.Coin `protobuf:"bytes,3,opt,name=proof_requirement_threshold,json=proofRequirementThreshold,proto3" json:"proof_requirement_threshold,omitempty"`
 	// proof_missing_penalty is the number of tokens (uPOKT) which should be slashed from a supplier
 	// when a proof is required (either via proof_requirement_threshold or proof_missing_penalty)
 	// but is not provided.
-	// TODO_MAINNET: Consider renaming this to `proof_missing_penalty_upokt`.
+	// TODO_MAINNET_MIGRATION: Consider renaming this to `proof_missing_penalty_upokt`.
 	ProofMissingPenalty *v1beta1.Coin `protobuf:"bytes,4,opt,name=proof_missing_penalty,json=proofMissingPenalty,proto3" json:"proof_missing_penalty,omitempty"`
 	// proof_submission_fee is the number of tokens (uPOKT) which should be paid by
 	// the supplier operator when submitting a proof.
 	// This is needed to account for the cost of storing proofs onchain and prevent
 	// spamming (i.e. sybil bloat attacks) the network with non-required proofs.
-	// TODO_MAINNET: Consider renaming this to `proof_submission_fee_upokt`.
+	// TODO_MAINNET_MIGRATION: Consider renaming this to `proof_submission_fee_upokt`.
 	ProofSubmissionFee *v1beta1.Coin `protobuf:"bytes,5,opt,name=proof_submission_fee,json=proofSubmissionFee,proto3" json:"proof_submission_fee,omitempty"`
 }
 
diff --git a/api/pocket/shared/service.pulsar.go b/api/pocket/shared/service.pulsar.go
index ae061f2927..58bb1204a2 100644
--- a/api/pocket/shared/service.pulsar.go
+++ b/api/pocket/shared/service.pulsar.go
@@ -3396,7 +3396,7 @@ type Service struct {
 
 	// For example, what if we want to request a session for a certain service but with some additional configs that identify it?
 	Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // Unique identifier for the service
-	// TODO_BETA(@bryanchriswhite): Either remove this or rename it to alias.
+	// TODO_MAINNET_MIGRATION(@bryanchriswhite): Either remove this or rename it to alias.
 	Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // (Optional) Semantic human readable name for the service
 	// The cost of a single relay for this service in terms of compute units.
 	// Must be used alongside the global 'compute_units_to_tokens_multipler' to calculate the cost of a relay for this service.
diff --git a/e2e/tests/init_test.go b/e2e/tests/init_test.go
index c0fb59cc14..c5f4266fc9 100644
--- a/e2e/tests/init_test.go
+++ b/e2e/tests/init_test.go
@@ -26,7 +26,6 @@ import (
 	cometcli "github.com/cometbft/cometbft/libs/cli"
 	cometjson "github.com/cometbft/cometbft/libs/json"
 	"github.com/cosmos/cosmos-sdk/codec"
-	codectypes "github.com/cosmos/cosmos-sdk/codec/types"
 	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
 	"github.com/gorilla/websocket"
 	"github.com/regen-network/gocuke"
@@ -77,6 +76,33 @@ var (
 	allFeaturesTags = fmt.Sprintf("not %s and not %s", manualTag, oneshotTag)
 )
 
+type actorTypeEnum = string
+
+const (
+	actorTypeApp      actorTypeEnum = "application"
+	actorTypeSupplier actorTypeEnum = "supplier"
+)
+
+// cliAccountQueryResopnse is a data structure that matches the serialized account
+// which is returned when querying for onchain accounts via the CLI.
+type cliAccountQueryResponse struct {
+	Type  string `json:"type"`
+	Value struct {
+		Account authtypes.BaseAccount `json:"account"`
+	} `json:"value"`
+}
+
+// cliBlockQueryResponse is a data structure that matches the serialized block
+// which is returned when querying for the current block via the CLI.
+type cliBlockQueryResponse struct {
+	Header struct {
+		Height int64 `json:"height"`
+	} `json:"header"`
+	LastCommit struct {
+		Height int64 `json:"height"`
+	} `json:"last_commit"`
+}
+
 func init() {
 	addrRe = regexp.MustCompile(`address:\s+(\S+)\s+name:\s+(\S+)`)
 	amountRe = regexp.MustCompile(`amount:\s+"(.+?)"\s+denom:\s+upokt`)
@@ -338,6 +364,11 @@ func (s *suite) TheUserStakesAWithUpoktForServiceFromTheAccount(actorType string
 		chainIdFlag,
 		"-y",
 	}
+	switch actorType {
+	case actorTypeApp:
+		args = append(args)
+	}
+
 	res, err := s.pocketd.RunCommandOnHost("", args...)
 	require.NoError(s, err, "error staking %s for service %s due to: %v", actorType, serviceId, err)
 
@@ -928,8 +959,39 @@ func (s *suite) getSharedParams() sharedtypes.Params {
 	return sharedParamsRes.Params
 }
 
-// getCurrentBlockHeight returns the current block height
+// getSupplierParams returns the supplier module parameters
+func (s *suite) getSupplierParams() suppliertypes.Params {
+	args := []string{
+		"query",
+		"supplier",
+		"params",
+		"--output=json",
+	}
+	res, err := s.pocketd.RunCommandOnHost("", args...)
+	require.NoError(s, err, "error querying supplier params")
+
+	var supplierParamsRes suppliertypes.QueryParamsResponse
+
+	s.cdc.MustUnmarshalJSON([]byte(res.Stdout), &supplierParamsRes)
+	require.NoError(s, err)
+
+	return supplierParamsRes.Params
+}
+
+// getCurrentBlockHeight returns the current (uncommitted) block height.
 func (s *suite) getCurrentBlockHeight() int64 {
+	blockRes := s.queryBlockResponse()
+	return blockRes.Header.Height
+}
+
+// getLastCommitBlockHeight returns the block height of the most recently committed block.
+func (s *suite) getLastCommitBlockHeight() int64 {
+	blockRes := s.queryBlockResponse()
+	return blockRes.LastCommit.Height
+}
+
+// queryBlock queries for the current block information.
+func (s *suite) queryBlockResponse() cliBlockQueryResponse {
 	args := []string{
 		"query",
 		"block",
@@ -945,16 +1007,11 @@ func (s *suite) getCurrentBlockHeight() int64 {
 	require.Greater(s, len(stdoutLines), 1, "expected at least one line of output")
 	res.Stdout = strings.Join(stdoutLines[1:], "\n")
 
-	var blockRes struct {
-		Header struct {
-			Height int64 `json:"height"`
-		} `json:"header"`
-	}
-
+	var blockRes cliBlockQueryResponse
 	err = cometjson.Unmarshal([]byte(res.Stdout), &blockRes)
 	require.NoError(s, err)
 
-	return blockRes.Header.Height
+	return blockRes
 }
 
 // readEVMSubscriptionEvents reads the eth_subscription events from the websocket
@@ -991,7 +1048,7 @@ func (s *suite) readEVMSubscriptionEvents() context.Context {
 }
 
 // queryAccount queries the auth module for the account associated with the given address.
-func (s *suite) queryAccount(accAddr string) (account *codectypes.Any, isFound bool) {
+func (s *suite) queryAccount(accAddr string) (account *authtypes.BaseAccount, isFound bool) {
 	args := []string{
 		"query",
 		"auth",
@@ -1007,11 +1064,13 @@ func (s *suite) queryAccount(accAddr string) (account *codectypes.Any, isFound b
 	}
 	require.NoError(s, err, "error getting account for address %s", accAddr)
 
-	var resp authtypes.QueryAccountResponse
+	var resp cliAccountQueryResponse
 	responseBz := []byte(strings.TrimSpace(result.Stdout))
-	s.cdc.MustUnmarshalJSON(responseBz, &resp)
 
-	return resp.Account, true
+	err = cometjson.Unmarshal(responseBz, &resp)
+	require.NoError(s, err)
+
+	return &resp.Value.Account, true
 }
 
 // accBalanceKey is a helper function to create a key to store the balance
diff --git a/e2e/tests/migration_fixture.feature b/e2e/tests/migration_fixture.feature
index b1e765b80b..0d0c94d245 100644
--- a/e2e/tests/migration_fixture.feature
+++ b/e2e/tests/migration_fixture.feature
@@ -8,9 +8,19 @@ Feature: Morse account import and claim all account types (with fixture data)
 
   Background:
     Given the user has the pocketd binary installed
-    And a MorseAccountState with "20" accounts in a "round-robin" distribution has successfully been imported
+    And a MorseAccountState with "30" accounts in a "round-robin" distribution has successfully been imported
     And an unclaimed MorseClaimableAccount with a known private key exists
     And a Shannon destination key exists in the local keyring
+    And the "application" module parameters are set as follows
+      | name                   | value | type   |
+      | min_stake              | 100   | coin   |
+      | max_delegated_gateways | 7     | uint64 |
+    And all "application" module params should be updated
+    And the "supplier" module parameters are set as follows
+      | name        | value | type |
+      | min_stake   | 100   | coin |
+      | staking_fee | 1     | coin |
+    And all "supplier" module params should be updated
 
   Rule: Non-actor account claims MAY reference existing Shannon accounts
     Scenario: Morse account-holder claims as a new non-actor account
@@ -42,12 +52,11 @@ Feature: Morse account import and claim all account types (with fixture data)
       Examples:
         | actor       |
         | application |
-      # TODO_MAINNET_CRITICAL(@bryanchriswhite, #1034: Uncomment the following example once supplier Morse account claiming is available.
-      # | supplier    |
+        | supplier    |
 
     Scenario Outline: Morse account-holder claims as an existing staked actor
       Given the Shannon account is funded with "1234567upokt"
-      And the Shannon destination account is staked as an "<actor>" with "1234567" uPOKT for "anvil" service
+      And the Shannon destination account is staked as an "<actor>" with "1230000" uPOKT for "anvil" service
       When the Morse private key is used to claim a MorseClaimableAccount as an "<actor>" for "ollama" service
       Then the Shannon destination account balance is increased by the unstaked balance amount of the MorseClaimableAccount
       And the Shannon destination account is staked as an "<actor>"
@@ -57,8 +66,7 @@ Feature: Morse account import and claim all account types (with fixture data)
       Examples:
         | actor       |
         | application |
-      # TODO_MAINNET_CRITICAL(@bryanchriswhite, #1034: Uncomment the following example once supplier Morse account claiming is available.
-      # | supplier    |
+        | supplier    |
 
 # TODO_MAINNET_CRITICAL(@bryanchriswhite, #1034): Enumerate and implement error scenarios.
 # TODO_POST_MAINNET(@bryanchriswhite, #1034): Scenario: Morse account-holder claims with a stake below the minimum
diff --git a/e2e/tests/migration_steps_test.go b/e2e/tests/migration_steps_test.go
index 09cb1cff78..e1eb2ac28b 100644
--- a/e2e/tests/migration_steps_test.go
+++ b/e2e/tests/migration_steps_test.go
@@ -16,10 +16,13 @@ import (
 	cosmostypes "github.com/cosmos/cosmos-sdk/types"
 	"github.com/regen-network/gocuke"
 	"github.com/stretchr/testify/require"
+	"gopkg.in/yaml.v2"
 
 	"github.com/pokt-network/poktroll/app/volatile"
 	"github.com/pokt-network/poktroll/testutil/testmigration"
 	migrationtypes "github.com/pokt-network/poktroll/x/migration/types"
+	sharedtypes "github.com/pokt-network/poktroll/x/shared/types"
+	"github.com/pokt-network/poktroll/x/supplier/config"
 )
 
 const (
@@ -27,8 +30,9 @@ const (
 	// to determine whether it was unsuccessful, despite returning a zero exit code.
 	cmdUsagePattern = `--help" for more`
 
-	defaultMorseStateExportJSONFilename  = "morse_state_export.json"
-	defaultMorseAccountStateJSONFilename = "morse_account_state.json"
+	defaultMorseStateExportJSONFilename    = "morse_state_export.json"
+	defaultMorseAccountStateJSONFilename   = "morse_account_state.json"
+	defaultSupplierStakeConfigYAMLFileName = "supplier_stake_config.yaml"
 )
 
 var (
@@ -75,6 +79,10 @@ type migrationSuite struct {
 	// application staked MorseClaimableAccount in consideration.
 	appMorseClaimableAccount *migrationtypes.MorseClaimableAccount
 
+	// supplierMorseClaimableAccount is used to hold the query result for the current
+	// supplier staked MorseClaimableAccount in consideration.
+	supplierMorseClaimableAccount *migrationtypes.MorseClaimableAccount
+
 	// expectedMorseClaimableAccount is used to hold the query result for the
 	// current expected MorseClaimableAccount in consideration.
 	expectedMorseClaimableAccount *migrationtypes.MorseClaimableAccount
@@ -88,21 +96,24 @@ type migrationSuite struct {
 	// and populated in the Before() method, which is called before each test case.
 	previousUnstakedBalanceUpoktOfCurrentShannonIdx cosmostypes.Coin
 
-	// previousStakedApplicationUpokt is the upokt which is staked in scenarios where an account
-	// is already staked.
+	// previousStakedActorUpokt is the upokt amount if the actor was already staked in Morse.
 	// It is initialized in the Before() method and the updated in relevant subsequent steps.
-	previousStakedApplicationUpokt cosmostypes.Coin
+	previousStakedActorUpokt cosmostypes.Coin
 
 	// faucetFundedBalanceUpokt is the upokt balance that is transferred by the faucet during setup.
 	faucetFundedBalanceUpokt cosmostypes.Coin
-}
 
-type actorTypeEnum = string
+	// supplierStakingFeeIfApplicable is used to make arithmetic corrections to
+	// the account balance expectations when staking a supplier. This is necessary
+	// in order to account for the fee incurred by suppliers when staking.
+	supplierStakingFeeIfApplicable cosmostypes.Coin
 
-const (
-	actorTypeApp      actorTypeEnum = "application"
-	actorTypeSupplier actorTypeEnum = "supplier"
-)
+	// expectedSupplierEffectiveServiceHeight is used to indicate when it is safe
+	// to make assertions regarding the respective supplier's services state.
+	// This is necessary when claiming a supplier Morse account with an existing
+	// Shannon supplier account.
+	expectedSupplierEffectiveServiceHeight int64
+}
 
 var (
 	defaultMorseDataDir    = path.Join(".pocket", "data")
@@ -128,17 +139,16 @@ func (s *migrationSuite) Before() {
 	s.nextMorseKeyIdx()
 
 	// If the current Shannon key has an onchain balance, track it for later use in assertions.
-	s.previousUnstakedBalanceUpoktOfCurrentShannonIdx = cosmostypes.NewInt64Coin(volatile.DenomuPOKT, 0)
-	if shannonDestAddr, isFound := s.getShannonKeyAddress(); isFound {
-		if _, isFound = s.queryAccount(shannonDestAddr); isFound {
-			upoktBalanceInt := s.getAccBalance(s.getShannonKeyName())
-			upoktBalanceCoin := cosmostypes.NewInt64Coin(volatile.DenomuPOKT, int64(upoktBalanceInt))
-			s.previousUnstakedBalanceUpoktOfCurrentShannonIdx = upoktBalanceCoin
-		}
-	}
+	s.updatePreviousUnstakedBalanceUpoktOfCurrentShannonIdx()
 
 	// Initialize the previous actor stake here. It is updated in a relevant subsequent step.
-	s.previousStakedApplicationUpokt = cosmostypes.NewInt64Coin(volatile.DenomuPOKT, 0)
+	s.previousStakedActorUpokt = cosmostypes.NewInt64Coin(volatile.DenomuPOKT, 0)
+
+	// Initialize the fauced funded balance to zero upokt.
+	s.faucetFundedBalanceUpokt = cosmostypes.NewInt64Coin(volatile.DenomuPOKT, 0)
+
+	// Initialize the supplier staking fee to zero upokt, non-supplier for cases.
+	s.supplierStakingFeeIfApplicable = cosmostypes.NewInt64Coin(volatile.DenomuPOKT, 0)
 }
 
 // TestMigrationWithFixtureData runs the migration_fixture.feature file ONLY.
@@ -170,34 +180,57 @@ func (s *migrationSuite) ALocalMorseNodePersistedStateExists() {
 	}
 }
 
+// updatePreviousUnstakedBalanceUpoktOfCurrentShannonIdx queries for the current
+// balance of the current Shannon account and updates s.previousUnstakedBalanceUpoktOfCurrentShannonIdx
+// for later assertions.
+// It is updated:
+//   - In the Before(), at the beginning of the scenario
+//   - In the "shannon destination account is staked as an <actor> with <amount> upokt for <service_id> service step", after staking
+func (s *migrationSuite) updatePreviousUnstakedBalanceUpoktOfCurrentShannonIdx() {
+	s.previousUnstakedBalanceUpoktOfCurrentShannonIdx = cosmostypes.NewInt64Coin(volatile.DenomuPOKT, 0)
+	if shannonDestAddr, isFound := s.getShannonKeyAddress(); isFound {
+		if _, isFound = s.queryAccount(shannonDestAddr); isFound {
+			upoktBalanceInt := s.getAccBalance(s.getShannonKeyName())
+			upoktBalanceCoin := cosmostypes.NewInt64Coin(volatile.DenomuPOKT, int64(upoktBalanceInt))
+			s.previousUnstakedBalanceUpoktOfCurrentShannonIdx = upoktBalanceCoin
+		}
+	}
+}
+
 func (s *migrationSuite) NoMorseclaimableaccountsExist() {
 	morseClaimableAccounts := s.queryListMorseClaimableAccounts()
 	require.Lessf(s, len(morseClaimableAccounts), 1, "expected 0 morse claimable accounts, got %d", len(morseClaimableAccounts))
 }
 
 func (s *migrationSuite) TheShannonDestinationAccountIsStakedAsAnWithUpoktForService(actorType actorTypeEnum, upoktAmount int64, serviceId string) {
-	s.previousStakedApplicationUpokt = cosmostypes.NewInt64Coin(volatile.DenomuPOKT, upoktAmount)
+	s.previousStakedActorUpokt = cosmostypes.NewInt64Coin(volatile.DenomuPOKT, upoktAmount)
 
-	s.TheUserStakesAWithUpoktForServiceFromTheAccount(actorType, s.previousStakedApplicationUpokt.Amount.Int64(), serviceId, s.getShannonKeyName())
+	s.TheUserStakesAWithUpoktForServiceFromTheAccount(actorType, s.previousStakedActorUpokt.Amount.Int64(), serviceId, s.getShannonKeyName())
 	s.TheUserShouldBeAbleToSeeStandardOutputContaining("txhash:")
 	s.TheUserShouldBeAbleToSeeStandardOutputContaining("code: 0")
 	s.ThePocketdBinaryShouldExitWithoutError()
 	s.TheUserShouldWaitForSeconds(3)
 
-	s.TheForAccountIsStakedWithUpokt(actorType, s.getShannonKeyName(), s.previousStakedApplicationUpokt.Amount.Int64())
+	s.TheForAccountIsStakedWithUpokt(actorType, s.getShannonKeyName(), s.previousStakedActorUpokt.Amount.Int64())
+
+	s.updatePreviousUnstakedBalanceUpoktOfCurrentShannonIdx()
 }
 
 func (s *migrationSuite) TheShannonDestinationAccountIsStakedAsAn(actorType actorTypeEnum) {
+	var expectedActorStake cosmostypes.Coin
+
 	switch actorType {
 	case actorTypeApp:
-		expectedAppStake := s.appMorseClaimableAccount.GetApplicationStake().Add(s.previousStakedApplicationUpokt)
-		s.TheForAccountIsStakedWithUpokt(actorType, s.getShannonKeyName(), expectedAppStake.Amount.Int64())
-	//case actorTypeSupplier:
-	//  expectedSupplierStake := s.supplierMorseClaimableAccount.GetSupplierStake().Add(s.previousSupplierStakeOfCurrentShannonIdx)
-	//	s.TheForAccountIsStakedWithUpokt(actorType, s.getShannonKeyName(), s.supplierMorseClaimableAccount.GetSupplierStake().Amount.Int64())
+		expectedActorStake = s.expectedMorseClaimableAccount.GetApplicationStake().Add(s.previousStakedActorUpokt)
+	case actorTypeSupplier:
+		expectedActorStake = s.supplierMorseClaimableAccount.GetSupplierStake().Add(s.previousStakedActorUpokt)
+		sharedParams := s.getSharedParams()
+		s.expectedSupplierEffectiveServiceHeight = sharedtypes.GetNextSessionStartHeight(&sharedParams, s.getCurrentBlockHeight())
 	default:
 		s.Fatal("unexpected actor type %q", actorType)
 	}
+
+	s.TheForAccountIsStakedWithUpokt(actorType, s.getShannonKeyName(), expectedActorStake.Amount.Int64())
 }
 
 func (s *migrationSuite) TheShannonStakeIncreasedByTheCorrespondingActorStakeAmountOfTheMorseclaimableaccount(actorType actorTypeEnum) {
@@ -207,10 +240,12 @@ func (s *migrationSuite) TheShannonStakeIncreasedByTheCorrespondingActorStakeAmo
 	switch actorType {
 	case actorTypeApp:
 		currentActorStake := s.getApplicationInfo(s.getShannonKeyName()).GetStake()
-		*actorStakeDiff = currentActorStake.Sub(s.previousUnstakedBalanceUpoktOfCurrentShannonIdx)
-		*expectedStakeDiff = s.expectedMorseClaimableAccount.GetApplicationStake().
-			Add(s.previousStakedApplicationUpokt)
-	//case actorTypeSupplier:
+		*actorStakeDiff = currentActorStake.Sub(s.previousStakedActorUpokt)
+		*expectedStakeDiff = s.expectedMorseClaimableAccount.GetApplicationStake()
+	case actorTypeSupplier:
+		currentActorStake := s.getSupplierInfo(s.getShannonKeyName()).GetStake()
+		*actorStakeDiff = currentActorStake.Sub(s.previousStakedActorUpokt)
+		*expectedStakeDiff = s.expectedMorseClaimableAccount.GetSupplierStake()
 	default:
 		s.Fatal("unexpected actor type %q", actorType)
 	}
@@ -227,6 +262,13 @@ func (s *migrationSuite) TheMorsePrivateKeyIsUsedToClaimAMorseclaimableaccountAs
 		// Assign the expected claimable account for the current scenario.
 		s.expectedMorseClaimableAccount = s.appMorseClaimableAccount
 		morseKeyIdx = s.appAccountIdx
+	case actorTypeSupplier:
+		sharedParams := s.getSharedParams()
+		s.expectedSupplierEffectiveServiceHeight = sharedtypes.GetNextSessionStartHeight(&sharedParams, s.getCurrentBlockHeight())
+
+		s.expectedMorseClaimableAccount = s.supplierMorseClaimableAccount
+		s.supplierStakingFeeIfApplicable = *s.getSupplierParams().StakingFee
+		morseKeyIdx = s.supplierAccountIdx
 	default:
 		s.Fatal("unexpected actor type %q", actorType)
 	}
@@ -238,10 +280,8 @@ func (s *migrationSuite) TheMorsePrivateKeyIsUsedToClaimAMorseclaimableaccountAs
 	require.NoError(s, err)
 
 	privKeyArmoredJSONPath := s.writeTempFile("morse_private_key.json", []byte(privKeyArmoredJSONString))
-
-	// poktrolld tx migration claim-application --from=shannon-key-xxx <morse_src_address> <service_id>
-	res, err := s.pocketd.RunCommandOnHost("",
-		"tx", "migration", "claim-application",
+	commandStringParts := []string{
+		"tx", "migration", fmt.Sprintf("claim-%s", actorType),
 		"--from", s.getShannonKeyName(),
 		keyRingFlag,
 		chainIdFlag,
@@ -249,8 +289,17 @@ func (s *migrationSuite) TheMorsePrivateKeyIsUsedToClaimAMorseclaimableaccountAs
 		"--output=json",
 		"--no-passphrase",
 		privKeyArmoredJSONPath,
-		serviceId,
-	)
+	}
+
+	switch actorType {
+	case actorTypeApp:
+		commandStringParts = append(commandStringParts, serviceId)
+	case actorTypeSupplier:
+		supplierStakeConfigPath := s.writeTempSupplierStakeConfig(serviceId, "")
+		commandStringParts = append(commandStringParts, supplierStakeConfigPath)
+	}
+
+	res, err := s.pocketd.RunCommandOnHost("", commandStringParts...)
 	require.NoError(s, err)
 
 	// Track the height at which the morse claimable account was claimed.
@@ -277,12 +326,14 @@ func (s *migrationSuite) AMorsestateexportIsWrittenTo(morseStateExportFile strin
 func (s *migrationSuite) AnUnclaimedMorseclaimableaccountWithAKnownPrivateKeyExists() {
 	s.unstakedAccountIdx = s.nextMorseUnstakedKeyIdx()
 	s.appAccountIdx = s.nextMorseApplicationKeyIdx()
+	s.supplierAccountIdx = s.nextMorseSupplierKeyIdx()
 
 	// Since this is a step which is common to usage across multiple actor types,
 	// we must store the next available account index for each actor type for use
 	// in subsequent steps.
 	s.unstakedMorseClaimableAccount = s.getMorseClaimableAccountByIdx(s.unstakedAccountIdx)
 	s.appMorseClaimableAccount = s.getMorseClaimableAccountByIdx(s.appAccountIdx)
+	s.supplierMorseClaimableAccount = s.getMorseClaimableAccountByIdx(s.supplierAccountIdx)
 }
 
 // getMorseClaimableAccountByIdx returns a MorseClaimableAccount for the given index.
@@ -316,7 +367,21 @@ func (s *migrationSuite) TheShannonDestinationAccountBalanceIsIncreasedByTheUnst
 
 	expectedBalanceUpoktDiffCoin := s.expectedMorseClaimableAccount.GetUnstakedBalance().
 		Add(s.faucetFundedBalanceUpokt).
-		Sub(s.previousStakedApplicationUpokt)
+		// s.supplierStakingFeeIfApplicable is subtracted to account
+		// for the staking fee incurred during the action under test;
+		// e.g., claiming the Morse supplier account.
+		Sub(s.supplierStakingFeeIfApplicable).
+		Sub(s.previousStakedActorUpokt)
+
+	if !s.previousUnstakedBalanceUpoktOfCurrentShannonIdx.IsZero() {
+		expectedBalanceUpoktDiffCoin = expectedBalanceUpoktDiffCoin.
+			Sub(s.previousUnstakedBalanceUpoktOfCurrentShannonIdx).
+			// s.previousUnstakedBalanceUpoktOfCurrentShannonIdx will be missing
+			// s.supplierStakingFeeIfApplicable upokt due to the staking fee
+			// incurred during scenario setup.
+			Sub(s.supplierStakingFeeIfApplicable)
+	}
+
 	require.Equal(s, expectedBalanceUpoktDiffCoin, balanceUpoktDiffCoin)
 }
 
@@ -326,7 +391,12 @@ func (s *migrationSuite) TheShannonServiceConfigMatchesTheOneProvidedWhenClaimin
 		foundApp := s.getApplicationInfo(s.getShannonKeyName())
 		require.Equal(s, s.claimedActorServiceId, foundApp.GetServiceConfigs()[0].GetServiceId())
 	case actorTypeSupplier:
-		s.Skip("TODO_MAINNET_CRITICAL(@bryanchriswhite, #1034): Implement.")
+		effectiveServiceHeight := s.expectedSupplierEffectiveServiceHeight
+		s.Logf("waiting for effective service height: %d", effectiveServiceHeight)
+		s.waitForBlockHeight(effectiveServiceHeight)
+		foundSupplier := s.getSupplierInfo(s.getShannonKeyName())
+
+		require.Equal(s, s.claimedActorServiceId, foundSupplier.GetServices()[0].GetServiceId())
 	default:
 		s.Fatal("unexpected actor type %q", actorType)
 	}
@@ -470,14 +540,7 @@ func (s *migrationSuite) TheShannonDestinationAccountExistsOnchain() {
 }
 
 func (s *migrationSuite) TheShannonDestinationAccountIsNotStakedAsAn(actorType actorTypeEnum) {
-	switch actorType {
-	case actorTypeApp:
-		s.TheUserVerifiesTheForAccountIsNotStaked(actorType, s.getShannonKeyName())
-	case actorTypeSupplier:
-		s.Skip("TODO_MAINNET_CRITICAL(@bryanchriswhite, #1034): Implement.")
-	default:
-		s.Fatalf("unknown actor type %q", actorType)
-	}
+	s.TheUserVerifiesTheForAccountIsNotStaked(actorType, s.getShannonKeyName())
 }
 
 func (s *migrationSuite) MorsePrivateKeysAreAvailableInTheFollowingActorTypeDistribution(a gocuke.DataTable) {
@@ -639,6 +702,15 @@ func (s *migrationSuite) nextMorseApplicationKeyIdx() uint64 {
 	return s.nextMorseActorKeyIdx(testmigration.MorseApplicationActor)
 }
 
+// nextMorseSupplierKeyIdx returns the next morse private key index which is
+// intended to be used for staked supplier morse accounts. If the current
+// morseKeyIdx is not a staked supplier morse account, morseKeyIdx is incremented
+// until the next Morse key index which should be a staked supplier account,
+// given the round-robin distribution of morse account actor types.
+func (s *migrationSuite) nextMorseSupplierKeyIdx() uint64 {
+	return s.nextMorseActorKeyIdx(testmigration.MorseSupplierActor)
+}
+
 // nextMorseActorKeyIdx returns the next morse private key index which matches
 // the given actor type.
 //
@@ -718,3 +790,43 @@ func (s *migrationSuite) queryListMorseClaimableAccounts() []migrationtypes.Mors
 
 	return res.MorseClaimableAccount
 }
+
+// writeTempSupplierStakeConfig writes a supplier stake config with the given
+// serviceID and optional operatorAddress to a temporary file which is deleted
+// when the test completes. If operatorAddress is empty, the current shannon
+// address is used.
+func (s *migrationSuite) writeTempSupplierStakeConfig(serviceId, operatorAddress string) string {
+	ownerAddress, isFound := s.getShannonKeyAddress()
+	require.True(s, isFound)
+
+	if operatorAddress == "" {
+		operatorAddress = ownerAddress
+	}
+
+	supplierStakeConfig := config.YAMLStakeConfig{
+		OwnerAddress:    ownerAddress,
+		OperatorAddress: operatorAddress,
+		Services: []*config.YAMLStakeService{
+			{
+				ServiceId: serviceId,
+				Endpoints: []config.YAMLServiceEndpoint{
+					{
+						PubliclyExposedUrl: "http://relayminer1:8545",
+						RPCType:            "JSON_RPC",
+					},
+				},
+				// RevSharePercent: (intentionally omitted;
+				// the default rev share will be applied),
+			},
+		},
+		DefaultRevSharePercent: map[string]uint64{
+			ownerAddress: 100,
+		},
+		// StakeAmount: 		(explicitly omitted),
+		// see https://dev.poktroll.com/operate/morse_migration/claiming#claim-a-morse-supplier-staked-actor
+	}
+	supplierStakeConfigBz, err := yaml.Marshal(supplierStakeConfig)
+	require.NoError(s, err)
+
+	return s.writeTempFile(defaultSupplierStakeConfigYAMLFileName, supplierStakeConfigBz)
+}
diff --git a/e2e/tests/parse_params_test.go b/e2e/tests/parse_params_test.go
index 3d078b58cd..df83149613 100644
--- a/e2e/tests/parse_params_test.go
+++ b/e2e/tests/parse_params_test.go
@@ -19,6 +19,7 @@ import (
 	prooftypes "github.com/pokt-network/poktroll/x/proof/types"
 	servicetypes "github.com/pokt-network/poktroll/x/service/types"
 	sharedtypes "github.com/pokt-network/poktroll/x/shared/types"
+	suppliertypes "github.com/pokt-network/poktroll/x/supplier/types"
 	tokenomicstypes "github.com/pokt-network/poktroll/x/tokenomics/types"
 )
 
@@ -56,6 +57,8 @@ func (s *suite) parseParam(table gocuke.DataTable, rowIdx int) paramAny {
 		paramValue = table.Cell(rowIdx, paramValueColIdx).String()
 	case "int64":
 		paramValue = table.Cell(rowIdx, paramValueColIdx).Int64()
+	case "uint64":
+		paramValue = uint64(table.Cell(rowIdx, paramValueColIdx).Int64())
 	case "bytes":
 		paramValue = []byte(table.Cell(rowIdx, paramValueColIdx).String())
 	case "float":
@@ -94,7 +97,8 @@ func (s *suite) paramsMapToMsgUpdateParams(moduleName string, paramsMap paramsAn
 		msgUpdateParams = s.newAppMsgUpdateParams(paramsMap)
 	case servicetypes.ModuleName:
 		msgUpdateParams = s.newServiceMsgUpdateParams(paramsMap)
-	// NB: supplier module currently has no parameters
+	case suppliertypes.ModuleName:
+		msgUpdateParams = s.newSupplierMsgUpdateParams(paramsMap)
 	default:
 		err := fmt.Errorf("ERROR: unexpected module name %q", moduleName)
 		s.Fatal(err)
@@ -195,7 +199,9 @@ func (s *suite) newAppMsgUpdateParams(params paramsAnyMap) cosmostypes.Msg {
 		s.Logf("paramName: %s, value: %v", paramName, paramValue.value)
 		switch paramName {
 		case apptypes.ParamMaxDelegatedGateways:
-			msgUpdateParams.Params.MaxDelegatedGateways = uint64(paramValue.value.(int64))
+			msgUpdateParams.Params.MaxDelegatedGateways = paramValue.value.(uint64)
+		case apptypes.ParamMinStake:
+			msgUpdateParams.Params.MinStake = paramValue.value.(*cosmostypes.Coin)
 		default:
 			s.Fatalf("ERROR: unexpected %q type param name %q", paramValue.typeStr, paramName)
 		}
@@ -223,6 +229,28 @@ func (s *suite) newServiceMsgUpdateParams(params paramsAnyMap) cosmostypes.Msg {
 	return proto.Message(msgUpdateParams)
 }
 
+func (s *suite) newSupplierMsgUpdateParams(params map[string]paramAny) cosmostypes.Msg {
+	authority := authtypes.NewModuleAddress(s.granterName).String()
+
+	msgUpdateParams := &suppliertypes.MsgUpdateParams{
+		Authority: authority,
+		Params:    suppliertypes.Params{},
+	}
+
+	for paramName, paramValue := range params {
+		s.Logf("paramName: %s, value: %v", paramName, paramValue.value)
+		switch paramName {
+		case suppliertypes.ParamMinStake:
+			msgUpdateParams.Params.MinStake = paramValue.value.(*cosmostypes.Coin)
+		case suppliertypes.ParamStakingFee:
+			msgUpdateParams.Params.StakingFee = paramValue.value.(*cosmostypes.Coin)
+		default:
+			s.Fatalf("ERROR: unexpected %q type param name %q", paramValue.typeStr, paramName)
+		}
+	}
+	return proto.Message(msgUpdateParams)
+}
+
 // newMsgUpdateParam returns a MsgUpdateParam for the given module name, param name,
 // and param type/value.
 func (s *suite) newMsgUpdateParam(
@@ -242,6 +270,8 @@ func (s *suite) newMsgUpdateParam(
 		msg = s.newSharedMsgUpdateParam(authority, param)
 	case servicetypes.ModuleName:
 		msg = s.newServiceMsgUpdateParam(authority, param)
+	case suppliertypes.ModuleName:
+		msg = s.newSupplierMsgUpdateParam(authority, param)
 	default:
 		err := fmt.Errorf("ERROR: unexpected module name %q", moduleName)
 		s.Fatal(err)
@@ -373,3 +403,20 @@ func (s *suite) newGatewayMsgUpdateParam(authority string, param paramAny) (msg
 
 	return msg
 }
+
+func (s *suite) newSupplierMsgUpdateParam(authority string, param paramAny) (msg proto.Message) {
+	switch param.typeStr {
+	case "coin":
+		msg = proto.Message(&suppliertypes.MsgUpdateParam{
+			Authority: authority,
+			Name:      param.name,
+			AsType: &suppliertypes.MsgUpdateParam_AsCoin{
+				AsCoin: param.value.(*cosmostypes.Coin),
+			},
+		})
+	default:
+		s.Fatalf("unexpected param type %q for %s module", param.typeStr, suppliertypes.ModuleName)
+	}
+
+	return msg
+}
diff --git a/e2e/tests/session_steps_test.go b/e2e/tests/session_steps_test.go
index aaf63fa123..2d7d14f9a4 100644
--- a/e2e/tests/session_steps_test.go
+++ b/e2e/tests/session_steps_test.go
@@ -280,9 +280,8 @@ func (s *suite) TheApplicationReceivesEvmSubscriptionEventsUntilTheSessionEnds()
 }
 
 func (s *suite) TheSubscriptionIsClosedBeforeClaimWindowOpenHeightIsReached() {
-	currentHeight := s.getCurrentBlockHeight()
-
-	require.Equal(s, s.wsCloseHeight, currentHeight)
+	lastCommitHeight := s.getLastCommitBlockHeight()
+	require.Equal(s, s.wsCloseHeight, lastCommitHeight)
 }
 
 func (s *suite) sendRelaysForSession(
diff --git a/e2e/tests/update_params_test.go b/e2e/tests/update_params_test.go
index f9b38c5221..67acbe1b99 100644
--- a/e2e/tests/update_params_test.go
+++ b/e2e/tests/update_params_test.go
@@ -451,12 +451,14 @@ func (s *suite) assertExpectedModuleParamsUpdated(moduleName string) {
 			},
 		)
 	case apptypes.ModuleName:
-		maxDelegatedGateways := uint64(s.expectedModuleParams[moduleName][apptypes.ParamMaxDelegatedGateways].value.(int64))
+		maxDelegatedGateways := s.expectedModuleParams[moduleName][apptypes.ParamMaxDelegatedGateways].value.(uint64)
+		minStake := s.expectedModuleParams[moduleName][apptypes.ParamMinStake].value.(*cosmostypes.Coin)
 		assertUpdatedParams(s,
 			[]byte(res.Stdout),
 			&apptypes.QueryParamsResponse{
 				Params: apptypes.Params{
 					MaxDelegatedGateways: maxDelegatedGateways,
+					MinStake:             minStake,
 				},
 			},
 		)
@@ -470,6 +472,18 @@ func (s *suite) assertExpectedModuleParamsUpdated(moduleName string) {
 				},
 			},
 		)
+	case suppliertypes.ModuleName:
+		minStake := s.expectedModuleParams[moduleName][suppliertypes.ParamMinStake].value.(*cosmostypes.Coin)
+		stakingFee := s.expectedModuleParams[moduleName][suppliertypes.ParamStakingFee].value.(*cosmostypes.Coin)
+		assertUpdatedParams(s,
+			[]byte(res.Stdout),
+			&suppliertypes.QueryParamsResponse{
+				Params: suppliertypes.Params{
+					MinStake:   minStake,
+					StakingFee: stakingFee,
+				},
+			},
+		)
 	default:
 		s.Fatalf("ERROR: unexpected module name %q", moduleName)
 	}
diff --git a/go.mod b/go.mod
index 2d6c4beeb1..b1a5d9ac76 100644
--- a/go.mod
+++ b/go.mod
@@ -76,7 +76,7 @@ require (
 	google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142
 	google.golang.org/grpc v1.67.1
 	google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.4.0
-	google.golang.org/protobuf v1.35.1
+	google.golang.org/protobuf v1.35.2
 	gopkg.in/yaml.v2 v2.4.0
 )
 
@@ -135,7 +135,7 @@ require (
 	github.com/cockroachdb/redact v1.1.5 // indirect
 	github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect
 	github.com/cometbft/cometbft-db v0.11.0 // indirect
-	github.com/containerd/continuity v0.4.2 // indirect
+	github.com/containerd/continuity v0.4.4 // indirect
 	github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect
 	github.com/cosmos/btcutil v1.0.5 // indirect
 	github.com/cosmos/go-bip39 v1.0.0 // indirect
diff --git a/go.sum b/go.sum
index 4b2dc8819e..b6cc105a24 100644
--- a/go.sum
+++ b/go.sum
@@ -376,8 +376,8 @@ github.com/cometbft/cometbft v0.38.12 h1:OWsLZN2KcSSFe8bet9xCn07VwhBnavPea3VyPnN
 github.com/cometbft/cometbft v0.38.12/go.mod h1:GPHp3/pehPqgX1930HmK1BpBLZPxB75v/dZg8Viwy+o=
 github.com/cometbft/cometbft-db v0.11.0 h1:M3Lscmpogx5NTbb1EGyGDaFRdsoLWrUWimFEyf7jej8=
 github.com/cometbft/cometbft-db v0.11.0/go.mod h1:GDPJAC/iFHNjmZZPN8V8C1yr/eyityhi2W1hz2MGKSc=
-github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM=
-github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
+github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII=
+github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
 github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
 github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
 github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU=
@@ -1863,8 +1863,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
-google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
+google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/x/application/types/types.pb.go b/x/application/types/types.pb.go
index c6ee4c5ccf..633c9c503d 100644
--- a/x/application/types/types.pb.go
+++ b/x/application/types/types.pb.go
@@ -40,7 +40,7 @@ type Application struct {
 	//   - https://github.com/pokt-network/poktroll/pull/750#discussion_r1735025033
 	//   - https://www.notion.so/buildwithgrove/Off-chain-Application-Stake-Tracking-6a8bebb107db4f7f9dc62cbe7ba555f7
 	ServiceConfigs []*types1.ApplicationServiceConfig `protobuf:"bytes,3,rep,name=service_configs,json=serviceConfigs,proto3" json:"service_configs,omitempty"`
-	// TODO_BETA(@bryanchriswhite): Rename `delegatee_gateway_addresses` to `gateway_addresses_delegated_to`.
+	// TODO_MAINNET_MIGRATION(@bryanchriswhite): Rename `delegatee_gateway_addresses` to `gateway_addresses_delegated_to`.
 	// Ensure to rename all relevant configs, comments, variables, function names, etc as well.
 	// Non-nullable list of Bech32 encoded delegatee Gateway addresses
 	DelegateeGatewayAddresses []string `protobuf:"bytes,4,rep,name=delegatee_gateway_addresses,json=delegateeGatewayAddresses,proto3" json:"delegatee_gateway_addresses,omitempty"`
diff --git a/x/migration/module/cmd/claim_morse_supplier.go b/x/migration/module/cmd/claim_morse_supplier.go
new file mode 100644
index 0000000000..82ab1f7948
--- /dev/null
+++ b/x/migration/module/cmd/claim_morse_supplier.go
@@ -0,0 +1,211 @@
+package cmd
+
+import (
+	"bufio"
+	"encoding/json"
+	"fmt"
+	"os"
+
+	cosmosclient "github.com/cosmos/cosmos-sdk/client"
+	cosmosflags "github.com/cosmos/cosmos-sdk/client/flags"
+	"github.com/spf13/cobra"
+	"gopkg.in/yaml.v2"
+
+	"github.com/pokt-network/poktroll/cmd/flags"
+	"github.com/pokt-network/poktroll/cmd/logger"
+	"github.com/pokt-network/poktroll/x/migration/types"
+	"github.com/pokt-network/poktroll/x/supplier/config"
+)
+
+func ClaimSupplierCmd() *cobra.Command {
+	claimSupplierCmd := &cobra.Command{
+		Use:   "claim-supplier [morse_key_export_path] [path_to_supplier_stake_config] --from [shannon_dest_key_name]",
+		Args:  cobra.ExactArgs(2),
+		Short: "Claim an onchain MorseClaimableAccount as a staked supplier account",
+		Long: `Claim an onchain MorseClaimableAccount as a staked supplier account.
+
+The unstaked balance amount of the onchain MorseClaimableAccount will be minted to the Shannon account specified by the --from flag.
+The Shannon account will also be staked as a supplier with a stake equal to the supplier stake the MorseClaimableAccount had on Morse.
+
+This will construct, sign, and broadcast a tx containing a MsgClaimMorseSupplier message.
+
+For more information, see: https://dev.poktroll.com/operate/morse_migration/claiming`,
+		RunE:    runClaimSupplier,
+		PreRunE: logger.PreRunESetup,
+	}
+
+	// Add a string flag for providing a passphrase to decrypt the Morse keyfile.
+	claimSupplierCmd.Flags().StringVarP(
+		&morseKeyfileDecryptPassphrase,
+		flags.FlagPassphrase,
+		flags.FlagPassphraseShort,
+		"",
+		flags.FlagPassphraseUsage,
+	)
+
+	// Add a bool flag indicating whether to skip the passphrase prompt.
+	claimSupplierCmd.Flags().BoolVar(
+		&noPassphrase,
+		flags.FlagNoPassphrase,
+		false,
+		flags.FlagNoPassphraseUsage,
+	)
+
+	// This command depends on the conventional cosmos-sdk CLI tx flags.
+	cosmosflags.AddTxFlagsToCmd(claimSupplierCmd)
+
+	return claimSupplierCmd
+}
+
+// runClaimSupplier performs the following sequence:
+// - Load the Morse private key from the morse_key_export_path argument (arg 0).
+// - Load and validate the supplier service staking config from the path_to_supplier_stake_config argument pointing to a local config file (arg 1).
+// - Sign and broadcast the MsgClaimMorseSupplier message using the Shannon key named by the `--from` flag.
+// - Wait until the tx is committed onchain for either a synchronous or asynchronous error.
+func runClaimSupplier(cmd *cobra.Command, args []string) error {
+	ctx := cmd.Context()
+
+	// Retrieve and validate the morse key based on the first argument provided.
+	morseKeyExportPath := args[0]
+	morsePrivKey, err := loadMorsePrivateKey(morseKeyExportPath, morseKeyfileDecryptPassphrase)
+	if err != nil {
+		return err
+	}
+
+	// Conventionally derive a cosmos-sdk client context from the cobra command.
+	clientCtx, err := cosmosclient.GetClientTxContext(cmd)
+	if err != nil {
+		return err
+	}
+
+	// Load the supplier stake config from the YAML file.
+	supplierStakeConfigPath := args[1]
+	supplierStakeConfig, err := loadSupplierStakeConfigYAML(supplierStakeConfigPath)
+	if err != nil {
+		return err
+	}
+
+	// Ensure that the signing account matches either the configured owner or operator address.
+	signingAddr := clientCtx.GetFromAddress().String()
+	ownerAddr := supplierStakeConfig.OwnerAddress
+	operatorAddr := supplierStakeConfig.OperatorAddress
+	switch signingAddr {
+	case ownerAddr, operatorAddr:
+		// All good.
+	default:
+		return fmt.Errorf(
+			"signer address %s does not match owner address %s or supplier operator address %s",
+			signingAddr, ownerAddr, operatorAddr,
+		)
+	}
+
+	// Construct a MsgClaimMorseSupplier message.
+	msgClaimMorseSupplier, err := types.NewMsgClaimMorseSupplier(
+		ownerAddr,
+		operatorAddr,
+		morsePrivKey.PubKey().Address().String(),
+		morsePrivKey,
+		supplierStakeConfig.Services,
+	)
+	if err != nil {
+		return err
+	}
+
+	// Serialize, as JSON, and print the MsgClaimMorseSupplier for posterity and/or confirmation.
+	msgClaimMorseSupplierJSON, err := json.MarshalIndent(msgClaimMorseSupplier, "", "  ")
+	if err != nil {
+		return err
+	}
+
+	fmt.Printf("MsgClaimMorseSupplier %s\n", string(msgClaimMorseSupplierJSON))
+
+	// Last chance for the user to abort.
+	skipConfirmation, err := cmd.Flags().GetBool(cosmosflags.FlagSkipConfirmation)
+	if err != nil {
+		return err
+	}
+
+	if !skipConfirmation {
+		fmt.Printf("Confirm MsgClaimMorseSupplier: y/[n]: ")
+		stdinReader := bufio.NewReader(os.Stdin)
+
+		// This call to ReadLine() will block until the user sends a new line to stdin.
+		inputLine, _, readErr := stdinReader.ReadLine()
+		if readErr != nil {
+			return err
+		}
+
+		// Terminate the confirmation prompt output line.
+		fmt.Println()
+
+		// Abort unless some affirmative confirmation is given.
+		switch string(inputLine) {
+		case "Yes", "yes", "Y", "y":
+		default:
+			return nil
+		}
+	}
+
+	// Construct a tx client.
+	txClient, err := flags.GetTxClient(ctx, cmd)
+	if err != nil {
+		return err
+	}
+
+	// Sign and broadcast the claim Morse account message.
+	eitherErr := txClient.SignAndBroadcast(ctx, msgClaimMorseSupplier)
+	err, errCh := eitherErr.SyncOrAsyncError()
+	if err != nil {
+		return err
+	}
+
+	// Wait for an async error, timeout, or the errCh to close on success.
+	return <-errCh
+}
+
+// loadSupplierStakeConfigYAML loads, parses, and validates the supplier stake
+// config from configYAMLPath.
+func loadSupplierStakeConfigYAML(configYAMLPath string) (*config.SupplierStakeConfig, error) {
+	// Read the YAML file from the provided path.
+	yamlStakeConfigBz, err := os.ReadFile(configYAMLPath)
+	if err != nil {
+		return nil, err
+	}
+
+	// Unmarshal the YAML into a config.YAMLStakeConfig struct.
+	var yamlStakeConfig config.YAMLStakeConfig
+	if err = yaml.Unmarshal(yamlStakeConfigBz, &yamlStakeConfig); err != nil {
+		return nil, err
+	}
+
+	// Validate that the stake amount is not set in the YAML config.
+	if len(yamlStakeConfig.StakeAmount) != 0 {
+		return nil, config.ErrSupplierConfigInvalidStake.Wrapf("stake_amount MUST NOT be set in the supplier config YAML; it is automatically determined by the onchain MorseClaimableAccount state")
+	}
+
+	// Validate the owner and operator addresses.
+	if err = yamlStakeConfig.ValidateAndNormalizeAddresses(logger.Logger); err != nil {
+		return nil, err
+	}
+
+	// Validate the default revenue share map.
+	defaultRevShareMap, err := yamlStakeConfig.ValidateAndNormalizeDefaultRevShare()
+	if err != nil {
+		return nil, err
+	}
+
+	// Validate and parse the service configs.
+	supplierServiceConfigs, err := yamlStakeConfig.ValidateAndParseServiceConfigs(defaultRevShareMap)
+	if err != nil {
+		return nil, err
+	}
+
+	return &config.SupplierStakeConfig{
+		OwnerAddress:    yamlStakeConfig.OwnerAddress,
+		OperatorAddress: yamlStakeConfig.OperatorAddress,
+		Services:        supplierServiceConfigs,
+		// StakeAmount:  (intentionally omitted),
+		// The stake amount is determined by the sum of any existing supplier stake
+		// and the supplier stake amount of the associated MorseClaimableAccount.
+	}, nil
+}
diff --git a/x/migration/module/tx.go b/x/migration/module/tx.go
index a891782eeb..c192e59649 100644
--- a/x/migration/module/tx.go
+++ b/x/migration/module/tx.go
@@ -32,6 +32,7 @@ func TxCommands() *cobra.Command {
 	migrateCmd.AddCommand(cmd.CollectMorseAccountsCmd())
 	migrateCmd.AddCommand(cmd.ClaimAccountCmd())
 	migrateCmd.AddCommand(cmd.ClaimApplicationCmd())
+	migrateCmd.AddCommand(cmd.ClaimSupplierCmd())
 	migrateCmd.AddCommand(cmd.ImportMorseAccountsCmd())
 	migrateCmd.PersistentFlags().StringVar(&logger.LogLevel, flags.FlagLogLevel, "info", flags.FlagLogLevelUsage)
 	migrateCmd.PersistentFlags().StringVar(&logger.LogOutput, flags.FlagLogOutput, flags.DefaultLogOutput, flags.FlagLogOutputUsage)
diff --git a/x/migration/types/morse_offchain.pb.go b/x/migration/types/morse_offchain.pb.go
index 57c99e4c5c..9cb847c691 100644
--- a/x/migration/types/morse_offchain.pb.go
+++ b/x/migration/types/morse_offchain.pb.go
@@ -358,7 +358,7 @@ type MorseApplication struct {
 	Address github_com_cometbft_cometbft_crypto.Address `protobuf:"bytes,1,opt,name=address,proto3,casttype=github.com/cometbft/cometbft/crypto.Address" json:"address" yaml:"address"`
 	// PublicKey is the binary representation of a Morse application's ed25519 public key.
 	PublicKey []byte `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key" yaml:"public_key"`
-	// TODO_MAINNET(@Olshansk):  Should status and/or jailed be considered during the migration, and if so, how?
+	// TODO_MAINNET_MIGRATION(@Olshansk):  Should status and/or jailed be considered during the migration, and if so, how?
 	Jailed bool  `protobuf:"varint,3,opt,name=jailed,proto3" json:"jailed" yaml:"jailed"`
 	Status int32 `protobuf:"varint,4,opt,name=status,proto3" json:"status" yaml:"status"`
 	// The string representation of the BigInt amount of upokt.
@@ -403,7 +403,7 @@ type MorseValidator struct {
 	Address github_com_cometbft_cometbft_crypto.Address `protobuf:"bytes,1,opt,name=address,proto3,casttype=github.com/cometbft/cometbft/crypto.Address" json:"address" yaml:"address"`
 	// The binary representation of a Morse application's ed25519 public key.
 	PublicKey []byte `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key" yaml:"public_key"`
-	// TODO_MAINNET(@Olshansk):  Should status and/or jailed be considered during the migration, and if so, how?
+	// TODO_MAINNET_MIGRATION(@Olshansk):  Should status and/or jailed be considered during the migration, and if so, how?
 	Jailed bool  `protobuf:"varint,3,opt,name=jailed,proto3" json:"jailed"`
 	Status int32 `protobuf:"varint,4,opt,name=status,proto3" json:"status"`
 	// The string representation of the BigInt amount of upokt.
diff --git a/x/proof/types/params.pb.go b/x/proof/types/params.pb.go
index 6e190e191f..4c1c475821 100644
--- a/x/proof/types/params.pb.go
+++ b/x/proof/types/params.pb.go
@@ -36,18 +36,18 @@ type Params struct {
 	// is equal to or above the threshold. This is in contrast to the this requirement
 	// being determined probabilistically via ProofRequestProbability.
 	//
-	// TODO_MAINNET: Consider renaming this to `proof_requirement_threshold_upokt`.
+	// TODO_MAINNET_MIGRATION: Consider renaming this to `proof_requirement_threshold_upokt`.
 	ProofRequirementThreshold *types.Coin `protobuf:"bytes,3,opt,name=proof_requirement_threshold,json=proofRequirementThreshold,proto3" json:"proof_requirement_threshold"`
 	// proof_missing_penalty is the number of tokens (uPOKT) which should be slashed from a supplier
 	// when a proof is required (either via proof_requirement_threshold or proof_missing_penalty)
 	// but is not provided.
-	// TODO_MAINNET: Consider renaming this to `proof_missing_penalty_upokt`.
+	// TODO_MAINNET_MIGRATION: Consider renaming this to `proof_missing_penalty_upokt`.
 	ProofMissingPenalty *types.Coin `protobuf:"bytes,4,opt,name=proof_missing_penalty,json=proofMissingPenalty,proto3" json:"proof_missing_penalty"`
 	// proof_submission_fee is the number of tokens (uPOKT) which should be paid by
 	// the supplier operator when submitting a proof.
 	// This is needed to account for the cost of storing proofs onchain and prevent
 	// spamming (i.e. sybil bloat attacks) the network with non-required proofs.
-	// TODO_MAINNET: Consider renaming this to `proof_submission_fee_upokt`.
+	// TODO_MAINNET_MIGRATION: Consider renaming this to `proof_submission_fee_upokt`.
 	ProofSubmissionFee *types.Coin `protobuf:"bytes,5,opt,name=proof_submission_fee,json=proofSubmissionFee,proto3" json:"proof_submission_fee"`
 }
 
diff --git a/x/shared/types/service.pb.go b/x/shared/types/service.pb.go
index 7e25515631..ad4eb177d6 100644
--- a/x/shared/types/service.pb.go
+++ b/x/shared/types/service.pb.go
@@ -93,7 +93,7 @@ func (ConfigOptions) EnumDescriptor() ([]byte, []int) {
 type Service struct {
 	// For example, what if we want to request a session for a certain service but with some additional configs that identify it?
 	Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
-	// TODO_BETA(@bryanchriswhite): Either remove this or rename it to alias.
+	// TODO_MAINNET_MIGRATION(@bryanchriswhite): Either remove this or rename it to alias.
 	Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
 	// The cost of a single relay for this service in terms of compute units.
 	// Must be used alongside the global 'compute_units_to_tokens_multipler' to calculate the cost of a relay for this service.
diff --git a/x/supplier/config/supplier_configs_reader.go b/x/supplier/config/supplier_configs_reader.go
index 6bfbd775a4..1d70f3aea0 100644
--- a/x/supplier/config/supplier_configs_reader.go
+++ b/x/supplier/config/supplier_configs_reader.go
@@ -62,7 +62,7 @@ func ParseSupplierConfigs(ctx context.Context, configContent []byte) (*SupplierS
 		return nil, ErrSupplierConfigUnmarshalYAML.Wrapf("%s", err)
 	}
 
-	if err := stakeConfig.validateAndNormalizeAddresses(logger); err != nil {
+	if err := stakeConfig.ValidateAndNormalizeAddresses(logger); err != nil {
 		return nil, err
 	}
 
@@ -71,12 +71,12 @@ func ParseSupplierConfigs(ctx context.Context, configContent []byte) (*SupplierS
 		return nil, err
 	}
 
-	defaultRevShareMap, err := stakeConfig.validateAndNormalizeDefaultRevShare()
+	defaultRevShareMap, err := stakeConfig.ValidateAndNormalizeDefaultRevShare()
 	if err != nil {
 		return nil, err
 	}
 
-	supplierServiceConfigs, err := stakeConfig.validateAndParseServiceConfigs(defaultRevShareMap)
+	supplierServiceConfigs, err := stakeConfig.ValidateAndParseServiceConfigs(defaultRevShareMap)
 	if err != nil {
 		return nil, err
 	}
@@ -167,12 +167,12 @@ func validateEndpointURL(endpoint YAMLServiceEndpoint) (string, error) {
 	return endpoint.PubliclyExposedUrl, nil
 }
 
-// validateAndNormalizeAddresses validates the configured owner and operator addresses
+// ValidateAndNormalizeAddresses validates the configured owner and operator addresses
 // as bech32-encoded pokt addresses.
 //
 // If the operator address is empty:
 // • The owner address is set as the operator address as well
-func (yamlStakeConfig *YAMLStakeConfig) validateAndNormalizeAddresses(logger polylog.Logger) error {
+func (yamlStakeConfig *YAMLStakeConfig) ValidateAndNormalizeAddresses(logger polylog.Logger) error {
 	// Validate required owner address.
 	if _, err := sdk.AccAddressFromBech32(yamlStakeConfig.OwnerAddress); err != nil {
 		return ErrSupplierConfigInvalidOwnerAddress.Wrap("invalid owner address")
@@ -236,7 +236,7 @@ func (yamlStakeConfig *YAMLStakeConfig) ParseAndValidateStakeAmount() (*sdk.Coin
 // • If DefaultRevSharePercent is not specified:
 //   - Require a valid owner address
 //   - Automatically assigns 100% revenue share to the owner
-func (yamlStakeConfig *YAMLStakeConfig) validateAndNormalizeDefaultRevShare() (map[string]uint64, error) {
+func (yamlStakeConfig *YAMLStakeConfig) ValidateAndNormalizeDefaultRevShare() (map[string]uint64, error) {
 	defaultRevSharePercent := map[string]uint64{}
 
 	// Check if the default rev share is already configured and return if so
@@ -254,7 +254,7 @@ func (yamlStakeConfig *YAMLStakeConfig) validateAndNormalizeDefaultRevShare() (m
 	return defaultRevSharePercent, nil
 }
 
-// validateAndParseServiceConfigs performs the following:
+// ValidateAndParseServiceConfigs performs the following:
 //   - Validate that at least one service is configured
 //   - Validate the configured service IDs
 //   - Parse the configured endpoints
@@ -263,7 +263,7 @@ func (yamlStakeConfig *YAMLStakeConfig) validateAndNormalizeDefaultRevShare() (m
 //   - If the configured rev share for any service is empty, defaultRevSharePercent is configured for that service
 //
 // Returns the resulting supplier service configs.
-func (stakeConfig *YAMLStakeConfig) validateAndParseServiceConfigs(defaultRevSharePercent map[string]uint64) ([]*sharedtypes.SupplierServiceConfig, error) {
+func (stakeConfig *YAMLStakeConfig) ValidateAndParseServiceConfigs(defaultRevSharePercent map[string]uint64) ([]*sharedtypes.SupplierServiceConfig, error) {
 	// Validate at least one service is specified
 	if len(stakeConfig.Services) == 0 {
 		return nil, ErrSupplierConfigInvalidServiceId.Wrap("serviceIds cannot be empty")


Reviews:
user: login: Olshansk
body: Haven't reviewed `e2e/tests/migration_steps_test.go` yet but pushed a few small NITs here.

@bryanchriswhite Can we take this as an opportunity (I just don't have time myself) to ensure that onchain fails claiming if the actor is already staked. E.g.
 - Shannon Addr A is staked as a supplier with 1000 POKT
 - Morse Addr B tries to claim as A with some value (less or greater than 1000)
 - What happens then?
 
 Apoligize if I'm not being as clear as I usually am but appreciate if you can fill in the gaps/details.
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T04:39:58Z
user: login: bryanchriswhite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T14:48:08Z
user: login: bryanchriswhite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T14:50:34Z
user: login: Olshansk
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-02T19:16:58Z
user: login: github-actions[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-03T09:29:25Z

Review comments:
diff_hunk: @@ -0,0 +1,203 @@
+package cmd
+
+import (
+	"bufio"
+	"encoding/json"
+	"fmt"
+	"os"
+
+	cosmosclient "github.com/cosmos/cosmos-sdk/client"
+	cosmosflags "github.com/cosmos/cosmos-sdk/client/flags"
+	"github.com/spf13/cobra"
+	"gopkg.in/yaml.v2"
+
+	"github.com/pokt-network/poktroll/cmd/flags"
+	"github.com/pokt-network/poktroll/cmd/logger"
+	"github.com/pokt-network/poktroll/x/migration/types"
+	"github.com/pokt-network/poktroll/x/supplier/config"
+)
+
+func ClaimSupplierCmd() *cobra.Command {
+	claimSupplierCmd := &cobra.Command{
+		Use:   "claim-supplier [morse_key_export_path] [path_to_supplier_stake_config] --from [shannon_dest_key_name]",
+		Args:  cobra.ExactArgs(2),
+		Short: "Claim an onchain MorseClaimableAccount as a staked supplier account",
+		Long: `Claim an onchain MorseClaimableAccount as a staked supplier account.
+
+The unstaked balance amount of the onchain MorseClaimableAccount will be minted to the Shannon account specified by the --from flag.
+The Shannon account will also be staked as a supplier with a stake equal to the supplier stake the MorseClaimableAccount had on Morse.
+
+This will construct, sign, and broadcast a tx containing a MsgClaimMorseSupplier message.
+
+For more information, see: https://dev.poktroll.com/operate/morse_migration/claiming`,
+		RunE:    runClaimSupplier,
+		PreRunE: logger.PreRunESetup,
+	}
+
+	// Add a string flag for providing a passphrase to decrypt the Morse keyfile.
+	claimSupplierCmd.Flags().StringVarP(
+		&morseKeyfileDecryptPassphrase,
+		flags.FlagPassphrase,
+		flags.FlagPassphraseShort,
+		"",
+		flags.FlagPassphraseUsage,
+	)
+
+	// Add a bool flag indicating whether to skip the passphrase prompt.
+	claimSupplierCmd.Flags().BoolVar(
+		&noPassphrase,
+		flags.FlagNoPassphrase,
+		false,
+		flags.FlagNoPassphraseUsage,
+	)
+
+	// This command depends on the conventional cosmos-sdk CLI tx flags.
+	cosmosflags.AddTxFlagsToCmd(claimSupplierCmd)
+
+	return claimSupplierCmd
+}
+
+// runClaimSupplier performs the following sequence:
+// - Load the Morse private key from the morse_key_export_path argument (arg 0).
+// - Load and validate the supplier service staking config from the shannon_service_id argument (arg 1).
+// - Sign and broadcast the MsgClaimMorseSupplier message using the Shannon key named by the `--from` flag.
+// - Wait until the tx is committed onchain for either a synchronous or asynchronous error.
+func runClaimSupplier(cmd *cobra.Command, args []string) error {
+	ctx := cmd.Context()
+
+	// Retrieve and validate the morse key based on the first argument provided.
+	morseKeyExportPath := args[0]
+	morsePrivKey, err := loadMorsePrivateKey(morseKeyExportPath, morseKeyfileDecryptPassphrase)
+	if err != nil {
+		return err
+	}
+
+	// Conventionally derive a cosmos-sdk client context from the cobra command.
+	clientCtx, err := cosmosclient.GetClientTxContext(cmd)
+	if err != nil {
+		return err
+	}
+
+	// Load the supplier stake config from the YAML file.
+	supplierStakeConfigPath := args[1]
+	supplierStakeConfig, err := loadSupplierStakeConfigYAML(supplierStakeConfigPath)
+	if err != nil {
+		return err
+	}
+
+	// Ensure that the signing account matches either the configured owner or operator address.
+	signingAddr := clientCtx.GetFromAddress().String()
+	ownerAddr := supplierStakeConfig.OwnerAddress
+	operatorAddr := supplierStakeConfig.OperatorAddress
+	switch signingAddr {
+	case ownerAddr, operatorAddr:
+		// All good.
+	default:
+		return fmt.Errorf(
+			"signer address %s does not match owner address %s or supplier operator address %s",
+			signingAddr, ownerAddr, operatorAddr,
+		)
+	}
+
+	// Construct a MsgClaimMorseSupplier message.
+	msgClaimMorseSupplier, err := types.NewMsgClaimMorseSupplier(
+		ownerAddr,
+		operatorAddr,
+		morsePrivKey.PubKey().Address().String(),
+		morsePrivKey,
+		supplierStakeConfig.Services,
+	)
+	if err != nil {
+		return err
+	}
+
+	// Serialize, as JSON, and print the MsgClaimMorseSupplier for posterity and/or confirmation.
+	msgClaimMorseSupplierJSON, err := json.MarshalIndent(msgClaimMorseSupplier, "", "  ")
+	if err != nil {
+		return err
+	}
+
+	fmt.Printf("MsgClaimMorseSupplier %s\n", string(msgClaimMorseSupplierJSON))
+
+	// Last chance for the user to abort.
+	skipConfirmation, err := cmd.Flags().GetBool(cosmosflags.FlagSkipConfirmation)
+	if err != nil {
+		return err
+	}
+
+	if !skipConfirmation {
+		fmt.Printf("Confirm MsgClaimMorseSupplier: y/[n]: ")
+		stdinReader := bufio.NewReader(os.Stdin)
+
+		// This call to ReadLine() will block until the user sends a new line to stdin.
+		inputLine, _, readErr := stdinReader.ReadLine()
+		if readErr != nil {
+			return err
+		}
+
+		// Terminate the confirmation prompt output line.
+		fmt.Println()
+
+		// Abort unless some affirmative confirmation is given.
+		switch string(inputLine) {
+		case "Yes", "yes", "Y", "y":
+		default:
+			return nil
+		}
+	}
+
+	// Construct a tx client.
+	txClient, err := flags.GetTxClient(ctx, cmd)
+	if err != nil {
+		return err
+	}
+
+	// Sign and broadcast the claim Morse account message.
+	eitherErr := txClient.SignAndBroadcast(ctx, msgClaimMorseSupplier)
+	err, errCh := eitherErr.SyncOrAsyncError()
+	if err != nil {
+		return err
+	}
+
+	// Wait for an async error, timeout, or the errCh to close on success.
+	return <-errCh
+}
+
+// loadSupplierStakeConfigYAML loads, parses, and validates the supplier stake
+// config from configYAMLPath.
+func loadSupplierStakeConfigYAML(configYAMLPath string) (*config.SupplierStakeConfig, error) {
+	yamlStakeConfigBz, err := os.ReadFile(configYAMLPath)
+	if err != nil {
+		return nil, err
+	}
+
+	var yamlStakeConfig config.YAMLStakeConfig
+	if err = yaml.Unmarshal(yamlStakeConfigBz, &yamlStakeConfig); err != nil {
+		return nil, err
+	}
+
+	if len(yamlStakeConfig.StakeAmount) != 0 {
+		return nil, config.ErrSupplierConfigInvalidStake.Wrapf("stake_amount MUST NOT be set in the supplier config YAML; it is automatically determined by the onchain MorseClaimableAccount state")
+	}
+
+	if err = yamlStakeConfig.ValidateAndNormalizeAddresses(logger.Logger); err != nil {
+		return nil, err
+	}
+
+	defaultRevShareMap, err := yamlStakeConfig.ValidateAndNormalizeDefaultRevShare()
+	if err != nil {
+		return nil, err
+	}
+
+	supplierServiceConfigs, err := yamlStakeConfig.ValidateAndParseServiceConfigs(defaultRevShareMap)
+	if err != nil {
+		return nil, err
+	}
+
+	return &config.SupplierStakeConfig{
+		OwnerAddress:    yamlStakeConfig.OwnerAddress,
+		OperatorAddress: yamlStakeConfig.OperatorAddress,
+		Services:        supplierServiceConfigs,
+		// StakeAmount:  (intentionally omitted),
path: x/migration/module/cmd/claim_morse_supplier.go
user: login: Olshansk
body: #PUC why
created_at: 2025-04-02T04:33:17Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,203 @@
+package cmd
+
+import (
+	"bufio"
+	"encoding/json"
+	"fmt"
+	"os"
+
+	cosmosclient "github.com/cosmos/cosmos-sdk/client"
+	cosmosflags "github.com/cosmos/cosmos-sdk/client/flags"
+	"github.com/spf13/cobra"
+	"gopkg.in/yaml.v2"
+
+	"github.com/pokt-network/poktroll/cmd/flags"
+	"github.com/pokt-network/poktroll/cmd/logger"
+	"github.com/pokt-network/poktroll/x/migration/types"
+	"github.com/pokt-network/poktroll/x/supplier/config"
+)
+
+func ClaimSupplierCmd() *cobra.Command {
+	claimSupplierCmd := &cobra.Command{
+		Use:   "claim-supplier [morse_key_export_path] [path_to_supplier_stake_config] --from [shannon_dest_key_name]",
+		Args:  cobra.ExactArgs(2),
+		Short: "Claim an onchain MorseClaimableAccount as a staked supplier account",
+		Long: `Claim an onchain MorseClaimableAccount as a staked supplier account.
+
+The unstaked balance amount of the onchain MorseClaimableAccount will be minted to the Shannon account specified by the --from flag.
+The Shannon account will also be staked as a supplier with a stake equal to the supplier stake the MorseClaimableAccount had on Morse.
+
+This will construct, sign, and broadcast a tx containing a MsgClaimMorseSupplier message.
+
+For more information, see: https://dev.poktroll.com/operate/morse_migration/claiming`,
+		RunE:    runClaimSupplier,
+		PreRunE: logger.PreRunESetup,
+	}
+
+	// Add a string flag for providing a passphrase to decrypt the Morse keyfile.
+	claimSupplierCmd.Flags().StringVarP(
+		&morseKeyfileDecryptPassphrase,
+		flags.FlagPassphrase,
+		flags.FlagPassphraseShort,
+		"",
+		flags.FlagPassphraseUsage,
+	)
+
+	// Add a bool flag indicating whether to skip the passphrase prompt.
+	claimSupplierCmd.Flags().BoolVar(
+		&noPassphrase,
+		flags.FlagNoPassphrase,
+		false,
+		flags.FlagNoPassphraseUsage,
+	)
+
+	// This command depends on the conventional cosmos-sdk CLI tx flags.
+	cosmosflags.AddTxFlagsToCmd(claimSupplierCmd)
+
+	return claimSupplierCmd
+}
+
+// runClaimSupplier performs the following sequence:
+// - Load the Morse private key from the morse_key_export_path argument (arg 0).
+// - Load and validate the supplier service staking config from the shannon_service_id argument (arg 1).
+// - Sign and broadcast the MsgClaimMorseSupplier message using the Shannon key named by the `--from` flag.
+// - Wait until the tx is committed onchain for either a synchronous or asynchronous error.
+func runClaimSupplier(cmd *cobra.Command, args []string) error {
+	ctx := cmd.Context()
+
+	// Retrieve and validate the morse key based on the first argument provided.
+	morseKeyExportPath := args[0]
+	morsePrivKey, err := loadMorsePrivateKey(morseKeyExportPath, morseKeyfileDecryptPassphrase)
+	if err != nil {
+		return err
+	}
+
+	// Conventionally derive a cosmos-sdk client context from the cobra command.
+	clientCtx, err := cosmosclient.GetClientTxContext(cmd)
+	if err != nil {
+		return err
+	}
+
+	// Load the supplier stake config from the YAML file.
+	supplierStakeConfigPath := args[1]
+	supplierStakeConfig, err := loadSupplierStakeConfigYAML(supplierStakeConfigPath)
+	if err != nil {
+		return err
+	}
+
+	// Ensure that the signing account matches either the configured owner or operator address.
+	signingAddr := clientCtx.GetFromAddress().String()
+	ownerAddr := supplierStakeConfig.OwnerAddress
+	operatorAddr := supplierStakeConfig.OperatorAddress
+	switch signingAddr {
+	case ownerAddr, operatorAddr:
+		// All good.
+	default:
+		return fmt.Errorf(
+			"signer address %s does not match owner address %s or supplier operator address %s",
+			signingAddr, ownerAddr, operatorAddr,
+		)
+	}
+
+	// Construct a MsgClaimMorseSupplier message.
+	msgClaimMorseSupplier, err := types.NewMsgClaimMorseSupplier(
+		ownerAddr,
+		operatorAddr,
+		morsePrivKey.PubKey().Address().String(),
+		morsePrivKey,
+		supplierStakeConfig.Services,
+	)
+	if err != nil {
+		return err
+	}
+
+	// Serialize, as JSON, and print the MsgClaimMorseSupplier for posterity and/or confirmation.
+	msgClaimMorseSupplierJSON, err := json.MarshalIndent(msgClaimMorseSupplier, "", "  ")
+	if err != nil {
+		return err
+	}
+
+	fmt.Printf("MsgClaimMorseSupplier %s\n", string(msgClaimMorseSupplierJSON))
+
+	// Last chance for the user to abort.
+	skipConfirmation, err := cmd.Flags().GetBool(cosmosflags.FlagSkipConfirmation)
+	if err != nil {
+		return err
+	}
+
+	if !skipConfirmation {
+		fmt.Printf("Confirm MsgClaimMorseSupplier: y/[n]: ")
+		stdinReader := bufio.NewReader(os.Stdin)
+
+		// This call to ReadLine() will block until the user sends a new line to stdin.
+		inputLine, _, readErr := stdinReader.ReadLine()
+		if readErr != nil {
+			return err
+		}
+
+		// Terminate the confirmation prompt output line.
+		fmt.Println()
+
+		// Abort unless some affirmative confirmation is given.
+		switch string(inputLine) {
+		case "Yes", "yes", "Y", "y":
+		default:
+			return nil
+		}
+	}
+
+	// Construct a tx client.
+	txClient, err := flags.GetTxClient(ctx, cmd)
+	if err != nil {
+		return err
+	}
+
+	// Sign and broadcast the claim Morse account message.
+	eitherErr := txClient.SignAndBroadcast(ctx, msgClaimMorseSupplier)
+	err, errCh := eitherErr.SyncOrAsyncError()
+	if err != nil {
+		return err
+	}
+
+	// Wait for an async error, timeout, or the errCh to close on success.
+	return <-errCh
+}
+
+// loadSupplierStakeConfigYAML loads, parses, and validates the supplier stake
+// config from configYAMLPath.
+func loadSupplierStakeConfigYAML(configYAMLPath string) (*config.SupplierStakeConfig, error) {
+	yamlStakeConfigBz, err := os.ReadFile(configYAMLPath)
+	if err != nil {
+		return nil, err
+	}
+
+	var yamlStakeConfig config.YAMLStakeConfig
+	if err = yaml.Unmarshal(yamlStakeConfigBz, &yamlStakeConfig); err != nil {
+		return nil, err
+	}
+
+	if len(yamlStakeConfig.StakeAmount) != 0 {
path: x/migration/module/cmd/claim_morse_supplier.go
user: login: Olshansk
body: Thoughts on enforcing this (in the CLI) to be the same as what's onchain for validation purposes?
created_at: 2025-04-02T04:33:56Z
author_association: COLLABORATOR
diff_hunk: @@ -170,34 +187,54 @@ func (s *migrationSuite) ALocalMorseNodePersistedStateExists() {
 	}
 }
 
+// updatePreviousUnstakedBalanceUpoktOfCurrentShannonIdx queries for the current
path: e2e/tests/migration_steps_test.go
user: login: Olshansk
body: Can you update the comment (bullet point please) on where/how this is used?
created_at: 2025-04-02T04:35:25Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,203 @@
+package cmd
+
+import (
+	"bufio"
+	"encoding/json"
+	"fmt"
+	"os"
+
+	cosmosclient "github.com/cosmos/cosmos-sdk/client"
+	cosmosflags "github.com/cosmos/cosmos-sdk/client/flags"
+	"github.com/spf13/cobra"
+	"gopkg.in/yaml.v2"
+
+	"github.com/pokt-network/poktroll/cmd/flags"
+	"github.com/pokt-network/poktroll/cmd/logger"
+	"github.com/pokt-network/poktroll/x/migration/types"
+	"github.com/pokt-network/poktroll/x/supplier/config"
+)
+
+func ClaimSupplierCmd() *cobra.Command {
+	claimSupplierCmd := &cobra.Command{
+		Use:   "claim-supplier [morse_key_export_path] [path_to_supplier_stake_config] --from [shannon_dest_key_name]",
+		Args:  cobra.ExactArgs(2),
+		Short: "Claim an onchain MorseClaimableAccount as a staked supplier account",
+		Long: `Claim an onchain MorseClaimableAccount as a staked supplier account.
+
+The unstaked balance amount of the onchain MorseClaimableAccount will be minted to the Shannon account specified by the --from flag.
+The Shannon account will also be staked as a supplier with a stake equal to the supplier stake the MorseClaimableAccount had on Morse.
+
+This will construct, sign, and broadcast a tx containing a MsgClaimMorseSupplier message.
+
+For more information, see: https://dev.poktroll.com/operate/morse_migration/claiming`,
+		RunE:    runClaimSupplier,
+		PreRunE: logger.PreRunESetup,
+	}
+
+	// Add a string flag for providing a passphrase to decrypt the Morse keyfile.
+	claimSupplierCmd.Flags().StringVarP(
+		&morseKeyfileDecryptPassphrase,
+		flags.FlagPassphrase,
+		flags.FlagPassphraseShort,
+		"",
+		flags.FlagPassphraseUsage,
+	)
+
+	// Add a bool flag indicating whether to skip the passphrase prompt.
+	claimSupplierCmd.Flags().BoolVar(
+		&noPassphrase,
+		flags.FlagNoPassphrase,
+		false,
+		flags.FlagNoPassphraseUsage,
+	)
+
+	// This command depends on the conventional cosmos-sdk CLI tx flags.
+	cosmosflags.AddTxFlagsToCmd(claimSupplierCmd)
+
+	return claimSupplierCmd
+}
+
+// runClaimSupplier performs the following sequence:
+// - Load the Morse private key from the morse_key_export_path argument (arg 0).
+// - Load and validate the supplier service staking config from the shannon_service_id argument (arg 1).
+// - Sign and broadcast the MsgClaimMorseSupplier message using the Shannon key named by the `--from` flag.
+// - Wait until the tx is committed onchain for either a synchronous or asynchronous error.
+func runClaimSupplier(cmd *cobra.Command, args []string) error {
+	ctx := cmd.Context()
+
+	// Retrieve and validate the morse key based on the first argument provided.
+	morseKeyExportPath := args[0]
+	morsePrivKey, err := loadMorsePrivateKey(morseKeyExportPath, morseKeyfileDecryptPassphrase)
+	if err != nil {
+		return err
+	}
+
+	// Conventionally derive a cosmos-sdk client context from the cobra command.
+	clientCtx, err := cosmosclient.GetClientTxContext(cmd)
+	if err != nil {
+		return err
+	}
+
+	// Load the supplier stake config from the YAML file.
+	supplierStakeConfigPath := args[1]
+	supplierStakeConfig, err := loadSupplierStakeConfigYAML(supplierStakeConfigPath)
+	if err != nil {
+		return err
+	}
+
+	// Ensure that the signing account matches either the configured owner or operator address.
+	signingAddr := clientCtx.GetFromAddress().String()
+	ownerAddr := supplierStakeConfig.OwnerAddress
+	operatorAddr := supplierStakeConfig.OperatorAddress
+	switch signingAddr {
+	case ownerAddr, operatorAddr:
+		// All good.
+	default:
+		return fmt.Errorf(
+			"signer address %s does not match owner address %s or supplier operator address %s",
+			signingAddr, ownerAddr, operatorAddr,
+		)
+	}
+
+	// Construct a MsgClaimMorseSupplier message.
+	msgClaimMorseSupplier, err := types.NewMsgClaimMorseSupplier(
+		ownerAddr,
+		operatorAddr,
+		morsePrivKey.PubKey().Address().String(),
+		morsePrivKey,
+		supplierStakeConfig.Services,
+	)
+	if err != nil {
+		return err
+	}
+
+	// Serialize, as JSON, and print the MsgClaimMorseSupplier for posterity and/or confirmation.
+	msgClaimMorseSupplierJSON, err := json.MarshalIndent(msgClaimMorseSupplier, "", "  ")
+	if err != nil {
+		return err
+	}
+
+	fmt.Printf("MsgClaimMorseSupplier %s\n", string(msgClaimMorseSupplierJSON))
+
+	// Last chance for the user to abort.
+	skipConfirmation, err := cmd.Flags().GetBool(cosmosflags.FlagSkipConfirmation)
+	if err != nil {
+		return err
+	}
+
+	if !skipConfirmation {
+		fmt.Printf("Confirm MsgClaimMorseSupplier: y/[n]: ")
+		stdinReader := bufio.NewReader(os.Stdin)
+
+		// This call to ReadLine() will block until the user sends a new line to stdin.
+		inputLine, _, readErr := stdinReader.ReadLine()
+		if readErr != nil {
+			return err
+		}
+
+		// Terminate the confirmation prompt output line.
+		fmt.Println()
+
+		// Abort unless some affirmative confirmation is given.
+		switch string(inputLine) {
+		case "Yes", "yes", "Y", "y":
+		default:
+			return nil
+		}
+	}
+
+	// Construct a tx client.
+	txClient, err := flags.GetTxClient(ctx, cmd)
+	if err != nil {
+		return err
+	}
+
+	// Sign and broadcast the claim Morse account message.
+	eitherErr := txClient.SignAndBroadcast(ctx, msgClaimMorseSupplier)
+	err, errCh := eitherErr.SyncOrAsyncError()
+	if err != nil {
+		return err
+	}
+
+	// Wait for an async error, timeout, or the errCh to close on success.
+	return <-errCh
+}
+
+// loadSupplierStakeConfigYAML loads, parses, and validates the supplier stake
+// config from configYAMLPath.
+func loadSupplierStakeConfigYAML(configYAMLPath string) (*config.SupplierStakeConfig, error) {
+	yamlStakeConfigBz, err := os.ReadFile(configYAMLPath)
+	if err != nil {
+		return nil, err
+	}
+
+	var yamlStakeConfig config.YAMLStakeConfig
+	if err = yaml.Unmarshal(yamlStakeConfigBz, &yamlStakeConfig); err != nil {
+		return nil, err
+	}
+
+	if len(yamlStakeConfig.StakeAmount) != 0 {
+		return nil, config.ErrSupplierConfigInvalidStake.Wrapf("stake_amount MUST NOT be set in the supplier config YAML; it is automatically determined by the onchain MorseClaimableAccount state")
+	}
+
+	if err = yamlStakeConfig.ValidateAndNormalizeAddresses(logger.Logger); err != nil {
+		return nil, err
+	}
+
+	defaultRevShareMap, err := yamlStakeConfig.ValidateAndNormalizeDefaultRevShare()
+	if err != nil {
+		return nil, err
+	}
+
+	supplierServiceConfigs, err := yamlStakeConfig.ValidateAndParseServiceConfigs(defaultRevShareMap)
+	if err != nil {
+		return nil, err
+	}
+
+	return &config.SupplierStakeConfig{
+		OwnerAddress:    yamlStakeConfig.OwnerAddress,
+		OperatorAddress: yamlStakeConfig.OperatorAddress,
+		Services:        supplierServiceConfigs,
+		// StakeAmount:  (intentionally omitted),
path: x/migration/module/cmd/claim_morse_supplier.go
user: login: bryanchriswhite
body: ```suggestion
		// StakeAmount:  (intentionally omitted),
		// The stake amount is determined by the sum of any existing supplier stake
		// and the supplier stake amount of the associated MorseClaimableAccount.
```
created_at: 2025-04-02T14:48:08Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,203 @@
+package cmd
+
+import (
+	"bufio"
+	"encoding/json"
+	"fmt"
+	"os"
+
+	cosmosclient "github.com/cosmos/cosmos-sdk/client"
+	cosmosflags "github.com/cosmos/cosmos-sdk/client/flags"
+	"github.com/spf13/cobra"
+	"gopkg.in/yaml.v2"
+
+	"github.com/pokt-network/poktroll/cmd/flags"
+	"github.com/pokt-network/poktroll/cmd/logger"
+	"github.com/pokt-network/poktroll/x/migration/types"
+	"github.com/pokt-network/poktroll/x/supplier/config"
+)
+
+func ClaimSupplierCmd() *cobra.Command {
+	claimSupplierCmd := &cobra.Command{
+		Use:   "claim-supplier [morse_key_export_path] [path_to_supplier_stake_config] --from [shannon_dest_key_name]",
+		Args:  cobra.ExactArgs(2),
+		Short: "Claim an onchain MorseClaimableAccount as a staked supplier account",
+		Long: `Claim an onchain MorseClaimableAccount as a staked supplier account.
+
+The unstaked balance amount of the onchain MorseClaimableAccount will be minted to the Shannon account specified by the --from flag.
+The Shannon account will also be staked as a supplier with a stake equal to the supplier stake the MorseClaimableAccount had on Morse.
+
+This will construct, sign, and broadcast a tx containing a MsgClaimMorseSupplier message.
+
+For more information, see: https://dev.poktroll.com/operate/morse_migration/claiming`,
+		RunE:    runClaimSupplier,
+		PreRunE: logger.PreRunESetup,
+	}
+
+	// Add a string flag for providing a passphrase to decrypt the Morse keyfile.
+	claimSupplierCmd.Flags().StringVarP(
+		&morseKeyfileDecryptPassphrase,
+		flags.FlagPassphrase,
+		flags.FlagPassphraseShort,
+		"",
+		flags.FlagPassphraseUsage,
+	)
+
+	// Add a bool flag indicating whether to skip the passphrase prompt.
+	claimSupplierCmd.Flags().BoolVar(
+		&noPassphrase,
+		flags.FlagNoPassphrase,
+		false,
+		flags.FlagNoPassphraseUsage,
+	)
+
+	// This command depends on the conventional cosmos-sdk CLI tx flags.
+	cosmosflags.AddTxFlagsToCmd(claimSupplierCmd)
+
+	return claimSupplierCmd
+}
+
+// runClaimSupplier performs the following sequence:
+// - Load the Morse private key from the morse_key_export_path argument (arg 0).
+// - Load and validate the supplier service staking config from the shannon_service_id argument (arg 1).
+// - Sign and broadcast the MsgClaimMorseSupplier message using the Shannon key named by the `--from` flag.
+// - Wait until the tx is committed onchain for either a synchronous or asynchronous error.
+func runClaimSupplier(cmd *cobra.Command, args []string) error {
+	ctx := cmd.Context()
+
+	// Retrieve and validate the morse key based on the first argument provided.
+	morseKeyExportPath := args[0]
+	morsePrivKey, err := loadMorsePrivateKey(morseKeyExportPath, morseKeyfileDecryptPassphrase)
+	if err != nil {
+		return err
+	}
+
+	// Conventionally derive a cosmos-sdk client context from the cobra command.
+	clientCtx, err := cosmosclient.GetClientTxContext(cmd)
+	if err != nil {
+		return err
+	}
+
+	// Load the supplier stake config from the YAML file.
+	supplierStakeConfigPath := args[1]
+	supplierStakeConfig, err := loadSupplierStakeConfigYAML(supplierStakeConfigPath)
+	if err != nil {
+		return err
+	}
+
+	// Ensure that the signing account matches either the configured owner or operator address.
+	signingAddr := clientCtx.GetFromAddress().String()
+	ownerAddr := supplierStakeConfig.OwnerAddress
+	operatorAddr := supplierStakeConfig.OperatorAddress
+	switch signingAddr {
+	case ownerAddr, operatorAddr:
+		// All good.
+	default:
+		return fmt.Errorf(
+			"signer address %s does not match owner address %s or supplier operator address %s",
+			signingAddr, ownerAddr, operatorAddr,
+		)
+	}
+
+	// Construct a MsgClaimMorseSupplier message.
+	msgClaimMorseSupplier, err := types.NewMsgClaimMorseSupplier(
+		ownerAddr,
+		operatorAddr,
+		morsePrivKey.PubKey().Address().String(),
+		morsePrivKey,
+		supplierStakeConfig.Services,
+	)
+	if err != nil {
+		return err
+	}
+
+	// Serialize, as JSON, and print the MsgClaimMorseSupplier for posterity and/or confirmation.
+	msgClaimMorseSupplierJSON, err := json.MarshalIndent(msgClaimMorseSupplier, "", "  ")
+	if err != nil {
+		return err
+	}
+
+	fmt.Printf("MsgClaimMorseSupplier %s\n", string(msgClaimMorseSupplierJSON))
+
+	// Last chance for the user to abort.
+	skipConfirmation, err := cmd.Flags().GetBool(cosmosflags.FlagSkipConfirmation)
+	if err != nil {
+		return err
+	}
+
+	if !skipConfirmation {
+		fmt.Printf("Confirm MsgClaimMorseSupplier: y/[n]: ")
+		stdinReader := bufio.NewReader(os.Stdin)
+
+		// This call to ReadLine() will block until the user sends a new line to stdin.
+		inputLine, _, readErr := stdinReader.ReadLine()
+		if readErr != nil {
+			return err
+		}
+
+		// Terminate the confirmation prompt output line.
+		fmt.Println()
+
+		// Abort unless some affirmative confirmation is given.
+		switch string(inputLine) {
+		case "Yes", "yes", "Y", "y":
+		default:
+			return nil
+		}
+	}
+
+	// Construct a tx client.
+	txClient, err := flags.GetTxClient(ctx, cmd)
+	if err != nil {
+		return err
+	}
+
+	// Sign and broadcast the claim Morse account message.
+	eitherErr := txClient.SignAndBroadcast(ctx, msgClaimMorseSupplier)
+	err, errCh := eitherErr.SyncOrAsyncError()
+	if err != nil {
+		return err
+	}
+
+	// Wait for an async error, timeout, or the errCh to close on success.
+	return <-errCh
+}
+
+// loadSupplierStakeConfigYAML loads, parses, and validates the supplier stake
+// config from configYAMLPath.
+func loadSupplierStakeConfigYAML(configYAMLPath string) (*config.SupplierStakeConfig, error) {
+	yamlStakeConfigBz, err := os.ReadFile(configYAMLPath)
+	if err != nil {
+		return nil, err
+	}
+
+	var yamlStakeConfig config.YAMLStakeConfig
+	if err = yaml.Unmarshal(yamlStakeConfigBz, &yamlStakeConfig); err != nil {
+		return nil, err
+	}
+
+	if len(yamlStakeConfig.StakeAmount) != 0 {
path: x/migration/module/cmd/claim_morse_supplier.go
user: login: bryanchriswhite
body: The operation was a typo, this should be:
```suggestion
	// Validate that the stake amount is not set in the YAML config.
	if len(yamlStakeConfig.StakeAmount) != 0 {
		return nil, config.ErrSupplierConfigInvalidStake.Wrapf("stake_amount MUST NOT be set in the supplier config YAML; it is automatically determined by the onchain MorseClaimableAccount state")
	}
```
created_at: 2025-04-02T14:50:33Z
author_association: COLLABORATOR
diff_hunk: @@ -326,7 +398,15 @@ func (s *migrationSuite) TheShannonServiceConfigMatchesTheOneProvidedWhenClaimin
 		foundApp := s.getApplicationInfo(s.getShannonKeyName())
 		require.Equal(s, s.claimedActorServiceId, foundApp.GetServiceConfigs()[0].GetServiceId())
 	case actorTypeSupplier:
-		s.Skip("TODO_MAINNET_CRITICAL(@bryanchriswhite, #1034): Implement.")
+		effectiveServiceHeight := s.expectedSupplierEffectiveServiceHeight
+		s.Logf("waiting for effective service height: %d", effectiveServiceHeight)
+		s.waitForBlockHeight(effectiveServiceHeight)
+		foundSupplier := s.getSupplierInfo(s.getShannonKeyName())
+
+		// TODO_IN_THIS_PR: Ensure that the provided staking configs reflect what's onchain
path: e2e/tests/migration_steps_test.go
user: login: github-actions[bot]
body: **[linter-name (fail-on-found)]** <sub>reported by [reviewdog](https://github.com/reviewdog/reviewdog) :dog:</sub><br>		// TODO_IN_THIS_PR: Ensure that the provided staking configs reflect what's onchain
created_at: 2025-04-03T09:29:24Z
author_association: NONE
diff_hunk: @@ -326,7 +398,15 @@ func (s *migrationSuite) TheShannonServiceConfigMatchesTheOneProvidedWhenClaimin
 		foundApp := s.getApplicationInfo(s.getShannonKeyName())
 		require.Equal(s, s.claimedActorServiceId, foundApp.GetServiceConfigs()[0].GetServiceId())
 	case actorTypeSupplier:
-		s.Skip("TODO_MAINNET_CRITICAL(@bryanchriswhite, #1034): Implement.")
+		effectiveServiceHeight := s.expectedSupplierEffectiveServiceHeight
+		s.Logf("waiting for effective service height: %d", effectiveServiceHeight)
+		s.waitForBlockHeight(effectiveServiceHeight)
+		foundSupplier := s.getSupplierInfo(s.getShannonKeyName())
+
+		// TODO_IN_THIS_PR: Ensure that the provided staking configs reflect what's onchain
+		// TODO_IN_THIS_PR: Ensure that if --from is already a staked supplier, onchain returns an error if claiming for the same account
path: e2e/tests/migration_steps_test.go
user: login: github-actions[bot]
body: **[linter-name (fail-on-found)]** <sub>reported by [reviewdog](https://github.com/reviewdog/reviewdog) :dog:</sub><br>		// TODO_IN_THIS_PR: Ensure that if --from is already a staked supplier, onchain returns an error if claiming for the same account
created_at: 2025-04-03T09:29:25Z
author_association: NONE

PR comments:
user: login: bryanchriswhite
created_at: 2025-03-28T09:16:41Z
body: ![image](https://github.com/user-attachments/assets/6fac84ff-83aa-469f-b98f-fcc701ac743d)

author_association: COLLABORATOR
user: login: bryanchriswhite
created_at: 2025-04-02T13:50:44Z
body: > Haven't reviewed `e2e/tests/migration_steps_test.go` yet but pushed a few small NITs here.
> 
> @bryanchriswhite Can we take this as an opportunity (I just don't have time myself) to ensure that onchain fails claiming if the actor is already staked. E.g.
> 
> * Shannon Addr A is staked as a supplier with 1000 POKT
> * Morse Addr B tries to claim as A with some value (less or greater than 1000)
> * What happens then?
> 
> Apoligize if I'm not being as clear as I usually am but appreciate if you can fill in the gaps/details.

**TL;DR:**
**1. Stakes of existing actors are incremented by the claimed stake amounts.**
**2. Service configuration(s) are overwritten (in accordance with existing re/up-staking timing logic).**

Currently, a Morse application/supplier CAN be claimed to a Shannon account which is already staked as either actor. The migration fixture feature exercises this for both existing applications and suppliers. Stakes of existing actors are incremented by the claimed stake amounts and service configuration(s) are overwritten (in accordance with existing re/up-staking timing logic):

```gherkin
  Rule: Actor (re-)stake claims MAY reference existing Shannon actors

    ...

    Scenario Outline: Morse account-holder claims as an existing staked actor
      Given the Shannon account is funded with "1234567upokt"
      And the Shannon destination account is staked as an "<actor>" with "1230000" uPOKT for "anvil" service
      When the Morse private key is used to claim a MorseClaimableAccount as an "<actor>" for "ollama" service
      Then the Shannon destination account balance is increased by the unstaked balance amount of the MorseClaimableAccount
      And the Shannon destination account is staked as an "<actor>"
      And the Shannon "<actor>" stake increased by the corresponding actor stake amount of the MorseClaimableAccount
      And the Shannon "<actor>" service config matches the one provided when claiming the MorseClaimableAccount

      Examples:
        | actor       |
        | application |
        | supplier    |
```
author_association: COLLABORATOR
user: login: github-actions[bot]
created_at: 2025-04-03T10:29:12Z
body: The CI will now also run the e2e tests on devnet, which increases the time it takes to complete all CI checks.

You may need to run `make trigger_ci` to submit an empty commit that'll trigger the tests.

[GCP workloads (requires changing the namespace to 1153)](https://console.cloud.google.com/kubernetes/workload/overview?project=protocol-us-central1-d505&pageState=(%22savedViews%22:(%22c%22:%5B%5D,%22n%22:%5B%22devnet-issue-1153%22%5D)))
[Grafana network dashboard for devnet-issue-1153](https://grafana.poktroll.com/d/b799a130-3789-416d-aa7f-de5f4599cf03/network-overview?orgId=1&var-namespace=devnet-issue-1153)
author_association: NONE