diff --git a/app/app.go b/app/app.go index 9010fddb..5bb4ed2d 100644 --- a/app/app.go +++ b/app/app.go @@ -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, diff --git a/app/wasm.go b/app/wasm.go new file mode 100644 index 00000000..d3f94d8d --- /dev/null +++ b/app/wasm.go @@ -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) { + 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, + } +} diff --git a/x/poe/contract/tg4_stake.go b/x/poe/contract/tg4_stake.go index 81d620da..24aff7a0 100644 --- a/x/poe/contract/tg4_stake.go +++ b/x/poe/contract/tg4_stake.go @@ -2,7 +2,6 @@ package contract import ( "encoding/json" - "testing" "time" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" @@ -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 { diff --git a/x/poe/contract/tg4_stake_integration_test.go b/x/poe/contract/tg4_stake_integration_test.go index 0785febe..c4a9e6f7 100644 --- a/x/poe/contract/tg4_stake_integration_test.go +++ b/x/poe/contract/tg4_stake_integration_test.go @@ -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 { + 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) diff --git a/x/poe/contract/tgrade_valset.go b/x/poe/contract/tgrade_valset.go index 055900f1..6d6202f5 100644 --- a/x/poe/contract/tgrade_valset.go +++ b/x/poe/contract/tgrade_valset.go @@ -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"` } diff --git a/x/poe/keeper/contracts.go b/x/poe/keeper/contracts.go index 9ce68119..8d54c10c 100644 --- a/x/poe/keeper/contracts.go +++ b/x/poe/keeper/contracts.go @@ -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) } @@ -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) } diff --git a/x/poe/keeper/poetesting/mock_contracts.go b/x/poe/keeper/poetesting/mock_contracts.go index 627adf50..e49c5603 100644 --- a/x/poe/keeper/poetesting/mock_contracts.go +++ b/x/poe/keeper/poetesting/mock_contracts.go @@ -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) diff --git a/x/poe/keeper/test_common.go b/x/poe/keeper/test_common.go index 2935cae3..196af72c 100644 --- a/x/poe/keeper/test_common.go +++ b/x/poe/keeper/test_common.go @@ -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" @@ -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 { + 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, diff --git a/x/poe/wasm/query_plugin.go b/x/poe/wasm/query_plugin.go new file mode 100644 index 00000000..8eb1fc96 --- /dev/null +++ b/x/poe/wasm/query_plugin.go @@ -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, + 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) + 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), + AccumulatedRewards: wasmvmtypes.Coins{wasmvmtypes.NewCoin(reward.Amount.Uint64(), reward.Denom)}, + } + } + return json.Marshal(res) + } + return nil, wasmvmtypes.UnsupportedRequest{Kind: "unknown Staking variant"} + } +} diff --git a/x/poe/wasm/query_plugin_test.go b/x/poe/wasm/query_plugin_test.go new file mode 100644 index 00000000..4d6549c5 --- /dev/null +++ b/x/poe/wasm/query_plugin_test.go @@ -0,0 +1,296 @@ +package wasm + +import ( + "testing" + + "github.com/confio/tgrade/x/twasm/types" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/confio/tgrade/x/poe/keeper" + "github.com/confio/tgrade/x/poe/keeper/poetesting" + poetypes "github.com/confio/tgrade/x/poe/types" +) + +func TestStakingQuerier(t *testing.T) { + t.Log(types.RandomBech32Address(t)) + specs := map[string]struct { + src wasmvmtypes.StakingQuery + mock ViewKeeper + expJson string + expErr bool + }{ + "bonded denum": { + src: wasmvmtypes.StakingQuery{BondedDenom: &struct{}{}}, + mock: ViewKeeperMock{GetBondDenomFn: func(ctx sdk.Context) string { + return "alx" + }}, + expJson: `{"denom": "alx"}`, + }, + "all validators - single": { + src: wasmvmtypes.StakingQuery{AllValidators: &wasmvmtypes.AllValidatorsQuery{}}, + mock: ViewKeeperMock{ValsetContractFn: func(ctx sdk.Context) keeper.ValsetContract { + return poetesting.ValsetContractMock{ + ListValidatorsFn: func(ctx sdk.Context) ([]stakingtypes.Validator, error) { + resp := []stakingtypes.Validator{ + poetypes.ValidatorFixture(func(m *stakingtypes.Validator) { + m.OperatorAddress = "myOperatorAddress" + }), + } + return resp, nil + }, + } + }}, + expJson: `{"validators":[{"address":"myOperatorAddress","commission":"0.000000000000000000","max_commission":"0.000000000000000000","max_change_rate":"0.000000000000000000"}]}`, + }, + "all validators - multiple": { + src: wasmvmtypes.StakingQuery{AllValidators: &wasmvmtypes.AllValidatorsQuery{}}, + mock: ViewKeeperMock{ValsetContractFn: func(ctx sdk.Context) keeper.ValsetContract { + return poetesting.ValsetContractMock{ + ListValidatorsFn: func(ctx sdk.Context) ([]stakingtypes.Validator, error) { + resp := []stakingtypes.Validator{ + poetypes.ValidatorFixture(func(m *stakingtypes.Validator) { + m.OperatorAddress = "myOperatorAddress" + }), + poetypes.ValidatorFixture(func(m *stakingtypes.Validator) { + m.OperatorAddress = "myOtherOperatorAddress" + }), + } + return resp, nil + }, + } + }}, + expJson: `{"validators":[ +{"address":"myOperatorAddress","commission":"0.000000000000000000","max_commission":"0.000000000000000000","max_change_rate":"0.000000000000000000"}, +{"address":"myOtherOperatorAddress","commission":"0.000000000000000000","max_commission":"0.000000000000000000","max_change_rate":"0.000000000000000000"} + ]}`, + }, + "query validator": { + src: wasmvmtypes.StakingQuery{Validator: &wasmvmtypes.ValidatorQuery{Address: "cosmos1yq8zt83jznmp94jkj65yvfz9n52akmxt52ehm3"}}, + mock: ViewKeeperMock{ValsetContractFn: func(ctx sdk.Context) keeper.ValsetContract { + return poetesting.ValsetContractMock{ + QueryValidatorFn: func(ctx sdk.Context, opAddr sdk.AccAddress) (*stakingtypes.Validator, error) { + val := poetypes.ValidatorFixture(func(m *stakingtypes.Validator) { + m.OperatorAddress = "cosmos1yq8zt83jznmp94jkj65yvfz9n52akmxt52ehm3" + }) + return &val, nil + }, + } + }}, + expJson: `{"validator":{"address":"cosmos1yq8zt83jznmp94jkj65yvfz9n52akmxt52ehm3","commission":"0.000000000000000000","max_commission":"0.000000000000000000","max_change_rate":"0.000000000000000000"}}`, + }, + "query validator - unknown address": { + src: wasmvmtypes.StakingQuery{Validator: &wasmvmtypes.ValidatorQuery{Address: "cosmos1yq8zt83jznmp94jkj65yvfz9n52akmxt52ehm3"}}, + mock: ViewKeeperMock{ValsetContractFn: func(ctx sdk.Context) keeper.ValsetContract { + return poetesting.ValsetContractMock{ + QueryValidatorFn: func(ctx sdk.Context, opAddr sdk.AccAddress) (*stakingtypes.Validator, error) { + return nil, nil + }, + } + }}, + expJson: `{"validator": null}`, + }, + "query validator - invalid address": { + src: wasmvmtypes.StakingQuery{Validator: &wasmvmtypes.ValidatorQuery{Address: "not a valid address"}}, + expErr: true, + }, + "all delegations": { + src: wasmvmtypes.StakingQuery{AllDelegations: &wasmvmtypes.AllDelegationsQuery{Delegator: "cosmos1yq8zt83jznmp94jkj65yvfz9n52akmxt52ehm3"}}, + mock: ViewKeeperMock{StakeContractFn: func(ctx sdk.Context) keeper.StakeContract { + return poetesting.StakeContractMock{ + QueryStakedAmountFn: func(ctx sdk.Context, opAddr sdk.AccAddress) (*sdk.Int, error) { + myValue := sdk.OneInt() + return &myValue, nil + }, + } + }, + GetBondDenomFn: func(ctx sdk.Context) string { + return "alx" + }}, + expJson: `{"delegations":[{"delegator":"cosmos1yq8zt83jznmp94jkj65yvfz9n52akmxt52ehm3","validator":"cosmos1yq8zt83jznmp94jkj65yvfz9n52akmxt52ehm3","amount":{"denom":"alx","amount":"1"}}]}`, + }, + "all delegations - unknown address": { + src: wasmvmtypes.StakingQuery{AllDelegations: &wasmvmtypes.AllDelegationsQuery{Delegator: "cosmos1yq8zt83jznmp94jkj65yvfz9n52akmxt52ehm3"}}, + mock: ViewKeeperMock{StakeContractFn: func(ctx sdk.Context) keeper.StakeContract { + return poetesting.StakeContractMock{ + QueryStakedAmountFn: func(ctx sdk.Context, opAddr sdk.AccAddress) (*sdk.Int, error) { + return nil, nil + }, + } + }}, + expJson: `{"delegations":[]}`, + }, + "all delegations - invalid address": { + src: wasmvmtypes.StakingQuery{AllDelegations: &wasmvmtypes.AllDelegationsQuery{Delegator: "not a valid address"}}, + expErr: true, + }, + "query delegation": { + src: wasmvmtypes.StakingQuery{Delegation: &wasmvmtypes.DelegationQuery{Delegator: "cosmos1yq8zt83jznmp94jkj65yvfz9n52akmxt52ehm3", Validator: "cosmos1yq8zt83jznmp94jkj65yvfz9n52akmxt52ehm3"}}, + mock: ViewKeeperMock{ + StakeContractFn: func(ctx sdk.Context) keeper.StakeContract { + return poetesting.StakeContractMock{ + QueryStakedAmountFn: func(ctx sdk.Context, opAddr sdk.AccAddress) (*sdk.Int, error) { + myValue := sdk.OneInt() + return &myValue, nil + }, + } + }, + GetBondDenomFn: func(ctx sdk.Context) string { + return "alx" + }, + DistributionContractFn: func(ctx sdk.Context) keeper.DistributionContract { + return poetesting.DistributionContractMock{ValidatorOutstandingRewardFn: func(ctx sdk.Context, addr sdk.AccAddress) (sdk.Coin, error) { + return sdk.NewCoin("alx", sdk.NewInt(2)), nil + }} + }, + }, + expJson: `{ + "delegation": { + "delegator": "cosmos1yq8zt83jznmp94jkj65yvfz9n52akmxt52ehm3", + "validator": "cosmos1yq8zt83jznmp94jkj65yvfz9n52akmxt52ehm3", + "amount": { + "denom": "alx", + "amount": "1" + }, + "accumulated_rewards": [ + { + "denom": "alx", + "amount": "2" + } + ], + "can_redelegate": { + "denom": "alx", + "amount": "0" + } + } +} +`, + }, + "query delegation - address do not match - return empty result": { + src: wasmvmtypes.StakingQuery{Delegation: &wasmvmtypes.DelegationQuery{Delegator: "cosmos1yq8zt83jznmp94jkj65yvfz9n52akmxt52ehm3", Validator: "cosmos17emnuddq662fpxpnd43ch0396452d48vc8ufsw"}}, + expJson: `{}`, + }, + "query delegation - unknown address return empty result": { + src: wasmvmtypes.StakingQuery{Delegation: &wasmvmtypes.DelegationQuery{Delegator: "cosmos1yq8zt83jznmp94jkj65yvfz9n52akmxt52ehm3", Validator: "cosmos1yq8zt83jznmp94jkj65yvfz9n52akmxt52ehm3"}}, + mock: ViewKeeperMock{ + StakeContractFn: func(ctx sdk.Context) keeper.StakeContract { + return poetesting.StakeContractMock{ + QueryStakedAmountFn: func(ctx sdk.Context, opAddr sdk.AccAddress) (*sdk.Int, error) { + return nil, nil + }, + } + }, + DistributionContractFn: func(ctx sdk.Context) keeper.DistributionContract { + return poetesting.DistributionContractMock{ValidatorOutstandingRewardFn: func(ctx sdk.Context, addr sdk.AccAddress) (sdk.Coin, error) { + return sdk.NewCoin("alx", sdk.ZeroInt()), nil + }} + }, + }, + expJson: `{}`, + }, + "query delegation - invalid delegator address": { + src: wasmvmtypes.StakingQuery{Delegation: &wasmvmtypes.DelegationQuery{Delegator: "not a valid address", Validator: "cosmos1yq8zt83jznmp94jkj65yvfz9n52akmxt52ehm3"}}, + expErr: true, + }, + "query delegation - invalid validator address": { + src: wasmvmtypes.StakingQuery{Delegation: &wasmvmtypes.DelegationQuery{Delegator: "cosmos1yq8zt83jznmp94jkj65yvfz9n52akmxt52ehm3", Validator: "not a valid address"}}, + expErr: true, + }, + "query delegation - no staking, pending rewards": { + src: wasmvmtypes.StakingQuery{Delegation: &wasmvmtypes.DelegationQuery{Delegator: "cosmos1yq8zt83jznmp94jkj65yvfz9n52akmxt52ehm3", Validator: "cosmos1yq8zt83jznmp94jkj65yvfz9n52akmxt52ehm3"}}, + mock: ViewKeeperMock{ + StakeContractFn: func(ctx sdk.Context) keeper.StakeContract { + return poetesting.StakeContractMock{ + QueryStakedAmountFn: func(ctx sdk.Context, opAddr sdk.AccAddress) (*sdk.Int, error) { + return nil, nil + }, + } + }, + GetBondDenomFn: func(ctx sdk.Context) string { + return "alx" + }, + DistributionContractFn: func(ctx sdk.Context) keeper.DistributionContract { + return poetesting.DistributionContractMock{ValidatorOutstandingRewardFn: func(ctx sdk.Context, addr sdk.AccAddress) (sdk.Coin, error) { + return sdk.NewCoin("alx", sdk.NewInt(2)), nil + }} + }, + }, + expJson: `{ + "delegation": { + "delegator": "cosmos1yq8zt83jznmp94jkj65yvfz9n52akmxt52ehm3", + "validator": "cosmos1yq8zt83jznmp94jkj65yvfz9n52akmxt52ehm3", + "amount": { + "denom": "alx", + "amount": "0" + }, + "accumulated_rewards": [ + { + "denom": "alx", + "amount": "2" + } + ], + "can_redelegate": { + "denom": "alx", + "amount": "0" + } + } +} +`, + }, + "unknown query": { + src: wasmvmtypes.StakingQuery{}, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + q := StakingQuerier(spec.mock) + gotRsp, gotErr := q(sdk.Context{}, &spec.src) + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + assert.JSONEq(t, spec.expJson, string(gotRsp), string(gotRsp)) + }) + } +} + +type ViewKeeperMock struct { + GetBondDenomFn func(ctx sdk.Context) string + DistributionContractFn func(ctx sdk.Context) keeper.DistributionContract + ValsetContractFn func(ctx sdk.Context) keeper.ValsetContract + StakeContractFn func(ctx sdk.Context) keeper.StakeContract +} + +func (m ViewKeeperMock) GetBondDenom(ctx sdk.Context) string { + if m.GetBondDenomFn == nil { + panic("not expected to be called") + } + return m.GetBondDenomFn(ctx) +} + +func (m ViewKeeperMock) DistributionContract(ctx sdk.Context) keeper.DistributionContract { + if m.DistributionContractFn == nil { + panic("not expected to be called") + } + return m.DistributionContractFn(ctx) +} + +func (m ViewKeeperMock) ValsetContract(ctx sdk.Context) keeper.ValsetContract { + if m.ValsetContractFn == nil { + panic("not expected to be called") + } + return m.ValsetContractFn(ctx) +} + +func (m ViewKeeperMock) StakeContract(ctx sdk.Context) keeper.StakeContract { + if m.StakeContractFn == nil { + panic("not expected to be called") + } + return m.StakeContractFn(ctx) +} diff --git a/x/twasm/keeper/handler_plugin.go b/x/twasm/keeper/handler_plugin.go index f6b7d5e4..360568e6 100644 --- a/x/twasm/keeper/handler_plugin.go +++ b/x/twasm/keeper/handler_plugin.go @@ -13,14 +13,16 @@ import ( "github.com/confio/tgrade/x/twasm/types" ) -// tgradeKeeper defines a subset of Keeper -type tgradeKeeper interface { +// TgradeWasmHandlerKeeper defines a subset of Keeper +type TgradeWasmHandlerKeeper interface { IsPrivileged(ctx sdk.Context, contract sdk.AccAddress) bool appendToPrivilegedContracts(ctx sdk.Context, privilegeType types.PrivilegeType, contractAddress sdk.AccAddress) (uint8, error) removePrivilegeRegistration(ctx sdk.Context, privilegeType types.PrivilegeType, pos uint8, contractAddr sdk.AccAddress) bool setContractDetails(ctx sdk.Context, contract sdk.AccAddress, details *types.TgradeContractDetails) error GetContractInfo(ctx sdk.Context, contractAddress sdk.AccAddress) *wasmtypes.ContractInfo } + +// minter is a subset of bank keeper type minter interface { MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error @@ -30,14 +32,14 @@ var _ wasmkeeper.Messenger = TgradeHandler{} // TgradeHandler is a custom message handler plugin for wasmd. type TgradeHandler struct { - keeper tgradeKeeper + keeper TgradeWasmHandlerKeeper minter minter govRouter govtypes.Router cdc codec.Marshaler } // NewTgradeHandler constructor -func NewTgradeHandler(cdc codec.Marshaler, keeper tgradeKeeper, bankKeeper minter, govRouter govtypes.Router) *TgradeHandler { +func NewTgradeHandler(cdc codec.Marshaler, keeper TgradeWasmHandlerKeeper, bankKeeper minter, govRouter govtypes.Router) *TgradeHandler { return &TgradeHandler{cdc: cdc, keeper: keeper, govRouter: govRouter, minter: bankKeeper} } diff --git a/x/twasm/keeper/handler_plugin_test.go b/x/twasm/keeper/handler_plugin_test.go index a2372604..a629ea58 100644 --- a/x/twasm/keeper/handler_plugin_test.go +++ b/x/twasm/keeper/handler_plugin_test.go @@ -601,7 +601,7 @@ func noopRegisterHook(m *handlerTgradeKeeperMock, mutators ...func(*wasmtypes.Co } } -var _ tgradeKeeper = handlerTgradeKeeperMock{} +var _ TgradeWasmHandlerKeeper = handlerTgradeKeeperMock{} type handlerTgradeKeeperMock struct { IsPrivilegedFn func(ctx sdk.Context, contract sdk.AccAddress) bool diff --git a/x/twasm/keeper/keeper.go b/x/twasm/keeper/keeper.go index 0dc5b332..0365c9ab 100644 --- a/x/twasm/keeper/keeper.go +++ b/x/twasm/keeper/keeper.go @@ -3,10 +3,8 @@ package keeper import ( "fmt" - "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" @@ -54,34 +52,6 @@ func NewKeeper( govRouter: govRouter, } // configure wasm keeper via options - - 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) { - 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 - NewTgradeHandler(cdc, &result, bankKeeper, govRouter), - ) - }) - extQueryHandlerOpt := wasmkeeper.WithQueryHandlerDecorator(func(nested wasmkeeper.WasmVMQueryHandler) wasmkeeper.WasmVMQueryHandler { - return wasmkeeper.WasmVMQueryHandlerFn(func(ctx sdk.Context, caller sdk.AccAddress, request wasmvmtypes.QueryRequest) ([]byte, error) { - if request.Staking != nil { - return nil, wasmvmtypes.UnsupportedRequest{Kind: "not supported, yet"} - } - return nested.HandleQuery(ctx, caller, request) - }) - }) - opts = append([]wasm.Option{ - extMessageHandlerOpt, - extQueryHandlerOpt, - }, opts...) - result.Keeper = wasmkeeper.NewKeeper( cdc, storeKey, diff --git a/x/twasm/keeper/test_common.go b/x/twasm/keeper/test_common.go index 064136d6..b0db5c1c 100644 --- a/x/twasm/keeper/test_common.go +++ b/x/twasm/keeper/test_common.go @@ -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" + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" "github.com/CosmWasm/wasmd/x/wasm/keeper/wasmtesting" "github.com/cosmos/cosmos-sdk/baseapp" @@ -257,7 +261,24 @@ func createTestInput( stakingtypes.RegisterQueryServer(querier, stakingkeeper.Querier{Keeper: stakingKeeper}) distributiontypes.RegisterQueryServer(querier, distKeeper) - keeper := NewKeeper( + var keeper Keeper + handler := 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) { + 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 + NewTgradeHandler(appCodec, &keeper, bankKeeper, nil), + ) + }) + + opts = append([]wasmkeeper.Option{handler}, opts...) + keeper = NewKeeper( appCodec, keyWasm, paramsKeeper.Subspace(types.DefaultParamspace),