Skip to content

Commit

Permalink
[Application] feat: app stake transfer (#743)
Browse files Browse the repository at this point in the history
## Summary

- Add `MsgTransferApplicationStake`.
- Implement application stake transfer to new "beneficiary" application
(unstakes original application).

## Dependents

- #788
- #789

## Issue

- #657

## Type of change

Select one or more:

- [x] New feature, functionality or library
- [ ] Bug fix
- [ ] Code health or cleanup
- [ ] Documentation
- [ ] Other (specify)

## Testing

**Documentation changes** (only if making doc changes)
- [ ] `make docusaurus_start`; only needed if you make doc changes

**Local Testing** (only if making code changes)
- [x] **Unit Tests**: `make go_develop_and_test`
- [x] **LocalNet E2E Tests**: `make test_e2e`
- See [quickstart
guide](https://dev.poktroll.com/developer_guide/quickstart) for
instructions

**PR Testing** (only if making code changes)
- [x] **DevNet E2E Tests**: Add the `devnet-test-e2e` label to the PR.
- **THIS IS VERY EXPENSIVE**, so only do it after all the reviews are
complete.
- Optionally run `make trigger_ci` if you want to re-trigger tests
without any code changes
- If tests fail, try re-running failed tests only using the GitHub UI as
shown
[here](https://github.com/pokt-network/poktroll/assets/1892194/607984e9-0615-4569-9452-4c730190c1d2)


## Sanity Checklist

- [x] I have tested my changes using the available tooling
- [x] I have commented my code
- [x] I have performed a self-review of my own code; both comments &
source code
- [ ] I create and reference any new tickets, if applicable
- [x] I have left TODOs throughout the codebase, if applicable

---------

Co-authored-by: Daniel Olshansky <[email protected]>
  • Loading branch information
bryanchriswhite and Olshansk authored Sep 23, 2024
1 parent f7b5794 commit bad0e8a
Show file tree
Hide file tree
Showing 15 changed files with 2,196 additions and 206 deletions.
1,347 changes: 1,202 additions & 145 deletions api/poktroll/application/tx.pulsar.go

Large diffs are not rendered by default.

48 changes: 43 additions & 5 deletions api/poktroll/application/tx_grpc.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions e2e/tests/stake_app_transfer.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Feature: App Stake Transfer Namespace

Scenario: User can transfer Application stake to non-existing application address
Given the user has the pocketd binary installed
# Unstake the applications in case they're already staked.
And this test ensures the "application" for account "app2" is not staked
And this test ensures the "application" for account "app3" is not staked
# Stake with 1 uPOKT more than the current stake used in genesis to make
# the transaction succeed.
And the account "app2" has a balance greater than "1000070" uPOKT
And an account exists for "app3"
And the user successfully stakes a "application" with "1000070" uPOKT for "anvil" service from the account "app2"
When the user transfers the "application" stake from account "app2" to account "app3"
Then the user should be able to see standard output containing "txhash:"
And the user should be able to see standard output containing "code: 0"
And the pocketd binary should exit without error
And the user should wait for the "application" module "TransferApplicationStake" message to be submitted
And the "application" for account "app3" is staked with "1000070" uPOKT
And the account balance of "app3" should be "0" uPOKT "less" than before
And the user verifies the "application" for account "app2" is not staked
And the account balance of "app2" should be "0" uPOKT "more" than before
51 changes: 51 additions & 0 deletions e2e/tests/stake_app_transfer_steps_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//go:build e2e

package e2e

import (
"github.com/stretchr/testify/require"
)

func (s *suite) TheUserSuccessfullyStakesAWithUpoktForServiceFromTheAccount(actorType string, amount int64, serviceId, accName string) {
s.TheUserStakesAWithUpoktForServiceFromTheAccount(actorType, amount, serviceId, accName)
s.TheUserShouldBeAbleToSeeStandardOutputContaining("txhash:")
s.TheUserShouldBeAbleToSeeStandardOutputContaining("code: 0")
s.ThePocketdBinaryShouldExitWithoutError()
s.TheUserShouldWaitForTheModuleMessageToBeSubmitted("application", "StakeApplication")
s.TheForAccountIsStakedWithUpokt(actorType, accName, amount)
s.TheAccountBalanceOfShouldBeUpoktThanBefore(accName, amount, "less")
}

func (s *suite) TheUserTransfersTheStakeFromAccountToAccount(actorType, fromAccName, toAccName string) {
fromAddr, fromAddrIsFound := accNameToAddrMap[fromAccName]
require.Truef(s, fromAddrIsFound, "account name %s not found in accNameToAddrMap", fromAccName)

toAddr, toAddrIsFound := accNameToAddrMap[toAccName]
require.Truef(s, toAddrIsFound, "account name %s not found in accNameToAddrMap", toAccName)

args := []string{
"tx",
actorType,
"transfer",
fromAddr,
toAddr,
"--from",
fromAccName,
keyRingFlag,
chainIdFlag,
"-y",
}
res, err := s.pocketd.RunCommandOnHost("", args...)
require.NoError(s, err)

s.pocketd.result = res
}

// This helper ensures that the actor is unstaked if it was staked when this step ran.
func (s *suite) ThisTestEnsuresTheForAccountIsNotStaked(actorType, accName string) {
if _, ok := s.getStakedAmount(actorType, accName); ok {
s.TheUserUnstakesAFromTheAccount(actorType, accName)
s.TheUserShouldBeAbleToSeeStandardOutputContaining("txhash:")
s.TheUserShouldBeAbleToSeeStandardOutputContaining("code: 0")
}
}
36 changes: 24 additions & 12 deletions proto/poktroll/application/tx.proto
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
syntax = "proto3";

package poktroll.application;

option go_package = "github.com/pokt-network/poktroll/x/application/types";
Expand All @@ -9,27 +10,27 @@ import "cosmos_proto/cosmos.proto";
import "cosmos/base/v1beta1/coin.proto";
import "cosmos/msg/v1/msg.proto";
import "gogoproto/gogo.proto";

import "poktroll/application/types.proto";
import "poktroll/application/params.proto";
import "poktroll/shared/service.proto";


// Msg defines the Msg service.
service Msg {
option (cosmos.msg.v1.service) = true;

// UpdateParams defines a (governance) operation for updating the module
// parameters. The authority defaults to the x/gov module account.
rpc UpdateParams (MsgUpdateParams ) returns (MsgUpdateParamsResponse );
rpc StakeApplication (MsgStakeApplication ) returns (MsgStakeApplicationResponse );
rpc UnstakeApplication (MsgUnstakeApplication ) returns (MsgUnstakeApplicationResponse );
rpc DelegateToGateway (MsgDelegateToGateway ) returns (MsgDelegateToGatewayResponse );
rpc UndelegateFromGateway (MsgUndelegateFromGateway) returns (MsgUndelegateFromGatewayResponse);
rpc UpdateParams (MsgUpdateParams) returns (MsgUpdateParamsResponse);
rpc StakeApplication (MsgStakeApplication) returns (MsgStakeApplicationResponse);
rpc UnstakeApplication (MsgUnstakeApplication) returns (MsgUnstakeApplicationResponse);
rpc DelegateToGateway (MsgDelegateToGateway) returns (MsgDelegateToGatewayResponse);
rpc UndelegateFromGateway (MsgUndelegateFromGateway) returns (MsgUndelegateFromGatewayResponse);
rpc TransferApplicationStake (MsgTransferApplicationStake) returns (MsgTransferApplicationStakeResponse);
}
// MsgUpdateParams is the Msg/UpdateParams request type.
message MsgUpdateParams {
option (cosmos.msg.v1.signer) = "authority";
option (amino.name) = "poktroll/x/application/MsgUpdateParams";
option (amino.name) = "poktroll/x/application/MsgUpdateParams";

// authority is the address that controls the module (defaults to x/gov unless overwritten).
string authority = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
Expand All @@ -49,10 +50,12 @@ message MsgUpdateParamsResponse {}

message MsgStakeApplication {
option (cosmos.msg.v1.signer) = "address";

string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the application.
cosmos.base.v1beta1.Coin stake = 2; // The total amount of uPOKT the application has staked. Must be ≥ to the current amount that the application has staked (if any)
string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the application.
cosmos.base.v1beta1.Coin stake = 2; // The total amount of uPOKT the application has staked. Must be ≥ to the current amount that the application has staked (if any)
repeated poktroll.shared.ApplicationServiceConfig services = 3; // The list of services this application is staked to request service for
// TODO_POST_MAINNET_CONSIDERATION: Consdier allowing appplications to delegate
// to gateways at time of staking for a better developer experience.
// repeated string gateway_addresss
}

message MsgStakeApplicationResponse {}
Expand All @@ -68,7 +71,6 @@ message MsgDelegateToGateway {
option (cosmos.msg.v1.signer) = "app_address"; // https://docs.cosmos.network/main/build/building-modules/messages-and-queries
string app_address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the application.
string gateway_address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the gateway the application wants to delegate to.

}

message MsgDelegateToGatewayResponse {}
Expand All @@ -81,3 +83,13 @@ message MsgUndelegateFromGateway {

message MsgUndelegateFromGatewayResponse {}

message MsgTransferApplicationStake {
option (cosmos.msg.v1.signer) = "source_address";
string source_address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
string destination_address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
}

message MsgTransferApplicationStakeResponse {
poktroll.application.Application application = 1;
}

68 changes: 68 additions & 0 deletions x/application/keeper/msg_server_transfer_application_stake.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package keeper

import (
"context"
"fmt"

"github.com/pokt-network/poktroll/telemetry"
"github.com/pokt-network/poktroll/x/application/types"
)

// TransferApplicationStake transfers the stake (held in escrow in the application
// module account) from a source to a (new) destination application account .
func (k msgServer) TransferApplicationStake(ctx context.Context, msg *types.MsgTransferApplicationStake) (*types.MsgTransferApplicationStakeResponse, error) {
isSuccessful := false
defer telemetry.EventSuccessCounter(
"transfer_application_stake",
telemetry.DefaultCounterFn,
func() bool { return isSuccessful },
)

logger := k.Logger().With("method", "TransferApplicationStake")

if err := msg.ValidateBasic(); err != nil {
return nil, err
}

// Ensure destination application does not already exist.
_, isDstFound := k.GetApplication(ctx, msg.GetDestinationAddress())
if isDstFound {
return nil, types.ErrAppDuplicateAddress.Wrapf("destination application (%q) exists", msg.GetDestinationAddress())
}

// Ensure source application exists.
srcApp, isAppFound := k.GetApplication(ctx, msg.GetSourceAddress())
if !isAppFound {
return nil, types.ErrAppNotFound.Wrapf("source application %q not found", msg.GetSourceAddress())
}

// Ensure source application is not already unbonding.
// TODO_TEST: Add E2E coverage to assert that an unbonding app cannot be transferred.
if srcApp.IsUnbonding() {
return nil, types.ErrAppIsUnstaking.Wrapf("cannot transfer stake of unbonding source application %q", msg.GetSourceAddress())
}

// Create a new application derived from the source application.
dstApp := srcApp
dstApp.Address = msg.GetDestinationAddress()

// TODO_NEXT(#789): Reconcile app unbonding logic with the new transfer stake logic.
// I.e., the source should not immediately be transferred.

// TODO_TEST: add E2E coverage to assert #DelegateeGatewayAddresses and #PendingUndelegations
// are present and correct on the dstApp application.

// Update the dstApp in the store
k.SetApplication(ctx, dstApp)
logger.Info(fmt.Sprintf("Successfully transferred application stake from (%s) to (%s)", srcApp.Address, dstApp.Address))

// Remove the transferred app from the store
k.RemoveApplication(ctx, srcApp.GetAddress())
logger.Info(fmt.Sprintf("Successfully removed the application: %+v", srcApp))

isSuccessful = true

return &types.MsgTransferApplicationStakeResponse{
Application: &dstApp,
}, nil
}
Loading

0 comments on commit bad0e8a

Please sign in to comment.