Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PoE staking query plugin #170

Merged
merged 6 commits into from
Nov 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,8 @@ func NewTgradeApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest
// TODO: add tgrade here soon
supportedFeatures := "staking,stargate,iterator"

wasmOpts = append(SetupWasmHandlers(appCodec, app.bankKeeper, govRouter, &app.twasmKeeper, &app.poeKeeper), wasmOpts...)

stakingAdapter := stakingKeeper
app.twasmKeeper = twasmkeeper.NewKeeper(
appCodec,
Expand Down
46 changes: 46 additions & 0 deletions app/wasm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package app

import (
"github.com/CosmWasm/wasmd/x/wasm"
wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper"
wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types"
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"

poewasm "github.com/confio/tgrade/x/poe/wasm"
twasmkeeper "github.com/confio/tgrade/x/twasm/keeper"
twasmtypes "github.com/confio/tgrade/x/twasm/types"
)

func SetupWasmHandlers(cdc codec.Marshaler,
bankKeeper twasmtypes.BankKeeper,
govRouter govtypes.Router,
result twasmkeeper.TgradeWasmHandlerKeeper,
poeKeeper poewasm.ViewKeeper,
) []wasmkeeper.Option {
queryPluginOpt := wasmkeeper.WithQueryPlugins(&wasmkeeper.QueryPlugins{
Staking: poewasm.StakingQuerier(poeKeeper),
})

extMessageHandlerOpt := wasmkeeper.WithMessageHandlerDecorator(func(nested wasmkeeper.Messenger) wasmkeeper.Messenger {
return wasmkeeper.NewMessageHandlerChain(
// disable staking messages
wasmkeeper.MessageHandlerFunc(func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
alpe marked this conversation as resolved.
Show resolved Hide resolved
if msg.Staking != nil {
return nil, nil, sdkerrors.Wrap(wasmtypes.ErrExecuteFailed, "not supported, yet")
}
return nil, nil, wasmtypes.ErrUnknownMsg
}),
nested,
// append our custom message handler
twasmkeeper.NewTgradeHandler(cdc, result, bankKeeper, govRouter),
)
})
return []wasm.Option{
queryPluginOpt,
extMessageHandlerOpt,
}
}
5 changes: 0 additions & 5 deletions x/poe/contract/tg4_stake.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package contract

import (
"encoding/json"
"testing"
"time"

stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
Expand All @@ -28,10 +27,6 @@ type TG4StakeInitMsg struct {
Preauths uint64 `json:"preauths,omitempty"`
}

func (m TG4StakeInitMsg) Json(t *testing.T) string {
return asJson(t, m)
}

// TG4StakeExecute staking contract execute messages
// See https://github.com/confio/tgrade-contracts/blob/v0.5.0-alpha/contracts/tg4-stake/src/msg.rs
type TG4StakeExecute struct {
Expand Down
60 changes: 60 additions & 0 deletions x/poe/contract/tg4_stake_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,66 @@ func TestQueryUnbondingPeriod(t *testing.T) {
assert.Equal(t, configuredTime, res)
}

func TestQueryStakedAmount(t *testing.T) {
// setup contracts and seed some data
ctx, example, _ := setupPoEContracts(t)
contractKeeper := example.TWasmKeeper.GetContractKeeper()
stakingContractAddr, err := example.PoEKeeper.GetPoEContractAddress(ctx, types.PoEContractTypeStaking)
require.NoError(t, err)
contractAdapter := contract.NewStakeContractAdapter(stakingContractAddr, example.TWasmKeeper, nil)

// fund account
var myOperatorAddr sdk.AccAddress = rand.Bytes(sdk.AddrLen)
example.BankKeeper.SetBalances(ctx, myOperatorAddr, sdk.NewCoins(sdk.NewCoin(types.DefaultBondDenom, sdk.NewInt(100))))

var oneInt = sdk.OneInt()
specs := map[string]struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good set of queries and high-level integration test for the contracts

addr sdk.AccAddress
expAmount *sdk.Int
setup func(ctx sdk.Context)
expErr bool
}{
"address has staked amount": {
addr: myOperatorAddr,
setup: func(ctx sdk.Context) {
err := contract.BondDelegation(ctx, stakingContractAddr, myOperatorAddr, sdk.NewCoins(sdk.NewCoin("utgd", sdk.OneInt())), contractKeeper)
require.NoError(t, err)
},
expAmount: &oneInt,
},
"address had formerly staked amount": {
addr: myOperatorAddr,
setup: func(ctx sdk.Context) {
err := contract.BondDelegation(ctx, stakingContractAddr, myOperatorAddr, sdk.NewCoins(sdk.NewCoin("utgd", sdk.OneInt())), contractKeeper)
require.NoError(t, err)
err = contract.UnbondDelegation(ctx, stakingContractAddr, myOperatorAddr, sdk.OneInt(), contractKeeper)
require.NoError(t, err)
},
expAmount: nil,
},
"unknown address": {
addr: rand.Bytes(sdk.AddrLen),
setup: func(ctx sdk.Context) {},
expAmount: nil,
},
}
for name, spec := range specs {
t.Run(name, func(t *testing.T) {
tCtx, _ := ctx.CacheContext()
spec.setup(tCtx)
// when
gotAmount, gotErr := contractAdapter.QueryStakedAmount(tCtx, spec.addr)
// then
if spec.expErr {
require.Error(t, gotErr)
return
}
require.NoError(t, gotErr)
assert.Equal(t, spec.expAmount, gotAmount, "exp %s but got %s", spec.expAmount, gotAmount)
})
}
}

func TestQueryValidatorUnboding(t *testing.T) {
// setup contracts and seed some data
ctx, example, vals := setupPoEContracts(t)
Expand Down
2 changes: 1 addition & 1 deletion x/poe/contract/tgrade_valset.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ type ValsetEpochResponse struct {
// The last time we updated the validator set - block height
LastUpdateHeight uint64 `json:"last_update_height"`
// TODO: add this if you want it, not in current code
/// Seconds (UTC UNIX time) of next timestamp that will trigger a validator recalculation
// Seconds (UTC UNIX time) of next timestamp that will trigger a validator recalculation
//NextUpdateTime int `json:"next_update_time"`
}

Expand Down
3 changes: 3 additions & 0 deletions x/poe/keeper/contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
)

type DistributionContract interface {
// ValidatorOutstandingReward returns amount or 0 for an unknown address
ValidatorOutstandingReward(ctx sdk.Context, addr sdk.AccAddress) (sdk.Coin, error)
}

Expand All @@ -31,8 +32,10 @@ func (k Keeper) ValsetContract(ctx sdk.Context) ValsetContract {
}

type StakeContract interface {
// QueryStakedAmount returns amount in default denom or nil value for an unknown address
QueryStakedAmount(ctx sdk.Context, opAddr sdk.AccAddress) (*sdk.Int, error)
QueryStakingUnbondingPeriod(ctx sdk.Context) (time.Duration, error)
// QueryStakingUnbonding returns the unbondings or empty list for an unknown address
QueryStakingUnbonding(ctx sdk.Context, opAddr sdk.AccAddress) ([]stakingtypes.UnbondingDelegationEntry, error)
}

Expand Down
2 changes: 1 addition & 1 deletion x/poe/keeper/poetesting/mock_contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (m ValsetContractMock) QueryConfig(ctx types.Context) (*contract.ValsetConf
return m.QueryConfigFn(ctx)
}

//var _ keeper.StakeContract = StakeContractMock{}
// var _ keeper.StakeContract = StakeContractMock{}

type StakeContractMock struct {
QueryStakingUnbondingPeriodFn func(ctx types.Context) (time.Duration, error)
Expand Down
25 changes: 24 additions & 1 deletion x/poe/keeper/test_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import (
"testing"
"time"

wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types"
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"

"github.com/CosmWasm/wasmd/x/wasm"
wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper"
"github.com/CosmWasm/wasmd/x/wasm/keeper/wasmtesting"
Expand Down Expand Up @@ -202,7 +206,26 @@ func createTestInput(

stakingAdapter := stakingadapter.NewStakingAdapter(nil, nil)
twasmSubspace := paramsKeeper.Subspace(twasmtypes.DefaultParamspace)
twasmKeeper := twasmkeeper.NewKeeper(

var twasmKeeper twasmkeeper.Keeper
handler := wasmkeeper.WithMessageHandlerDecorator(func(nested wasmkeeper.Messenger) wasmkeeper.Messenger {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to ensure this is in sync with the version in app/wasm.go, then maybe we just pull this decoration into a function in app/wasm.go and call the same wrapping function from the test code.

Copy link
Contributor Author

@alpe alpe Nov 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point but we would have cyclic dependencies in app.
We don't need the full set of custom queries/messages here but only the twasm ones for genesis/ boostrap tests in this module.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. Cyclic dependencies are annoying and Go is not terribly good at handling them,

return wasmkeeper.NewMessageHandlerChain(
// disable staking messages
wasmkeeper.MessageHandlerFunc(func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
if msg.Staking != nil {
return nil, nil, sdkerrors.Wrap(wasmtypes.ErrExecuteFailed, "not supported, yet")
}
return nil, nil, wasmtypes.ErrUnknownMsg
}),
nested,
// append our custom message handler
twasmkeeper.NewTgradeHandler(appCodec, &twasmKeeper, bankKeeper, nil),
)
})

opts = append([]wasmkeeper.Option{handler}, opts...)

twasmKeeper = twasmkeeper.NewKeeper(
appCodec,
keyWasm,
twasmSubspace,
Expand Down
131 changes: 131 additions & 0 deletions x/poe/wasm/query_plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package wasm

import (
"encoding/json"

wasmvmtypes "github.com/CosmWasm/wasmvm/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"

"github.com/confio/tgrade/x/poe/keeper"
)

type ViewKeeper interface {
GetBondDenom(ctx sdk.Context) string
DistributionContract(ctx sdk.Context) keeper.DistributionContract
ValsetContract(ctx sdk.Context) keeper.ValsetContract
StakeContract(ctx sdk.Context) keeper.StakeContract
}

func StakingQuerier(poeKeeper ViewKeeper) func(ctx sdk.Context, request *wasmvmtypes.StakingQuery) ([]byte, error) {
return func(ctx sdk.Context, request *wasmvmtypes.StakingQuery) ([]byte, error) {
if request.BondedDenom != nil {
denom := poeKeeper.GetBondDenom(ctx)
res := wasmvmtypes.BondedDenomResponse{
Denom: denom,
}
return json.Marshal(res)
}
zero := sdk.ZeroDec().String()
if request.AllValidators != nil {
validators, err := poeKeeper.ValsetContract(ctx).ListValidators(ctx)
if err != nil {
return nil, err
}
wasmVals := make([]wasmvmtypes.Validator, len(validators))
for i, v := range validators {
wasmVals[i] = wasmvmtypes.Validator{
Address: v.OperatorAddress,
Commission: zero,
alpe marked this conversation as resolved.
Show resolved Hide resolved
MaxCommission: zero,
MaxChangeRate: zero,
}
}
res := wasmvmtypes.AllValidatorsResponse{
Validators: wasmVals,
}
return json.Marshal(res)
}
if request.Validator != nil {
valAddr, err := sdk.AccAddressFromBech32(request.Validator.Address)
if err != nil {
return nil, err
}
v, err := poeKeeper.ValsetContract(ctx).QueryValidator(ctx, valAddr)
if err != nil {
return nil, err
}
res := wasmvmtypes.ValidatorResponse{}
if v != nil {
res.Validator = &wasmvmtypes.Validator{
Address: v.OperatorAddress,
Commission: zero,
MaxCommission: zero,
MaxChangeRate: zero,
}
}
return json.Marshal(res)
}
if request.AllDelegations != nil {
delegator, err := sdk.AccAddressFromBech32(request.AllDelegations.Delegator)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, request.AllDelegations.Delegator)
}
stakedAmount, err := poeKeeper.StakeContract(ctx).QueryStakedAmount(ctx, delegator)
alpe marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}
var res wasmvmtypes.AllDelegationsResponse
if stakedAmount != nil {
res.Delegations = []wasmvmtypes.Delegation{{
Delegator: delegator.String(),
Validator: delegator.String(),
Amount: wasmvmtypes.NewCoin(stakedAmount.Uint64(), poeKeeper.GetBondDenom(ctx)),
}}
}
return json.Marshal(res)
}
if request.Delegation != nil {
delegator, err := sdk.AccAddressFromBech32(request.Delegation.Delegator)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, request.Delegation.Delegator)
}
validator, err := sdk.AccAddressFromBech32(request.Delegation.Validator)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, request.Delegation.Validator)
}

var res wasmvmtypes.DelegationResponse
if !delegator.Equals(validator) { // no match
return json.Marshal(res)
}
stakeContract := poeKeeper.StakeContract(ctx)
stakedAmount, err := stakeContract.QueryStakedAmount(ctx, delegator)
if err != nil {
return nil, sdkerrors.Wrap(err, "query staked amount")
}
reward, err := poeKeeper.DistributionContract(ctx).ValidatorOutstandingReward(ctx, delegator)
if err != nil {
return nil, sdkerrors.Wrap(err, "query outstanding reward")
}
if stakedAmount == nil {
zeroInt := sdk.ZeroInt()
stakedAmount = &zeroInt
}
// there can be unclaimed rewards while all stacked amounts were unbound
if stakedAmount.GT(sdk.ZeroInt()) || reward.Amount.GT(sdk.ZeroInt()) {
bondDenom := poeKeeper.GetBondDenom(ctx)
stakedCoin := wasmvmtypes.NewCoin(stakedAmount.Uint64(), bondDenom)
res.Delegation = &wasmvmtypes.FullDelegation{
Delegator: delegator.String(),
Validator: delegator.String(),
Amount: stakedCoin,
CanRedelegate: wasmvmtypes.NewCoin(0, bondDenom),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good

AccumulatedRewards: wasmvmtypes.Coins{wasmvmtypes.NewCoin(reward.Amount.Uint64(), reward.Denom)},
}
}
return json.Marshal(res)
}
return nil, wasmvmtypes.UnsupportedRequest{Kind: "unknown Staking variant"}
}
}
Loading