Add fee grant module (#8061)

* Add docs

* Add BasicFeeAllowance implementation

* Add expiration structs and complete basic fee

* Add delegation messages, add validation logic

* Add keeper and helper structs

* Add alias and handler to top level

* Add delegation module

* Add basic querier

* Add types tests

* Add types tests

* More internal test coverage

* Solid internal test coverage

* Expose Querier to top level module

* Add FeeAccount to auth/types, like StdTx, SignDoc

* Fix all tests in x/auth

* All tests pass

* Appease the Golang Linter

* Add fee-account command line flag

* Start on DelegatedDeductFeeDecorator

* Cleanup the Decorator

* Wire up delegation module in simapp

* add basic test for decorator (no delegation)

* Table tests for deduct fees

* Table tests over all conditions of delegated fee decorator

* Build full ante handler stack and test it

* Start genesis

* Implement Genesis

* Rename package delegation to subkeys

* Clarify antes test cases, handle empty account w/o fees

* Allow paying delegated fees with no account

* Pull mempool into delegated ante, for control on StdFee

* Use custom DelegatedTx, DelegatedFee for subkeys

* Revert all changes to x/auth.StdTx

* Appease scopelint

* Register DelegatedTx with codec

* Address PR comments

* Remove unnecessary DelegatedMempoolFeeDecorator

* Cleaned up errors in querier

* Clean up message sign bytes

* Minor PR comments

* Replace GetAllFees... with Iterator variants

* PrepareForExport adjusts grant expiration height

* Panic on de/serialization error in keeper

* Move custom ante handler chain to tests, update docs

* More cleanup

* More doc cleanup

* Renamed subkeys module to fee_grant

* Rename subkeys/delegation to fee grant in all strings

* Modify Msg and Keeper methods to use Grant not Delegate

* Add PeriodicFeeAllowance

* Update aliases

* Cover all accept cases for PeriodicFeeAllowance

* Et tu scopelint?

* Update docs as requested

* Remove error return from GetFeeGrant

* Code cleanup as requested by PR

* Updated all errors to use new sdk/errors package

* Use test suite for keeper tests

* Clean up alias.go file

* Define expected interfaces in exported, rather than importing from account

* Remove dependency on auth/ante

* Improve godoc, Logger

* Cleaned up ExpiresAt

* Improve error reporting with UseGrantedFee

* Enforce period limit subset of basic limit

* Add events

* Rename fee_grant to feegrant

* Ensure KeeperTestSuite actually runs

* Move types/tx to types

* Update alias file, include ante

* I do need nolint in alias.go

* Properly emit events in the handler. Use cosmos-sdk in amino types

* Update godoc

* Linting...

* Update errors

* Update pkg doc and fix ante-handler order

* Merge PR #5782: Migrate x/feegrant to proto

* fix errors

* proto changes

* proto changes

* fix errors

* fix errors

* genesis state changed to proto

* fix keeper tests

* fix test

* fixed tests

* fix tests

* updated expected keepers

* updated ante tests

* lint

* deleted alias.go

* tx updated to proto tx

* remove explicit signmode

* tests

* Added `cli/query.go`

* Added tx.go in cli

* updated `module.go`

* resolve errors in tx.go

* Add fee payer gentx func

* updated tx

* fixed error

* WIP: cli tests

* fix query error

* fix tests

* Unused types and funcs

* fix tests

* rename helper func to create tx

* remove unused

* update tx cfg

* fix cli tests

* added simulations

* Add `decoder.go`

* fix build fail

* added init genesis code

* update tx.go

* fixed LGTM alert

* modified cli

* remove gogoproto extensions

* change acc address type to string

* lint

* fix simulations

* Add gen simulations

* remove legacy querier

* remove legacy code

* add grpc queries tests

* fix simulations

* update module.go

* lint

* register feegrant NewSimulationManager

* fix sims

* fix sims

* add genesis test

* add periodic grant

* updated cmd

* changed times

* updated flags

* removed days as period clock

* added condition for period and exp

* add periodic fee cli tests

* udpated tests

* fix lint

* fix tests

* fix sims

* renaming to `fee_grant`

* review changes

* fix test

* add condition for duplicate grants

* fix tests

* add `genTxWithFeeGranter` in tests

* fix simulation

* one of changes & test fixes

* fix test

* fix lint

* changed package name `feegrant` to `fee_grant`

* review comments

* review changes

* review change

* review changes

* added fee-account in flags

* address review changes

* read fee granter from cli

* updated create account with mnemonic

* Address review comments

* move `simapp/ante` file to `feegrant/ante`

* update keeper logic to create account

* update docs

* fix tests

* update `serviceMsgClientConn` from `msgservice`

* review changes

* add test case for using more fees than allowed

* eliminate panic checks from keeper

* fix lint

* change store keys string to bytes

* fix tests

* review changes

* review changes

* udpate docs

* make spend limit optional

* fix tests

* fix tests

* review changes

* add norace tag

* proto-docs

* add docs

Co-authored-by: Ethan Frey <ethanfrey@users.noreply.github.com>
Co-authored-by: Alexander Bezobchuk <alexanderbez@users.noreply.github.com>
Co-authored-by: Aleksandr Bezobchuk <aleks.bezobchuk@gmail.com>
Co-authored-by: SaReN <sahithnarahari@gmail.com>
Co-authored-by: aleem1413 <aleem@vitwit.com>
Co-authored-by: MD Aleem <72057206+aleem1314@users.noreply.github.com>
Co-authored-by: Anil Kumar Kammari <anil@vitwit.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
atheeshp 2021-01-30 01:24:51 +05:30 committed by GitHub
parent 3b8e0f9387
commit d97e7907f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 13983 additions and 4471 deletions

View File

@ -221,6 +221,19 @@ func readTxCommandFlags(clientCtx Context, flagSet *pflag.FlagSet) (Context, err
clientCtx = clientCtx.WithSignModeStr(signModeStr)
}
if clientCtx.FeeGranter == nil || flagSet.Changed(flags.FlagFeeAccount) {
granter, _ := flagSet.GetString(flags.FlagFeeAccount)
if granter != "" {
granterAcc, err := sdk.AccAddressFromBech32(granter)
if err != nil {
return clientCtx, err
}
clientCtx = clientCtx.WithFeeGranterAddress(granterAcc)
}
}
if clientCtx.From == "" || flagSet.Changed(flags.FlagFrom) {
from, _ := flagSet.GetString(flags.FlagFrom)
fromAddr, fromName, keyType, err := GetFromFields(clientCtx.Keyring, from, clientCtx.GenerateOnly)

View File

@ -44,6 +44,7 @@ type Context struct {
TxConfig TxConfig
AccountRetriever AccountRetriever
NodeURI string
FeeGranter sdk.AccAddress
// TODO: Deprecated (remove).
LegacyAmino *codec.LegacyAmino
@ -166,6 +167,13 @@ func (ctx Context) WithFromAddress(addr sdk.AccAddress) Context {
return ctx
}
// WithFeeGranterAddress returns a copy of the context with an updated fee granter account
// address.
func (ctx Context) WithFeeGranterAddress(addr sdk.AccAddress) Context {
ctx.FeeGranter = addr
return ctx
}
// WithBroadcastMode returns a copy of the context with an updated broadcast
// mode.
func (ctx Context) WithBroadcastMode(mode string) Context {

View File

@ -70,6 +70,7 @@ const (
FlagCountTotal = "count-total"
FlagTimeoutHeight = "timeout-height"
FlagKeyAlgorithm = "algo"
FlagFeeAccount = "fee-account"
// Tendermint logging flags
FlagLogLevel = "log_level"
@ -112,6 +113,7 @@ func AddTxFlagsToCmd(cmd *cobra.Command) {
cmd.Flags().String(FlagKeyringBackend, DefaultKeyringBackend, "Select keyring's backend (os|file|kwallet|pass|test)")
cmd.Flags().String(FlagSignMode, "", "Choose sign mode (direct|amino-json), this is an advanced feature")
cmd.Flags().Uint64(FlagTimeoutHeight, 0, "Set a block timeout height to prevent the tx from being committed past a certain height")
cmd.Flags().String(FlagFeeAccount, "", "Fee account pays fees for the transaction instead of deducting from the signer")
// --gas can accept integers and "auto"
cmd.Flags().String(FlagGas, "", fmt.Sprintf("gas limit to set per-transaction; set to %q to calculate sufficient gas automatically (default %d)", GasFlagAuto, DefaultGasLimit))

View File

@ -57,6 +57,11 @@ func (ctx Context) GetFromAddress() sdk.AccAddress {
return ctx.FromAddress
}
// GetFeeGranterAddress returns the fee granter address from the context
func (ctx Context) GetFeeGranterAddress() sdk.AccAddress {
return ctx.FeeGranter
}
// GetFromName returns the key name for the current context.
func (ctx Context) GetFromName() string {
return ctx.FromName

View File

@ -117,6 +117,7 @@ func BroadcastTx(clientCtx client.Context, txf Factory, msgs ...sdk.Msg) error {
}
}
tx.SetFeeGranter(clientCtx.GetFeeGranterAddress())
err = Sign(txf, clientCtx.GetFromName(), tx, true)
if err != nil {
return err

View File

@ -42,5 +42,6 @@ type (
SetFeeAmount(amount sdk.Coins)
SetGasLimit(limit uint64)
SetTimeoutHeight(height uint64)
SetFeeGranter(feeGranter sdk.AccAddress)
}
)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,81 @@
syntax = "proto3";
package cosmos.feegrant.v1beta1;
import "gogoproto/gogo.proto";
import "google/protobuf/any.proto";
import "cosmos_proto/cosmos.proto";
import "cosmos/base/v1beta1/coin.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
option go_package = "github.com/cosmos/cosmos-sdk/x/feegrant/types";
// BasicFeeAllowance implements FeeAllowance with a one-time grant of tokens
// that optionally expires. The delegatee can use up to SpendLimit to cover fees.
message BasicFeeAllowance {
option (cosmos_proto.implements_interface) = "FeeAllowanceI";
// spend_limit specifies the maximum amount of tokens that can be spent
// by this allowance and will be updated as tokens are spent. If it is
// empty, there is no spend limit and any amount of coins can be spent.
repeated cosmos.base.v1beta1.Coin spend_limit = 1
[(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"];
// expiration specifies an optional time when this allowance expires
ExpiresAt expiration = 2 [(gogoproto.nullable) = false];
}
// PeriodicFeeAllowance extends FeeAllowance to allow for both a maximum cap,
// as well as a limit per time period.
message PeriodicFeeAllowance {
option (cosmos_proto.implements_interface) = "FeeAllowanceI";
// basic specifies a struct of `BasicFeeAllowance`
BasicFeeAllowance basic = 1 [(gogoproto.nullable) = false];
// period specifies the time duration in which period_spend_limit coins can
// be spent before that allowance is reset
Duration period = 2 [(gogoproto.nullable) = false];
// period_spend_limit specifies the maximum number of coins that can be spent
// in the period
repeated cosmos.base.v1beta1.Coin period_spend_limit = 3
[(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"];
// period_can_spend is the number of coins left to be spent before the period_reset time
repeated cosmos.base.v1beta1.Coin period_can_spend = 4
[(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"];
// period_reset is the time at which this period resets and a new one begins,
// it is calculated from the start time of the first transaction after the
// last period ended
ExpiresAt period_reset = 5 [(gogoproto.nullable) = false];
}
// Duration is a span of a clock time or number of blocks.
// This is designed to be added to an ExpiresAt struct.
message Duration {
// sum is the oneof that represents either duration or block
oneof sum {
google.protobuf.Duration duration = 1 [(gogoproto.stdduration) = true];
uint64 blocks = 2;
}
}
// ExpiresAt is a point in time where something expires.
// It may be *either* block time or block height
message ExpiresAt {
// sum is the oneof that represents either time or height
oneof sum {
google.protobuf.Timestamp time = 1 [(gogoproto.stdtime) = true];
int64 height = 2;
}
}
// FeeAllowanceGrant is stored in the KVStore to record a grant with full context
message FeeAllowanceGrant {
string granter = 1;
string grantee = 2;
google.protobuf.Any allowance = 3 [(cosmos_proto.accepts_interface) = "FeeAllowanceI"];
}

View File

@ -0,0 +1,12 @@
syntax = "proto3";
package cosmos.feegrant.v1beta1;
import "gogoproto/gogo.proto";
import "cosmos/feegrant/v1beta1/feegrant.proto";
option go_package = "github.com/cosmos/cosmos-sdk/x/feegrant/types";
// GenesisState contains a set of fee allowances, persisted from the store
message GenesisState {
repeated FeeAllowanceGrant fee_allowances = 1 [(gogoproto.nullable) = false];
}

View File

@ -0,0 +1,52 @@
syntax = "proto3";
package cosmos.feegrant.v1beta1;
import "gogoproto/gogo.proto";
import "cosmos/feegrant/v1beta1/feegrant.proto";
import "cosmos/base/query/v1beta1/pagination.proto";
import "google/api/annotations.proto";
option go_package = "github.com/cosmos/cosmos-sdk/x/feegrant/types";
// Query defines the gRPC querier service.
service Query {
// FeeAllowance returns fee granted to the grantee by the granter.
rpc FeeAllowance(QueryFeeAllowanceRequest) returns (QueryFeeAllowanceResponse) {
option (google.api.http).get = "/cosmos/feegrant/v1beta1/fee_allowance/{granter}/{grantee}";
}
// FeeAllowances returns all the grants for address.
rpc FeeAllowances(QueryFeeAllowancesRequest) returns (QueryFeeAllowancesResponse) {
option (google.api.http).get = "/cosmos/feegrant/v1beta1/fee_allowances/{grantee}";
}
}
// QueryFeeAllowanceRequest is the request type for the Query/FeeAllowance RPC method.
message QueryFeeAllowanceRequest {
string granter = 1;
string grantee = 2;
}
// QueryFeeAllowanceResponse is the response type for the Query/FeeAllowance RPC method.
message QueryFeeAllowanceResponse {
// fee_allowance is a fee_allowance granted for grantee by granter.
cosmos.feegrant.v1beta1.FeeAllowanceGrant fee_allowance = 1;
}
// QueryFeeAllowancesRequest is the request type for the Query/FeeAllowances RPC method.
message QueryFeeAllowancesRequest {
string grantee = 1;
// pagination defines an pagination for the request.
cosmos.base.query.v1beta1.PageRequest pagination = 2;
}
// QueryFeeAllowancesResponse is the response type for the Query/FeeAllowances RPC method.
message QueryFeeAllowancesResponse {
// fee_allowances are fee_allowance's granted for grantee by granter.
repeated cosmos.feegrant.v1beta1.FeeAllowanceGrant fee_allowances = 1;
// pagination defines an pagination for the response.
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}

View File

@ -0,0 +1,40 @@
syntax = "proto3";
package cosmos.feegrant.v1beta1;
import "gogoproto/gogo.proto";
import "google/protobuf/any.proto";
import "cosmos_proto/cosmos.proto";
option go_package = "github.com/cosmos/cosmos-sdk/x/feegrant/types";
// Msg defines the feegrant msg service.
service Msg {
// GrantFeeAllowance grants fee allowance to the grantee on the granter's
// account with the provided expiration time.
rpc GrantFeeAllowance(MsgGrantFeeAllowance) returns (MsgGrantFeeAllowanceResponse);
// RevokeFeeAllowance revokes any fee allowance of granter's account that
// has been granted to the grantee.
rpc RevokeFeeAllowance(MsgRevokeFeeAllowance) returns (MsgRevokeFeeAllowanceResponse);
}
// MsgGrantFeeAllowance adds permission for Grantee to spend up to Allowance
// of fees from the account of Granter.
message MsgGrantFeeAllowance {
string granter = 1;
string grantee = 2;
google.protobuf.Any allowance = 3 [(cosmos_proto.accepts_interface) = "FeeAllowanceI"];
}
// MsgGrantFeeAllowanceResponse defines the Msg/GrantFeeAllowanceResponse response type.
message MsgGrantFeeAllowanceResponse {}
// MsgRevokeFeeAllowance removes any existing FeeAllowance from Granter to Grantee.
message MsgRevokeFeeAllowance {
string granter = 1;
string grantee = 2;
}
// MsgRevokeFeeAllowanceResponse defines the Msg/RevokeFeeAllowanceResponse response type.
message MsgRevokeFeeAllowanceResponse {}

View File

@ -55,6 +55,10 @@ import (
"github.com/cosmos/cosmos-sdk/x/evidence"
evidencekeeper "github.com/cosmos/cosmos-sdk/x/evidence/keeper"
evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types"
feegrant "github.com/cosmos/cosmos-sdk/x/feegrant"
feegrantante "github.com/cosmos/cosmos-sdk/x/feegrant/ante"
feegrantkeeper "github.com/cosmos/cosmos-sdk/x/feegrant/keeper"
feegranttypes "github.com/cosmos/cosmos-sdk/x/feegrant/types"
"github.com/cosmos/cosmos-sdk/x/genutil"
genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types"
"github.com/cosmos/cosmos-sdk/x/gov"
@ -120,6 +124,7 @@ var (
crisis.AppModuleBasic{},
slashing.AppModuleBasic{},
ibc.AppModuleBasic{},
feegrant.AppModuleBasic{},
upgrade.AppModuleBasic{},
evidence.AppModuleBasic{},
transfer.AppModuleBasic{},
@ -181,6 +186,7 @@ type SimApp struct {
IBCKeeper *ibckeeper.Keeper // IBC Keeper must be a pointer in the app, so we can SetRouter on it correctly
EvidenceKeeper evidencekeeper.Keeper
TransferKeeper ibctransferkeeper.Keeper
FeeGrantKeeper feegrantkeeper.Keeper
// make scoped keepers public for test purposes
ScopedIBCKeeper capabilitykeeper.ScopedKeeper
@ -222,7 +228,7 @@ func NewSimApp(
keys := sdk.NewKVStoreKeys(
authtypes.StoreKey, banktypes.StoreKey, stakingtypes.StoreKey,
minttypes.StoreKey, distrtypes.StoreKey, slashingtypes.StoreKey,
govtypes.StoreKey, paramstypes.StoreKey, ibchost.StoreKey, upgradetypes.StoreKey,
govtypes.StoreKey, paramstypes.StoreKey, ibchost.StoreKey, upgradetypes.StoreKey, feegranttypes.StoreKey,
evidencetypes.StoreKey, ibctransfertypes.StoreKey, capabilitytypes.StoreKey,
authztypes.StoreKey,
)
@ -277,6 +283,8 @@ func NewSimApp(
app.CrisisKeeper = crisiskeeper.NewKeeper(
app.GetSubspace(crisistypes.ModuleName), invCheckPeriod, app.BankKeeper, authtypes.FeeCollectorName,
)
app.FeeGrantKeeper = feegrantkeeper.NewKeeper(appCodec, keys[feegranttypes.StoreKey], app.AccountKeeper)
app.UpgradeKeeper = upgradekeeper.NewKeeper(skipUpgradeHeights, keys[upgradetypes.StoreKey], appCodec, homePath)
// register the staking hooks
@ -347,6 +355,7 @@ func NewSimApp(
bank.NewAppModule(appCodec, app.BankKeeper, app.AccountKeeper),
capability.NewAppModule(appCodec, *app.CapabilityKeeper),
crisis.NewAppModule(&app.CrisisKeeper, skipGenesisInvariants),
feegrant.NewAppModule(appCodec, app.AccountKeeper, app.BankKeeper, app.FeeGrantKeeper, app.interfaceRegistry),
gov.NewAppModule(appCodec, app.GovKeeper, app.AccountKeeper, app.BankKeeper),
mint.NewAppModule(appCodec, app.MintKeeper, app.AccountKeeper),
slashing.NewAppModule(appCodec, app.SlashingKeeper, app.AccountKeeper, app.BankKeeper, app.StakingKeeper),
@ -379,6 +388,7 @@ func NewSimApp(
capabilitytypes.ModuleName, authtypes.ModuleName, banktypes.ModuleName, distrtypes.ModuleName, stakingtypes.ModuleName,
slashingtypes.ModuleName, govtypes.ModuleName, minttypes.ModuleName, crisistypes.ModuleName,
ibchost.ModuleName, genutiltypes.ModuleName, evidencetypes.ModuleName, authztypes.ModuleName, ibctransfertypes.ModuleName,
feegranttypes.ModuleName,
)
app.mm.RegisterInvariants(&app.CrisisKeeper)
@ -396,6 +406,7 @@ func NewSimApp(
auth.NewAppModule(appCodec, app.AccountKeeper, authsims.RandomGenesisAccounts),
bank.NewAppModule(appCodec, app.BankKeeper, app.AccountKeeper),
capability.NewAppModule(appCodec, *app.CapabilityKeeper),
feegrant.NewAppModule(appCodec, app.AccountKeeper, app.BankKeeper, app.FeeGrantKeeper, app.interfaceRegistry),
gov.NewAppModule(appCodec, app.GovKeeper, app.AccountKeeper, app.BankKeeper),
mint.NewAppModule(appCodec, app.MintKeeper, app.AccountKeeper),
staking.NewAppModule(appCodec, app.StakingKeeper, app.AccountKeeper, app.BankKeeper),
@ -419,8 +430,8 @@ func NewSimApp(
app.SetInitChainer(app.InitChainer)
app.SetBeginBlocker(app.BeginBlocker)
app.SetAnteHandler(
ante.NewAnteHandler(
app.AccountKeeper, app.BankKeeper, ante.DefaultSigVerificationGasConsumer,
feegrantante.NewAnteHandler(
app.AccountKeeper, app.BankKeeper, app.FeeGrantKeeper, ante.DefaultSigVerificationGasConsumer,
encodingConfig.TxConfig.SignModeHandler(),
),
)

View File

@ -20,4 +20,8 @@ const (
DefaultWeightCommunitySpendProposal int = 5
DefaultWeightTextProposal int = 5
DefaultWeightParamChangeProposal int = 5
// feegrant
DefaultWeightGrantFeeAllowance int = 100
DefaultWeightRevokeFeeAllowance int = 100
)

View File

@ -84,6 +84,9 @@ func (s *StdTxBuilder) SetTimeoutHeight(height uint64) {
s.TimeoutHeight = height
}
// SetFeeGranter does nothing for stdtx
func (s *StdTxBuilder) SetFeeGranter(_ sdk.AccAddress) {}
// StdTxConfig is a context.TxConfig for StdTx
type StdTxConfig struct {
Cdc *codec.LegacyAmino

35
x/feegrant/ante/ante.go Normal file
View File

@ -0,0 +1,35 @@
package ante
import (
sdk "github.com/cosmos/cosmos-sdk/types"
authante "github.com/cosmos/cosmos-sdk/x/auth/ante"
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
"github.com/cosmos/cosmos-sdk/x/auth/signing"
feegrantkeeper "github.com/cosmos/cosmos-sdk/x/feegrant/keeper"
feegranttypes "github.com/cosmos/cosmos-sdk/x/feegrant/types"
)
// NewAnteHandler returns an AnteHandler that checks and increments sequence
// numbers, checks signatures & account numbers, and deducts fees from the
// fee_payer or from fee_granter (if valid grant exist).
func NewAnteHandler(
ak authkeeper.AccountKeeper, bankKeeper feegranttypes.BankKeeper, feeGrantKeeper feegrantkeeper.Keeper,
sigGasConsumer authante.SignatureVerificationGasConsumer, signModeHandler signing.SignModeHandler,
) sdk.AnteHandler {
return sdk.ChainAnteDecorators(
authante.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first
authante.NewRejectExtensionOptionsDecorator(),
authante.NewMempoolFeeDecorator(),
authante.NewValidateBasicDecorator(),
authante.TxTimeoutHeightDecorator{},
authante.NewValidateMemoDecorator(ak),
authante.NewConsumeGasForTxSizeDecorator(ak),
NewDeductGrantedFeeDecorator(ak, bankKeeper, feeGrantKeeper),
authante.NewSetPubKeyDecorator(ak), // SetPubKeyDecorator must be called before all signature verification decorators
authante.NewValidateSigCountDecorator(ak),
authante.NewSigGasConsumeDecorator(ak, sigGasConsumer),
authante.NewSigVerificationDecorator(ak, signModeHandler),
authante.NewIncrementSequenceDecorator(ak), // innermost AnteDecorator
)
}

82
x/feegrant/ante/fee.go Normal file
View File

@ -0,0 +1,82 @@
package ante
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
authante "github.com/cosmos/cosmos-sdk/x/auth/ante"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/cosmos/cosmos-sdk/x/feegrant/keeper"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
)
// DeductGrantedFeeDecorator deducts fees from fee_payer or fee_granter (if exists a valid fee allowance) of the tx
// If the fee_payer or fee_granter does not have the funds to pay for the fees, return with InsufficientFunds error
// Call next AnteHandler if fees successfully deducted
// CONTRACT: Tx must implement GrantedFeeTx interface to use DeductGrantedFeeDecorator
type DeductGrantedFeeDecorator struct {
ak types.AccountKeeper
k keeper.Keeper
bk types.BankKeeper
}
func NewDeductGrantedFeeDecorator(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper) DeductGrantedFeeDecorator {
return DeductGrantedFeeDecorator{
ak: ak,
k: k,
bk: bk,
}
}
// AnteHandle performs a decorated ante-handler responsible for deducting transaction
// fees. Fees will be deducted from the account designated by the FeePayer on a
// transaction by default. However, if the fee payer differs from the transaction
// signer, the handler will check if a fee grant has been authorized. If the
// transaction's signer does not exist, it will be created.
func (d DeductGrantedFeeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) {
feeTx, ok := tx.(sdk.FeeTx)
if !ok {
return ctx, sdkerrors.Wrap(sdkerrors.ErrTxDecode, "Tx must be a GrantedFeeTx")
}
// sanity check from DeductFeeDecorator
if addr := d.ak.GetModuleAddress(authtypes.FeeCollectorName); addr == nil {
panic(fmt.Sprintf("%s module account has not been set", authtypes.FeeCollectorName))
}
fee := feeTx.GetFee()
feePayer := feeTx.FeePayer()
feeGranter := feeTx.FeeGranter()
deductFeesFrom := feePayer
// ensure the grant is allowed, if we request a different fee payer
if feeGranter != nil && !feeGranter.Equals(feePayer) {
err := d.k.UseGrantedFees(ctx, feeGranter, feePayer, fee)
if err != nil {
return ctx, sdkerrors.Wrapf(err, "%s not allowed to pay fees from %s", feeGranter, feePayer)
}
deductFeesFrom = feeGranter
}
// now, either way, we know that we are authorized to deduct the fees from the deductFeesFrom account
deductFeesFromAcc := d.ak.GetAccount(ctx, deductFeesFrom)
if deductFeesFromAcc == nil {
return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownAddress, "fee payer address: %s does not exist", deductFeesFrom)
}
// move on if there is no fee to deduct
if fee.IsZero() {
return next(ctx, tx, simulate)
}
// deduct fee if non-zero
err = authante.DeductFees(d.bk, ctx, deductFeesFromAcc, fee)
if err != nil {
return ctx, err
}
return next(ctx, tx, simulate)
}

287
x/feegrant/ante/fee_test.go Normal file
View File

@ -0,0 +1,287 @@
package ante_test
import (
"math/rand"
"testing"
"time"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
"github.com/stretchr/testify/suite"
"github.com/tendermint/tendermint/crypto"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
"github.com/cosmos/cosmos-sdk/simapp"
"github.com/cosmos/cosmos-sdk/simapp/helpers"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
authante "github.com/cosmos/cosmos-sdk/x/auth/ante"
authsign "github.com/cosmos/cosmos-sdk/x/auth/signing"
"github.com/cosmos/cosmos-sdk/x/auth/tx"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/cosmos/cosmos-sdk/x/feegrant/ante"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
)
// AnteTestSuite is a test suite to be used with ante handler tests.
type AnteTestSuite struct {
suite.Suite
app *simapp.SimApp
anteHandler sdk.AnteHandler
ctx sdk.Context
clientCtx client.Context
txBuilder client.TxBuilder
}
// SetupTest setups a new test, with new app, context, and anteHandler.
func (suite *AnteTestSuite) SetupTest(isCheckTx bool) {
suite.app, suite.ctx = createTestApp(isCheckTx)
suite.ctx = suite.ctx.WithBlockHeight(1)
// Set up TxConfig.
encodingConfig := simapp.MakeTestEncodingConfig()
// We're using TestMsg encoding in some tests, so register it here.
encodingConfig.Amino.RegisterConcrete(&testdata.TestMsg{}, "testdata.TestMsg", nil)
testdata.RegisterInterfaces(encodingConfig.InterfaceRegistry)
suite.clientCtx = client.Context{}.
WithTxConfig(encodingConfig.TxConfig)
suite.anteHandler = ante.NewAnteHandler(suite.app.AccountKeeper, suite.app.BankKeeper, suite.app.FeeGrantKeeper, authante.DefaultSigVerificationGasConsumer, encodingConfig.TxConfig.SignModeHandler())
}
func (suite *AnteTestSuite) TestDeductFeesNoDelegation() {
suite.SetupTest(true)
// setup
app, ctx := suite.app, suite.ctx
protoTxCfg := tx.NewTxConfig(codec.NewProtoCodec(app.InterfaceRegistry()), tx.DefaultSignModes)
// this just tests our handler
dfd := ante.NewDeductGrantedFeeDecorator(app.AccountKeeper, app.BankKeeper, app.FeeGrantKeeper)
ourAnteHandler := sdk.ChainAnteDecorators(dfd)
// this tests the whole stack
anteHandlerStack := suite.anteHandler
// keys and addresses
priv1, _, addr1 := testdata.KeyTestPubAddr()
priv2, _, addr2 := testdata.KeyTestPubAddr()
priv3, _, addr3 := testdata.KeyTestPubAddr()
priv4, _, addr4 := testdata.KeyTestPubAddr()
priv5, _, addr5 := testdata.KeyTestPubAddr()
// Set addr1 with insufficient funds
acc1 := app.AccountKeeper.NewAccountWithAddress(ctx, addr1)
app.AccountKeeper.SetAccount(ctx, acc1)
app.BankKeeper.SetBalances(ctx, addr1, []sdk.Coin{sdk.NewCoin("atom", sdk.NewInt(10))})
// Set addr2 with more funds
acc2 := app.AccountKeeper.NewAccountWithAddress(ctx, addr2)
app.AccountKeeper.SetAccount(ctx, acc2)
app.BankKeeper.SetBalances(ctx, addr2, []sdk.Coin{sdk.NewCoin("atom", sdk.NewInt(99999))})
// grant fee allowance from `addr2` to `addr3` (plenty to pay)
err := app.FeeGrantKeeper.GrantFeeAllowance(ctx, addr2, addr3, &types.BasicFeeAllowance{
SpendLimit: sdk.NewCoins(sdk.NewInt64Coin("atom", 500)),
})
suite.Require().NoError(err)
// grant low fee allowance (20atom), to check the tx requesting more than allowed.
err = app.FeeGrantKeeper.GrantFeeAllowance(ctx, addr2, addr4, &types.BasicFeeAllowance{
SpendLimit: sdk.NewCoins(sdk.NewInt64Coin("atom", 20)),
})
suite.Require().NoError(err)
cases := map[string]struct {
signerKey cryptotypes.PrivKey
signer sdk.AccAddress
feeAccount sdk.AccAddress
feeAccountKey cryptotypes.PrivKey
handler sdk.AnteHandler
fee int64
valid bool
}{
"paying with low funds (only ours)": {
signerKey: priv1,
signer: addr1,
fee: 50,
handler: ourAnteHandler,
valid: false,
},
"paying with good funds (only ours)": {
signerKey: priv2,
signer: addr2,
fee: 50,
handler: ourAnteHandler,
valid: true,
},
"paying with no account (only ours)": {
signerKey: priv3,
signer: addr3,
fee: 1,
handler: ourAnteHandler,
valid: false,
},
"no fee with real account (only ours)": {
signerKey: priv1,
signer: addr1,
fee: 0,
handler: ourAnteHandler,
valid: true,
},
"no fee with no account (only ours)": {
signerKey: priv5,
signer: addr5,
fee: 0,
handler: ourAnteHandler,
valid: false,
},
"valid fee grant without account (only ours)": {
signerKey: priv3,
signer: addr3,
feeAccount: addr2,
fee: 50,
handler: ourAnteHandler,
valid: true,
},
"no fee grant (only ours)": {
signerKey: priv3,
signer: addr3,
feeAccount: addr1,
fee: 2,
handler: ourAnteHandler,
valid: false,
},
"allowance smaller than requested fee (only ours)": {
signerKey: priv4,
signer: addr4,
feeAccount: addr2,
fee: 50,
handler: ourAnteHandler,
valid: false,
},
"granter cannot cover allowed fee grant (only ours)": {
signerKey: priv4,
signer: addr4,
feeAccount: addr1,
fee: 50,
handler: ourAnteHandler,
valid: false,
},
}
for name, stc := range cases {
tc := stc // to make scopelint happy
suite.T().Run(name, func(t *testing.T) {
fee := sdk.NewCoins(sdk.NewInt64Coin("atom", tc.fee))
msgs := []sdk.Msg{testdata.NewTestMsg(tc.signer)}
acc := app.AccountKeeper.GetAccount(ctx, tc.signer)
privs, accNums, seqs := []cryptotypes.PrivKey{tc.signerKey}, []uint64{0}, []uint64{0}
if acc != nil {
accNums, seqs = []uint64{acc.GetAccountNumber()}, []uint64{acc.GetSequence()}
}
tx, err := genTxWithFeeGranter(protoTxCfg, msgs, fee, helpers.DefaultGenTxGas, ctx.ChainID(), accNums, seqs, tc.feeAccount, privs...)
suite.Require().NoError(err)
_, err = ourAnteHandler(ctx, tx, false)
if tc.valid {
suite.Require().NoError(err)
} else {
suite.Require().Error(err)
}
_, err = anteHandlerStack(ctx, tx, false)
if tc.valid {
suite.Require().NoError(err)
} else {
suite.Require().Error(err)
}
})
}
}
// returns context and app with params set on account keeper
func createTestApp(isCheckTx bool) (*simapp.SimApp, sdk.Context) {
app := simapp.Setup(isCheckTx)
ctx := app.BaseApp.NewContext(isCheckTx, tmproto.Header{})
app.AccountKeeper.SetParams(ctx, authtypes.DefaultParams())
return app, ctx
}
// don't consume any gas
func SigGasNoConsumer(meter sdk.GasMeter, sig []byte, pubkey crypto.PubKey, params authtypes.Params) error {
return nil
}
func genTxWithFeeGranter(gen client.TxConfig, msgs []sdk.Msg, feeAmt sdk.Coins, gas uint64, chainID string, accNums,
accSeqs []uint64, feeGranter sdk.AccAddress, priv ...cryptotypes.PrivKey) (sdk.Tx, error) {
sigs := make([]signing.SignatureV2, len(priv))
// create a random length memo
r := rand.New(rand.NewSource(time.Now().UnixNano()))
memo := simulation.RandStringOfLength(r, simulation.RandIntBetween(r, 0, 100))
signMode := gen.SignModeHandler().DefaultMode()
// 1st round: set SignatureV2 with empty signatures, to set correct
// signer infos.
for i, p := range priv {
sigs[i] = signing.SignatureV2{
PubKey: p.PubKey(),
Data: &signing.SingleSignatureData{
SignMode: signMode,
},
Sequence: accSeqs[i],
}
}
tx := gen.NewTxBuilder()
err := tx.SetMsgs(msgs...)
if err != nil {
return nil, err
}
err = tx.SetSignatures(sigs...)
if err != nil {
return nil, err
}
tx.SetMemo(memo)
tx.SetFeeAmount(feeAmt)
tx.SetGasLimit(gas)
tx.SetFeeGranter(feeGranter)
// 2nd round: once all signer infos are set, every signer can sign.
for i, p := range priv {
signerData := authsign.SignerData{
ChainID: chainID,
AccountNumber: accNums[i],
Sequence: accSeqs[i],
}
signBytes, err := gen.SignModeHandler().GetSignBytes(signMode, signerData, tx.GetTx())
if err != nil {
panic(err)
}
sig, err := p.Sign(signBytes)
if err != nil {
panic(err)
}
sigs[i].Data.(*signing.SingleSignatureData).Signature = sig
err = tx.SetSignatures(sigs...)
if err != nil {
panic(err)
}
}
return tx.GetTx(), nil
}
func TestAnteTestSuite(t *testing.T) {
suite.Run(t, new(AnteTestSuite))
}

View File

@ -0,0 +1,621 @@
// +build norace
package cli_test
import (
"fmt"
"testing"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/crypto/hd"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli"
"github.com/cosmos/cosmos-sdk/testutil/network"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/feegrant/client/cli"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
govtestutil "github.com/cosmos/cosmos-sdk/x/gov/client/testutil"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
"github.com/gogo/protobuf/proto"
"github.com/stretchr/testify/suite"
tmcli "github.com/tendermint/tendermint/libs/cli"
)
type IntegrationTestSuite struct {
suite.Suite
cfg network.Config
network *network.Network
addedGranter sdk.AccAddress
addedGrantee sdk.AccAddress
addedGrant types.FeeAllowanceGrant
}
func (s *IntegrationTestSuite) SetupSuite() {
s.T().Log("setting up integration test suite")
if testing.Short() {
s.T().Skip("skipping test in unit-tests mode.")
}
cfg := network.DefaultConfig()
cfg.NumValidators = 2
s.cfg = cfg
s.network = network.New(s.T(), cfg)
_, err := s.network.WaitForHeight(1)
s.Require().NoError(err)
val := s.network.Validators[0]
granter := val.Address
grantee := s.network.Validators[1].Address
clientCtx := val.ClientCtx
commonFlags := []string{
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
}
fee := sdk.NewCoin("stake", sdk.NewInt(100))
duration := 365 * 24 * 60 * 60
args := append(
[]string{
granter.String(),
grantee.String(),
fmt.Sprintf("--%s=%s", cli.FlagSpendLimit, fee.String()),
fmt.Sprintf("--%s=%s", flags.FlagFrom, granter),
fmt.Sprintf("--%s=%v", cli.FlagExpiration, duration),
},
commonFlags...,
)
cmd := cli.NewCmdFeeGrant()
_, err = clitestutil.ExecTestCLICmd(clientCtx, cmd, args)
s.Require().NoError(err)
_, err = s.network.WaitForHeight(1)
s.Require().NoError(err)
s.addedGranter = granter
s.addedGrantee = grantee
grant, err := types.NewFeeAllowanceGrant(granter, grantee, &types.BasicFeeAllowance{
SpendLimit: sdk.NewCoins(fee),
})
s.Require().NoError(err)
s.addedGrant = grant
}
func (s *IntegrationTestSuite) TearDownSuite() {
s.T().Log("tearing down integration test suite")
s.network.Cleanup()
}
func (s *IntegrationTestSuite) TestCmdGetFeeGrant() {
val := s.network.Validators[0]
granter := val.Address
grantee := s.addedGrantee
clientCtx := val.ClientCtx
testCases := []struct {
name string
args []string
expectErrMsg string
expectErr bool
respType *types.FeeAllowanceGrant
resp *types.FeeAllowanceGrant
}{
{
"wrong granter",
[]string{
"wrong_granter",
grantee.String(),
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
},
"decoding bech32 failed",
true, nil, nil,
},
{
"wrong grantee",
[]string{
granter.String(),
"wrong_grantee",
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
},
"decoding bech32 failed",
true, nil, nil,
},
{
"non existed grant",
[]string{
"cosmos1nph3cfzk6trsmfxkeu943nvach5qw4vwstnvkl",
grantee.String(),
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
},
"no fee allowance found",
true, nil, nil,
},
{
"valid req",
[]string{
granter.String(),
grantee.String(),
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
},
"",
false,
&types.FeeAllowanceGrant{},
&s.addedGrant,
},
}
for _, tc := range testCases {
tc := tc
s.Run(tc.name, func() {
cmd := cli.GetCmdQueryFeeGrant()
out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
if tc.expectErr {
s.Require().Error(err)
s.Require().Contains(err.Error(), tc.expectErrMsg)
} else {
s.Require().NoError(err)
s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
s.Require().Equal(tc.respType.Grantee, tc.respType.Grantee)
s.Require().Equal(tc.respType.Granter, tc.respType.Granter)
s.Require().Equal(
tc.respType.GetFeeGrant().(*types.BasicFeeAllowance).SpendLimit,
tc.resp.GetFeeGrant().(*types.BasicFeeAllowance).SpendLimit,
)
}
})
}
}
func (s *IntegrationTestSuite) TestCmdGetFeeGrants() {
val := s.network.Validators[0]
grantee := s.addedGrantee
clientCtx := val.ClientCtx
testCases := []struct {
name string
args []string
expectErr bool
resp *types.QueryFeeAllowancesResponse
expectLength int
}{
{
"wrong grantee",
[]string{
"wrong_grantee",
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
},
true, nil, 0,
},
{
"non existed grantee",
[]string{
"cosmos1nph3cfzk6trsmfxkeu943nvach5qw4vwstnvkl",
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
},
false, &types.QueryFeeAllowancesResponse{}, 0,
},
{
"valid req",
[]string{
grantee.String(),
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
},
false, &types.QueryFeeAllowancesResponse{}, 1,
},
}
for _, tc := range testCases {
tc := tc
s.Run(tc.name, func() {
cmd := cli.GetCmdQueryFeeGrants()
out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
if tc.expectErr {
s.Require().Error(err)
} else {
s.Require().NoError(err)
s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), tc.resp), out.String())
s.Require().Len(tc.resp.FeeAllowances, tc.expectLength)
}
})
}
}
func (s *IntegrationTestSuite) TestNewCmdFeeGrant() {
val := s.network.Validators[0]
granter := val.Address
alreadyExistedGrantee := s.addedGrantee
clientCtx := val.ClientCtx
commonFlags := []string{
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
}
testCases := []struct {
name string
args []string
expectErr bool
respType proto.Message
expectedCode uint32
}{
{
"wrong granter address",
append(
[]string{
"wrong_granter",
"cosmos1nph3cfzk6trsmfxkeu943nvach5qw4vwstnvkl",
fmt.Sprintf("--%s=%s", cli.FlagSpendLimit, "100stake"),
fmt.Sprintf("--%s=%s", flags.FlagFrom, granter),
},
commonFlags...,
),
true, nil, 0,
},
{
"wrong grantee address",
append(
[]string{
granter.String(),
"wrong_grantee",
fmt.Sprintf("--%s=%s", cli.FlagSpendLimit, "100stake"),
fmt.Sprintf("--%s=%s", flags.FlagFrom, granter),
},
commonFlags...,
),
true, nil, 0,
},
{
"valid basic fee grant",
append(
[]string{
granter.String(),
"cosmos1nph3cfzk6trsmfxkeu943nvach5qw4vwstnvkl",
fmt.Sprintf("--%s=%s", cli.FlagSpendLimit, "100stake"),
fmt.Sprintf("--%s=%s", flags.FlagFrom, granter),
},
commonFlags...,
),
false, &sdk.TxResponse{}, 0,
},
{
"valid basic fee grant without spend limit",
append(
[]string{
granter.String(),
"cosmos17h5lzptx3ghvsuhk7wx4c4hnl7rsswxjer97em",
fmt.Sprintf("--%s=%s", flags.FlagFrom, granter),
},
commonFlags...,
),
false, &sdk.TxResponse{}, 0,
},
{
"valid basic fee grant without expiration",
append(
[]string{
granter.String(),
"cosmos16dlc38dcqt0uralyd8hksxyrny6kaeqfjvjwp5",
fmt.Sprintf("--%s=%s", cli.FlagSpendLimit, "100stake"),
fmt.Sprintf("--%s=%s", flags.FlagFrom, granter),
},
commonFlags...,
),
false, &sdk.TxResponse{}, 0,
},
{
"valid basic fee grant without spend-limit and expiration",
append(
[]string{
granter.String(),
"cosmos1ku40qup9vwag4wtf8cls9mkszxfthaklxkp3c8",
fmt.Sprintf("--%s=%s", flags.FlagFrom, granter),
},
commonFlags...,
),
false, &sdk.TxResponse{}, 0,
},
{
"try to add existed grant",
append(
[]string{
granter.String(),
alreadyExistedGrantee.String(),
fmt.Sprintf("--%s=%s", cli.FlagSpendLimit, "100stake"),
fmt.Sprintf("--%s=%s", flags.FlagFrom, granter),
},
commonFlags...,
),
false, &sdk.TxResponse{}, 18,
},
{
"invalid number of args(periodic fee grant)",
append(
[]string{
granter.String(),
"cosmos1nph3cfzk6trsmfxkeu943nvach5qw4vwstnvkl",
fmt.Sprintf("--%s=%s", cli.FlagSpendLimit, "100stake"),
fmt.Sprintf("--%s=%s", cli.FlagPeriodLimit, "10stake"),
fmt.Sprintf("--%s=%s", flags.FlagFrom, granter),
fmt.Sprintf("--%s=%d", cli.FlagExpiration, 10*60*60),
},
commonFlags...,
),
true, nil, 0,
},
{
"period mentioned and period limit omitted, invalid periodic grant",
append(
[]string{
granter.String(),
"cosmos1nph3cfzk6trsmfxkeu943nvach5qw4vwstnvkl",
fmt.Sprintf("--%s=%s", cli.FlagSpendLimit, "100stake"),
fmt.Sprintf("--%s=%d", cli.FlagPeriod, 10*60*60),
fmt.Sprintf("--%s=%s", flags.FlagFrom, granter),
fmt.Sprintf("--%s=%d", cli.FlagExpiration, 60*60),
},
commonFlags...,
),
true, nil, 0,
},
{
"period cannot be greater than the actual expiration(periodic fee grant)",
append(
[]string{
granter.String(),
"cosmos1nph3cfzk6trsmfxkeu943nvach5qw4vwstnvkl",
fmt.Sprintf("--%s=%s", cli.FlagSpendLimit, "100stake"),
fmt.Sprintf("--%s=%d", cli.FlagPeriod, 10*60*60),
fmt.Sprintf("--%s=%s", cli.FlagPeriodLimit, "10stake"),
fmt.Sprintf("--%s=%s", flags.FlagFrom, granter),
fmt.Sprintf("--%s=%d", cli.FlagExpiration, 60*60),
},
commonFlags...,
),
true, nil, 0,
},
{
"valid periodic fee grant",
append(
[]string{
granter.String(),
"cosmos1w55kgcf3ltaqdy4ww49nge3klxmrdavrr6frmp",
fmt.Sprintf("--%s=%s", cli.FlagSpendLimit, "100stake"),
fmt.Sprintf("--%s=%d", cli.FlagPeriod, 60*60),
fmt.Sprintf("--%s=%s", cli.FlagPeriodLimit, "10stake"),
fmt.Sprintf("--%s=%s", flags.FlagFrom, granter),
fmt.Sprintf("--%s=%d", cli.FlagExpiration, 10*60*60),
},
commonFlags...,
),
false, &sdk.TxResponse{}, 0,
},
{
"valid periodic fee grant without spend-limit",
append(
[]string{
granter.String(),
"cosmos1vevyks8pthkscvgazc97qyfjt40m6g9xe85ry8",
fmt.Sprintf("--%s=%d", cli.FlagPeriod, 60*60),
fmt.Sprintf("--%s=%s", cli.FlagPeriodLimit, "10stake"),
fmt.Sprintf("--%s=%s", flags.FlagFrom, granter),
fmt.Sprintf("--%s=%d", cli.FlagExpiration, 10*60*60),
},
commonFlags...,
),
false, &sdk.TxResponse{}, 0,
},
{
"valid periodic fee grant without expiration",
append(
[]string{
granter.String(),
"cosmos14cm33pvnrv2497tyt8sp9yavhmw83nwej3m0e8",
fmt.Sprintf("--%s=%s", cli.FlagSpendLimit, "100stake"),
fmt.Sprintf("--%s=%d", cli.FlagPeriod, 60*60),
fmt.Sprintf("--%s=%s", cli.FlagPeriodLimit, "10stake"),
fmt.Sprintf("--%s=%s", flags.FlagFrom, granter),
},
commonFlags...,
),
false, &sdk.TxResponse{}, 0,
},
{
"valid periodic fee grant without spend-limit and expiration",
append(
[]string{
granter.String(),
"cosmos12nyk4pcf4arshznkpz882e4l4ts0lt0ap8ce54",
fmt.Sprintf("--%s=%d", cli.FlagPeriod, 60*60),
fmt.Sprintf("--%s=%s", cli.FlagPeriodLimit, "10stake"),
fmt.Sprintf("--%s=%s", flags.FlagFrom, granter),
},
commonFlags...,
),
false, &sdk.TxResponse{}, 0,
},
}
for _, tc := range testCases {
tc := tc
s.Run(tc.name, func() {
cmd := cli.NewCmdFeeGrant()
out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
if tc.expectErr {
s.Require().Error(err)
} else {
s.Require().NoError(err)
s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
txResp := tc.respType.(*sdk.TxResponse)
s.Require().Equal(tc.expectedCode, txResp.Code, out.String())
}
})
}
}
func (s *IntegrationTestSuite) TestNewCmdRevokeFeegrant() {
val := s.network.Validators[0]
granter := s.addedGranter
grantee := s.addedGrantee
clientCtx := val.ClientCtx
commonFlags := []string{
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
}
testCases := []struct {
name string
args []string
expectErr bool
respType proto.Message
expectedCode uint32
}{
{
"invalid grantee",
append(
[]string{
"wrong_granter",
grantee.String(),
fmt.Sprintf("--%s=%s", flags.FlagFrom, granter),
},
commonFlags...,
),
true,
nil,
0,
},
{
"invalid grantee",
append(
[]string{
granter.String(),
"wrong_grantee",
fmt.Sprintf("--%s=%s", flags.FlagFrom, granter),
},
commonFlags...,
),
true,
nil,
0,
},
{
"Non existed grant",
append(
[]string{
granter.String(),
"cosmos1aeuqja06474dfrj7uqsvukm6rael982kk89mqr",
fmt.Sprintf("--%s=%s", flags.FlagFrom, granter),
},
commonFlags...,
),
false,
&sdk.TxResponse{},
4,
},
{
"Valid revoke",
append(
[]string{
granter.String(),
grantee.String(),
fmt.Sprintf("--%s=%s", flags.FlagFrom, granter),
},
commonFlags...,
),
false,
&sdk.TxResponse{},
0,
},
}
for _, tc := range testCases {
tc := tc
s.Run(tc.name, func() {
cmd := cli.NewCmdRevokeFeegrant()
out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
if tc.expectErr {
s.Require().Error(err)
} else {
s.Require().NoError(err)
s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
txResp := tc.respType.(*sdk.TxResponse)
s.Require().Equal(tc.expectedCode, txResp.Code, out.String())
}
})
}
}
func (s *IntegrationTestSuite) TestTxWithFeeGrant() {
val := s.network.Validators[0]
clientCtx := val.ClientCtx
granter := val.Address
// creating an account manually (This account won't be exist in state)
info, _, err := val.ClientCtx.Keyring.NewMnemonic("grantee", keyring.English, sdk.FullFundraiserPath, hd.Secp256k1)
s.Require().NoError(err)
grantee := sdk.AccAddress(info.GetPubKey().Address())
commonFlags := []string{
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
}
fee := sdk.NewCoin("stake", sdk.NewInt(100))
duration := 365 * 24 * 60 * 60
args := append(
[]string{
granter.String(),
grantee.String(),
fmt.Sprintf("--%s=%s", cli.FlagSpendLimit, fee.String()),
fmt.Sprintf("--%s=%s", flags.FlagFrom, granter),
fmt.Sprintf("--%s=%v", cli.FlagExpiration, duration),
},
commonFlags...,
)
cmd := cli.NewCmdFeeGrant()
_, err = clitestutil.ExecTestCLICmd(clientCtx, cmd, args)
s.Require().NoError(err)
_, err = s.network.WaitForHeight(1)
s.Require().NoError(err)
// granted fee allowance for an account which is not in state and creating
// any tx with it by using --fee-account shouldn't fail
out, err := govtestutil.MsgSubmitProposal(val.ClientCtx, grantee.String(),
"Text Proposal", "No desc", govtypes.ProposalTypeText,
fmt.Sprintf("--%s=%s", flags.FlagFeeAccount, granter.String()),
)
s.Require().NoError(err)
var resp sdk.TxResponse
s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &resp), out.String())
s.Require().Equal(uint32(0), resp.Code)
}
func TestIntegrationTestSuite(t *testing.T) {
suite.Run(t, new(IntegrationTestSuite))
}

View File

@ -0,0 +1,129 @@
package cli
import (
"fmt"
"strings"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
"github.com/spf13/cobra"
)
// GetQueryCmd returns the cli query commands for this module
func GetQueryCmd() *cobra.Command {
feegrantQueryCmd := &cobra.Command{
Use: types.ModuleName,
Short: "Querying commands for the feegrant module",
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}
feegrantQueryCmd.AddCommand(
GetCmdQueryFeeGrant(),
GetCmdQueryFeeGrants(),
)
return feegrantQueryCmd
}
// GetCmdQueryFeeGrant returns cmd to query for a grant between granter and grantee.
func GetCmdQueryFeeGrant() *cobra.Command {
cmd := &cobra.Command{
Use: "grant [granter] [grantee]",
Args: cobra.ExactArgs(2),
Short: "Query details of a single grant",
Long: strings.TrimSpace(
fmt.Sprintf(`Query details for a grant.
You can find the fee-grant of a granter and grantee.
Example:
$ %s query feegrant grant [granter] [grantee]
`, version.AppName),
),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx := client.GetClientContextFromCmd(cmd)
queryClient := types.NewQueryClient(clientCtx)
granterAddr, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
granteeAddr, err := sdk.AccAddressFromBech32(args[1])
if err != nil {
return err
}
res, err := queryClient.FeeAllowance(
cmd.Context(),
&types.QueryFeeAllowanceRequest{
Granter: granterAddr.String(),
Grantee: granteeAddr.String(),
},
)
if err != nil {
return err
}
return clientCtx.PrintProto(res.FeeAllowance)
},
}
flags.AddQueryFlagsToCmd(cmd)
return cmd
}
// GetCmdQueryFeeGrants returns cmd to query for all grants for a grantee.
func GetCmdQueryFeeGrants() *cobra.Command {
cmd := &cobra.Command{
Use: "grants [grantee]",
Args: cobra.ExactArgs(1),
Short: "Query all grants of a grantee",
Long: strings.TrimSpace(
fmt.Sprintf(`Queries all the grants for a grantee address.
Example:
$ %s query feegrant grants [grantee]
`, version.AppName),
),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx := client.GetClientContextFromCmd(cmd)
queryClient := types.NewQueryClient(clientCtx)
granteeAddr, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
pageReq, err := client.ReadPageRequest(cmd.Flags())
if err != nil {
return err
}
res, err := queryClient.FeeAllowances(
cmd.Context(),
&types.QueryFeeAllowancesRequest{
Grantee: granteeAddr.String(),
Pagination: pageReq,
},
)
if err != nil {
return err
}
return clientCtx.PrintProto(res)
},
}
flags.AddQueryFlagsToCmd(cmd)
flags.AddPaginationFlagsToCmd(cmd, "grants")
return cmd
}

211
x/feegrant/client/cli/tx.go Normal file
View File

@ -0,0 +1,211 @@
package cli
import (
"fmt"
"strings"
"time"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/tx"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/msgservice"
"github.com/cosmos/cosmos-sdk/version"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
)
// flag for feegrant module
const (
FlagExpiration = "expiration"
FlagPeriod = "period"
FlagPeriodLimit = "period-limit"
FlagSpendLimit = "spend-limit"
)
// GetTxCmd returns the transaction commands for this module
func GetTxCmd() *cobra.Command {
feegrantTxCmd := &cobra.Command{
Use: types.ModuleName,
Short: "Feegrant transactions subcommands",
Long: "Grant and revoke fee allowance for a grantee by a granter",
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}
feegrantTxCmd.AddCommand(
NewCmdFeeGrant(),
NewCmdRevokeFeegrant(),
)
return feegrantTxCmd
}
// NewCmdFeeGrant returns a CLI command handler for creating a MsgGrantFeeAllowance transaction.
func NewCmdFeeGrant() *cobra.Command {
cmd := &cobra.Command{
Use: "grant [granter] [grantee]",
Short: "Grant Fee allowance to an address",
Long: strings.TrimSpace(
fmt.Sprintf(
`Grant authorization to pay fees from your address. Note, the'--from' flag is
ignored as it is implied from [granter].
Examples:
%s tx %s grant cosmos1skjw... cosmos1skjw... --spend-limit 100stake --expiration 36000 or
%s tx %s grant cosmos1skjw... cosmos1skjw... --spend-limit 100stake --period 3600 --period-limit 10stake --expiration 36000
`, version.AppName, types.ModuleName, version.AppName, types.ModuleName,
),
),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
_, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
cmd.Flags().Set(flags.FlagFrom, args[0])
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
grantee, err := sdk.AccAddressFromBech32(args[1])
if err != nil {
return err
}
granter := clientCtx.GetFromAddress()
sl, err := cmd.Flags().GetString(FlagSpendLimit)
if err != nil {
return err
}
// if `FlagSpendLimit` isn't set, limit will be nil
limit, err := sdk.ParseCoinsNormalized(sl)
if err != nil {
return err
}
exp, err := cmd.Flags().GetInt64(FlagExpiration)
if err != nil {
return err
}
basic := types.BasicFeeAllowance{
SpendLimit: limit,
}
if exp != 0 {
expDuration := time.Duration(exp) * time.Second
basic.Expiration = types.ExpiresAtTime(time.Now().Add(expDuration))
}
var grant types.FeeAllowanceI
grant = &basic
periodClock, err := cmd.Flags().GetInt64(FlagPeriod)
if err != nil {
return err
}
periodLimitVal, err := cmd.Flags().GetString(FlagPeriodLimit)
if err != nil {
return err
}
// Check any of period or periodLimit flags set, If set consider it as periodic fee allowance.
if periodClock > 0 || periodLimitVal != "" {
periodLimit, err := sdk.ParseCoinsNormalized(periodLimitVal)
if err != nil {
return err
}
if periodClock > 0 && periodLimit != nil {
if exp > 0 && periodClock > exp {
return fmt.Errorf("period(%d) cannot be greater than the expiration(%d)", periodClock, exp)
}
periodic := types.PeriodicFeeAllowance{
Basic: basic,
Period: types.ClockDuration(time.Duration(periodClock) * time.Second),
PeriodReset: types.ExpiresAtTime(time.Now().Add(time.Duration(periodClock) * time.Second)),
PeriodSpendLimit: periodLimit,
PeriodCanSpend: periodLimit,
}
grant = &periodic
} else {
return fmt.Errorf("invalid number of args %d", len(args))
}
}
msg, err := types.NewMsgGrantFeeAllowance(grant, granter, grantee)
if err != nil {
return err
}
svcMsgClientConn := &msgservice.ServiceMsgClientConn{}
feeGrantMsgClient := types.NewMsgClient(svcMsgClientConn)
_, err = feeGrantMsgClient.GrantFeeAllowance(cmd.Context(), msg)
if err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), svcMsgClientConn.GetMsgs()...)
},
}
flags.AddTxFlagsToCmd(cmd)
cmd.Flags().Int64(FlagExpiration, 0, "The second unit of time duration which the grant is active for the user")
cmd.Flags().String(FlagSpendLimit, "", "Spend limit specifies the max limit can be used, if not mentioned there is no limit")
cmd.Flags().Int64(FlagPeriod, 0, "period specifies the time duration in which period_spend_limit coins can be spent before that allowance is reset")
cmd.Flags().String(FlagPeriodLimit, "", "// period limit specifies the maximum number of coins that can be spent in the period")
return cmd
}
// NewCmdRevokeFeegrant returns a CLI command handler for creating a MsgRevokeFeeAllowance transaction.
func NewCmdRevokeFeegrant() *cobra.Command {
cmd := &cobra.Command{
Use: "revoke [granter_address] [grantee_address]",
Short: "revoke fee-grant",
Long: strings.TrimSpace(
fmt.Sprintf(`revoke fee grant from a granter to a grantee. Note, the'--from' flag is
ignored as it is implied from [granter_address].
Example:
$ %s tx %s revoke cosmos1skj.. cosmos1skj..
`, version.AppName, types.ModuleName),
),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cmd.Flags().Set(flags.FlagFrom, args[0])
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
grantee, err := sdk.AccAddressFromBech32(args[1])
if err != nil {
return err
}
msg := types.NewMsgRevokeFeeAllowance(clientCtx.GetFromAddress(), grantee)
svcMsgClientConn := &msgservice.ServiceMsgClientConn{}
feeGrantMsgClient := types.NewMsgClient(svcMsgClientConn)
_, err = feeGrantMsgClient.RevokeFeeAllowance(cmd.Context(), &msg)
if err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), svcMsgClientConn.GetMsgs()...)
},
}
flags.AddTxFlagsToCmd(cmd)
return cmd
}

View File

@ -0,0 +1,209 @@
package rest_test
import (
"fmt"
"testing"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/crypto/hd"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli"
"github.com/cosmos/cosmos-sdk/testutil/network"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
banktestutil "github.com/cosmos/cosmos-sdk/x/bank/client/testutil"
"github.com/cosmos/cosmos-sdk/x/feegrant/client/cli"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
"github.com/stretchr/testify/suite"
)
type IntegrationTestSuite struct {
suite.Suite
cfg network.Config
network *network.Network
grantee sdk.AccAddress
}
func (s *IntegrationTestSuite) SetupSuite() {
s.T().Log("setting up integration test suite")
cfg := network.DefaultConfig()
cfg.NumValidators = 1
s.cfg = cfg
s.network = network.New(s.T(), cfg)
val := s.network.Validators[0]
// Create new account in the keyring.
info, _, err := val.ClientCtx.Keyring.NewMnemonic("grantee", keyring.English, sdk.FullFundraiserPath, hd.Secp256k1)
s.Require().NoError(err)
newAddr := sdk.AccAddress(info.GetPubKey().Address())
// Send some funds to the new account.
_, err = banktestutil.MsgSendExec(
val.ClientCtx,
val.Address,
newAddr,
sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(200))), fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
)
s.Require().NoError(err)
s.grantee = newAddr
_, err = s.network.WaitForHeight(1)
s.Require().NoError(err)
}
func (s *IntegrationTestSuite) TearDownSuite() {
s.T().Log("tearing down integration test suite")
s.network.Cleanup()
}
func (s *IntegrationTestSuite) TestQueryFeeAllowance() {
val := s.network.Validators[0]
baseURL := val.APIAddress
testCases := []struct {
name string
url string
expectErr bool
errorMsg string
preRun func()
postRun func(_ types.QueryFeeAllowanceResponse)
}{
{
"fail: invalid granter",
fmt.Sprintf("%s/cosmos/feegrant/v1beta1/fee_allowance/%s/%s", baseURL, "invalid_granter", s.grantee.String()),
true,
"decoding bech32 failed: invalid index of 1: invalid request",
func() {},
func(types.QueryFeeAllowanceResponse) {},
},
{
"fail: invalid grantee",
fmt.Sprintf("%s/cosmos/feegrant/v1beta1/fee_allowance/%s/%s", baseURL, val.Address.String(), "invalid_grantee"),
true,
"decoding bech32 failed: invalid index of 1: invalid request",
func() {},
func(types.QueryFeeAllowanceResponse) {},
},
{
"fail: no grants",
fmt.Sprintf("%s/cosmos/feegrant/v1beta1/fee_allowance/%s/%s", baseURL, val.Address.String(), s.grantee.String()),
true,
"no fee allowance found",
func() {},
func(types.QueryFeeAllowanceResponse) {},
},
{
"valid query: expect single grant",
fmt.Sprintf("%s/cosmos/feegrant/v1beta1/fee_allowance/%s/%s", baseURL, val.Address.String(), s.grantee.String()),
false,
"",
func() {
execFeeAllowance(val, s)
},
func(allowance types.QueryFeeAllowanceResponse) {
s.Require().Equal(allowance.FeeAllowance.Granter, val.Address.String())
s.Require().Equal(allowance.FeeAllowance.Grantee, s.grantee.String())
},
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
tc.preRun()
resp, _ := rest.GetRequest(tc.url)
if tc.expectErr {
s.Require().Contains(string(resp), tc.errorMsg)
} else {
var allowance types.QueryFeeAllowanceResponse
err := val.ClientCtx.JSONMarshaler.UnmarshalJSON(resp, &allowance)
s.Require().NoError(err)
tc.postRun(allowance)
}
})
}
}
func (s *IntegrationTestSuite) TestQueryGranteeAllowances() {
val := s.network.Validators[0]
baseURL := val.APIAddress
testCases := []struct {
name string
url string
expectErr bool
errorMsg string
preRun func()
postRun func(_ types.QueryFeeAllowancesResponse)
}{
{
"fail: invalid grantee",
fmt.Sprintf("%s/cosmos/feegrant/v1beta1/fee_allowances/%s", baseURL, "invalid_grantee"),
true,
"decoding bech32 failed: invalid index of 1: invalid request",
func() {},
func(types.QueryFeeAllowancesResponse) {},
},
{
"success: no grants",
fmt.Sprintf("%s/cosmos/feegrant/v1beta1/fee_allowances/%s?pagination.offset=1", baseURL, s.grantee.String()),
false,
"",
func() {},
func(allowances types.QueryFeeAllowancesResponse) {
s.Require().Equal(len(allowances.FeeAllowances), 0)
},
},
{
"valid query: expect single grant",
fmt.Sprintf("%s/cosmos/feegrant/v1beta1/fee_allowances/%s", baseURL, s.grantee.String()),
false,
"",
func() {
execFeeAllowance(val, s)
},
func(allowances types.QueryFeeAllowancesResponse) {
s.Require().Equal(len(allowances.FeeAllowances), 1)
s.Require().Equal(allowances.FeeAllowances[0].Granter, val.Address.String())
s.Require().Equal(allowances.FeeAllowances[0].Grantee, s.grantee.String())
},
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
tc.preRun()
resp, _ := rest.GetRequest(tc.url)
if tc.expectErr {
s.Require().Contains(string(resp), tc.errorMsg)
} else {
var allowance types.QueryFeeAllowancesResponse
err := val.ClientCtx.JSONMarshaler.UnmarshalJSON(resp, &allowance)
s.Require().NoError(err)
tc.postRun(allowance)
}
})
}
}
func execFeeAllowance(val *network.Validator, s *IntegrationTestSuite) {
fee := sdk.NewCoin("steak", sdk.NewInt(100))
duration := 365 * 24 * 60 * 60
args := []string{
val.Address.String(),
s.grantee.String(),
fmt.Sprintf("--%s=%s", cli.FlagSpendLimit, fee.String()),
fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()),
fmt.Sprintf("--%s=%v", cli.FlagExpiration, duration),
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
}
cmd := cli.NewCmdFeeGrant()
_, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, args)
s.Require().NoError(err)
}
func TestIntegrationTestSuite(t *testing.T) {
suite.Run(t, new(IntegrationTestSuite))
}

29
x/feegrant/doc.go Normal file
View File

@ -0,0 +1,29 @@
/*
Package feegrant provides functionality for authorizing the payment of transaction
fees from one account (key) to another account (key).
Effectively, this allows for a user to pay fees using the balance of an account
different from their own. Example use cases would be allowing a key on a device to
pay for fees using a master wallet, or a third party service allowing users to
pay for transactions without ever really holding their own tokens. This package
provides ways for specifying fee allowances such that authorizing fee payment to
another account can be done with clear and safe restrictions.
A user would authorize granting fee payment to another user using
MsgDelegateFeeAllowance and revoke that delegation using MsgRevokeFeeAllowance.
In both cases, Granter is the one who is authorizing fee payment and Grantee is
the one who is receiving the fee payment authorization. So grantee would correspond
to the one who is signing a transaction and the granter would be the address that
pays the fees.
The fee allowance that a grantee receives is specified by an implementation of
the FeeAllowance interface. Two FeeAllowance implementations are provided in
this package: BasicFeeAllowance and PeriodicFeeAllowance.
In order to integrate this into an application, we must use the DeductGrantedFeeDecorator
ante handler from this package instead of the default DeductFeeDecorator from x/auth.
To allow handling txs from empty accounts (with fees paid from an existing account),
we have to re-order the decorators as well.
*/
package feegrant

61
x/feegrant/genesis.go Normal file
View File

@ -0,0 +1,61 @@
package feegrant
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/feegrant/keeper"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
)
// GenesisState contains a set of fee allowances, persisted from the store
type GenesisState []types.FeeAllowanceGrant
// ValidateBasic ensures all grants in the genesis state are valid
func (g GenesisState) ValidateBasic() error {
for _, f := range g {
err := f.GetFeeGrant().ValidateBasic()
if err != nil {
return err
}
}
return nil
}
// InitGenesis will initialize the keeper from a *previously validated* GenesisState
func InitGenesis(ctx sdk.Context, k keeper.Keeper, data *types.GenesisState) {
for _, f := range data.FeeAllowances {
granter, err := sdk.AccAddressFromBech32(f.Granter)
if err != nil {
panic(err)
}
grantee, err := sdk.AccAddressFromBech32(f.Grantee)
if err != nil {
panic(err)
}
err = k.GrantFeeAllowance(ctx, granter, grantee, f.GetFeeGrant())
if err != nil {
panic(err)
}
}
}
// ExportGenesis will dump the contents of the keeper into a serializable GenesisState
//
// All expiration heights will be thrown off if we dump state and start at a new
// chain at height 0. Thus, we allow the Allowances to "prepare themselves"
// for export, like if they have expiry at 5000 and current is 4000, they export with
// expiry of 1000. Every FeeAllowance has a method `PrepareForExport` that allows
// them to perform any changes needed prior to export.
func ExportGenesis(ctx sdk.Context, k keeper.Keeper) (*types.GenesisState, error) {
time, height := ctx.BlockTime(), ctx.BlockHeight()
var grants []types.FeeAllowanceGrant
err := k.IterateAllFeeAllowances(ctx, func(grant types.FeeAllowanceGrant) bool {
grants = append(grants, grant.PrepareForExport(time, height))
return false
})
return &types.GenesisState{
FeeAllowances: grants,
}, err
}

View File

@ -0,0 +1,56 @@
package feegrant_test
import (
"testing"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
"github.com/cosmos/cosmos-sdk/simapp"
sdk "github.com/cosmos/cosmos-sdk/types"
feegrant "github.com/cosmos/cosmos-sdk/x/feegrant"
"github.com/cosmos/cosmos-sdk/x/feegrant/keeper"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
"github.com/stretchr/testify/suite"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)
type GenesisTestSuite struct {
suite.Suite
ctx sdk.Context
keeper keeper.Keeper
}
func (suite *GenesisTestSuite) SetupTest() {
checkTx := false
app := simapp.Setup(checkTx)
suite.ctx = app.BaseApp.NewContext(checkTx, tmproto.Header{Height: 1})
suite.keeper = app.FeeGrantKeeper
}
var (
granteePub = secp256k1.GenPrivKey().PubKey()
granterPub = secp256k1.GenPrivKey().PubKey()
granteeAddr = sdk.AccAddress(granteePub.Address())
granterAddr = sdk.AccAddress(granterPub.Address())
)
func (suite *GenesisTestSuite) TestImportExportGenesis() {
coins := sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(1_000)))
now := suite.ctx.BlockHeader().Time
allowance := &types.BasicFeeAllowance{SpendLimit: coins, Expiration: types.ExpiresAtTime(now.AddDate(1, 0, 0))}
err := suite.keeper.GrantFeeAllowance(suite.ctx, granterAddr, granteeAddr, allowance)
suite.Require().NoError(err)
genesis, err := feegrant.ExportGenesis(suite.ctx, suite.keeper)
suite.Require().NoError(err)
// Clear keeper
suite.keeper.RevokeFeeAllowance(suite.ctx, granterAddr, granteeAddr)
feegrant.InitGenesis(suite.ctx, suite.keeper, genesis)
newGenesis, err := feegrant.ExportGenesis(suite.ctx, suite.keeper)
suite.Require().NoError(err)
suite.Require().Equal(genesis, newGenesis)
}
func TestGenesisTestSuite(t *testing.T) {
suite.Run(t, new(GenesisTestSuite))
}

View File

@ -0,0 +1,94 @@
package keeper
import (
"context"
"github.com/gogo/protobuf/proto"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/query"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
)
var _ types.QueryServer = Keeper{}
// FeeAllowance returns fee granted to the grantee by the granter.
func (q Keeper) FeeAllowance(c context.Context, req *types.QueryFeeAllowanceRequest) (*types.QueryFeeAllowanceResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
granterAddr, err := sdk.AccAddressFromBech32(req.Granter)
if err != nil {
return nil, err
}
granteeAddr, err := sdk.AccAddressFromBech32(req.Grantee)
if err != nil {
return nil, err
}
ctx := sdk.UnwrapSDKContext(c)
feeAllowance := q.GetFeeAllowance(ctx, granterAddr, granteeAddr)
if feeAllowance == nil {
return nil, status.Errorf(codes.NotFound, "no fee allowance found")
}
msg, ok := feeAllowance.(proto.Message)
if !ok {
return nil, status.Errorf(codes.Internal, "can't proto marshal %T", msg)
}
feeAllowanceAny, err := codectypes.NewAnyWithValue(msg)
if err != nil {
return nil, status.Errorf(codes.Internal, err.Error())
}
return &types.QueryFeeAllowanceResponse{
FeeAllowance: &types.FeeAllowanceGrant{
Granter: granterAddr.String(),
Grantee: granteeAddr.String(),
Allowance: feeAllowanceAny,
},
}, nil
}
func (q Keeper) FeeAllowances(c context.Context, req *types.QueryFeeAllowancesRequest) (*types.QueryFeeAllowancesResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
granteeAddr, err := sdk.AccAddressFromBech32(req.Grantee)
if err != nil {
return nil, err
}
ctx := sdk.UnwrapSDKContext(c)
var grants []*types.FeeAllowanceGrant
store := ctx.KVStore(q.storeKey)
grantsStore := prefix.NewStore(store, types.FeeAllowancePrefixByGrantee(granteeAddr))
pageRes, err := query.Paginate(grantsStore, req.Pagination, func(key []byte, value []byte) error {
var grant types.FeeAllowanceGrant
if err := q.cdc.UnmarshalBinaryBare(value, &grant); err != nil {
return err
}
grants = append(grants, &grant)
return nil
})
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &types.QueryFeeAllowancesResponse{FeeAllowances: grants, Pagination: pageRes}, nil
}

190
x/feegrant/keeper/keeper.go Normal file
View File

@ -0,0 +1,190 @@
package keeper
import (
"fmt"
"github.com/tendermint/tendermint/libs/log"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
)
// Keeper manages state of all fee grants, as well as calculating approval.
// It must have a codec with all available allowances registered.
type Keeper struct {
cdc codec.BinaryMarshaler
storeKey sdk.StoreKey
authKeeper types.AccountKeeper
}
// NewKeeper creates a fee grant Keeper
func NewKeeper(cdc codec.BinaryMarshaler, storeKey sdk.StoreKey, ak types.AccountKeeper) Keeper {
return Keeper{
cdc: cdc,
storeKey: storeKey,
authKeeper: ak,
}
}
// Logger returns a module-specific logger.
func (k Keeper) Logger(ctx sdk.Context) log.Logger {
return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName))
}
// GrantFeeAllowance creates a new grant
func (k Keeper) GrantFeeAllowance(ctx sdk.Context, granter, grantee sdk.AccAddress, feeAllowance types.FeeAllowanceI) error {
// create the account if it is not in account state
granteeAcc := k.authKeeper.GetAccount(ctx, grantee)
if granteeAcc == nil {
granteeAcc = k.authKeeper.NewAccountWithAddress(ctx, grantee)
k.authKeeper.SetAccount(ctx, granteeAcc)
}
store := ctx.KVStore(k.storeKey)
key := types.FeeAllowanceKey(granter, grantee)
grant, err := types.NewFeeAllowanceGrant(granter, grantee, feeAllowance)
if err != nil {
return err
}
bz, err := k.cdc.MarshalBinaryBare(&grant)
if err != nil {
return err
}
store.Set(key, bz)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeSetFeeGrant,
sdk.NewAttribute(types.AttributeKeyGranter, grant.Granter),
sdk.NewAttribute(types.AttributeKeyGrantee, grant.Grantee),
),
)
return nil
}
// RevokeFeeAllowance removes an existing grant
func (k Keeper) RevokeFeeAllowance(ctx sdk.Context, granter, grantee sdk.AccAddress) error {
store := ctx.KVStore(k.storeKey)
key := types.FeeAllowanceKey(granter, grantee)
_, found := k.GetFeeGrant(ctx, granter, grantee)
if !found {
return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "fee-grant not found")
}
store.Delete(key)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeRevokeFeeGrant,
sdk.NewAttribute(types.AttributeKeyGranter, granter.String()),
sdk.NewAttribute(types.AttributeKeyGrantee, grantee.String()),
),
)
return nil
}
// GetFeeAllowance returns the allowance between the granter and grantee.
// If there is none, it returns nil, nil.
// Returns an error on parsing issues
func (k Keeper) GetFeeAllowance(ctx sdk.Context, granter, grantee sdk.AccAddress) types.FeeAllowanceI {
grant, found := k.GetFeeGrant(ctx, granter, grantee)
if !found {
return nil
}
return grant.GetFeeGrant()
}
// GetFeeGrant returns entire grant between both accounts
func (k Keeper) GetFeeGrant(ctx sdk.Context, granter sdk.AccAddress, grantee sdk.AccAddress) (types.FeeAllowanceGrant, bool) {
store := ctx.KVStore(k.storeKey)
key := types.FeeAllowanceKey(granter, grantee)
bz := store.Get(key)
if len(bz) == 0 {
return types.FeeAllowanceGrant{}, false
}
var feegrant types.FeeAllowanceGrant
k.cdc.MustUnmarshalBinaryBare(bz, &feegrant)
return feegrant, true
}
// IterateAllGranteeFeeAllowances iterates over all the grants from anyone to the given grantee.
// Callback to get all data, returns true to stop, false to keep reading
func (k Keeper) IterateAllGranteeFeeAllowances(ctx sdk.Context, grantee sdk.AccAddress, cb func(types.FeeAllowanceGrant) bool) error {
store := ctx.KVStore(k.storeKey)
prefix := types.FeeAllowancePrefixByGrantee(grantee)
iter := sdk.KVStorePrefixIterator(store, prefix)
defer iter.Close()
stop := false
for ; iter.Valid() && !stop; iter.Next() {
bz := iter.Value()
var feeGrant types.FeeAllowanceGrant
k.cdc.MustUnmarshalBinaryBare(bz, &feeGrant)
stop = cb(feeGrant)
}
return nil
}
// IterateAllFeeAllowances iterates over all the grants in the store.
// Callback to get all data, returns true to stop, false to keep reading
// Calling this without pagination is very expensive and only designed for export genesis
func (k Keeper) IterateAllFeeAllowances(ctx sdk.Context, cb func(types.FeeAllowanceGrant) bool) error {
store := ctx.KVStore(k.storeKey)
iter := sdk.KVStorePrefixIterator(store, types.FeeAllowanceKeyPrefix)
defer iter.Close()
stop := false
for ; iter.Valid() && !stop; iter.Next() {
bz := iter.Value()
var feeGrant types.FeeAllowanceGrant
k.cdc.MustUnmarshalBinaryBare(bz, &feeGrant)
stop = cb(feeGrant)
}
return nil
}
// UseGrantedFees will try to pay the given fee from the granter's account as requested by the grantee
func (k Keeper) UseGrantedFees(ctx sdk.Context, granter, grantee sdk.AccAddress, fee sdk.Coins) error {
grant, found := k.GetFeeGrant(ctx, granter, grantee)
if !found || grant.GetFeeGrant() == nil {
return sdkerrors.Wrapf(types.ErrNoAllowance, "grant missing")
}
remove, err := grant.GetFeeGrant().Accept(fee, ctx.BlockTime(), ctx.BlockHeight())
if err == nil {
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeUseFeeGrant,
sdk.NewAttribute(types.AttributeKeyGranter, granter.String()),
sdk.NewAttribute(types.AttributeKeyGrantee, grantee.String()),
),
)
}
if remove {
k.RevokeFeeAllowance(ctx, granter, grantee)
// note this returns nil if err == nil
return sdkerrors.Wrap(err, "removed grant")
}
if err != nil {
return sdkerrors.Wrap(err, "invalid grant")
}
// if we accepted, store the updated state of the allowance
return k.GrantFeeAllowance(ctx, granter, grantee, grant.GetFeeGrant())
}

View File

@ -0,0 +1,257 @@
package keeper_test
import (
"testing"
"github.com/stretchr/testify/suite"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
"github.com/cosmos/cosmos-sdk/simapp"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
)
type KeeperTestSuite struct {
suite.Suite
app *simapp.SimApp
ctx sdk.Context
addrs []sdk.AccAddress
}
func TestKeeperTestSuite(t *testing.T) {
suite.Run(t, new(KeeperTestSuite))
}
func (suite *KeeperTestSuite) SetupTest() {
app := simapp.Setup(false)
ctx := app.BaseApp.NewContext(false, tmproto.Header{})
suite.app = app
suite.ctx = ctx
suite.addrs = simapp.AddTestAddrsIncremental(app, ctx, 4, sdk.NewInt(30000000))
}
func (suite *KeeperTestSuite) TestKeeperCrud() {
ctx := suite.ctx
k := suite.app.FeeGrantKeeper
// some helpers
atom := sdk.NewCoins(sdk.NewInt64Coin("atom", 555))
eth := sdk.NewCoins(sdk.NewInt64Coin("eth", 123))
basic := &types.BasicFeeAllowance{
SpendLimit: atom,
Expiration: types.ExpiresAtHeight(334455),
}
basic2 := &types.BasicFeeAllowance{
SpendLimit: eth,
Expiration: types.ExpiresAtHeight(172436),
}
// let's set up some initial state here
err := k.GrantFeeAllowance(ctx, suite.addrs[0], suite.addrs[1], basic)
suite.Require().NoError(err)
err = k.GrantFeeAllowance(ctx, suite.addrs[0], suite.addrs[2], basic2)
suite.Require().NoError(err)
err = k.GrantFeeAllowance(ctx, suite.addrs[1], suite.addrs[2], basic)
suite.Require().NoError(err)
err = k.GrantFeeAllowance(ctx, suite.addrs[1], suite.addrs[3], basic)
suite.Require().NoError(err)
err = k.GrantFeeAllowance(ctx, suite.addrs[3], suite.addrs[0], basic2)
suite.Require().NoError(err)
// remove some, overwrite other
k.RevokeFeeAllowance(ctx, suite.addrs[0], suite.addrs[1])
k.RevokeFeeAllowance(ctx, suite.addrs[0], suite.addrs[2])
err = k.GrantFeeAllowance(ctx, suite.addrs[0], suite.addrs[2], basic)
suite.Require().NoError(err)
err = k.GrantFeeAllowance(ctx, suite.addrs[1], suite.addrs[2], basic2)
suite.Require().NoError(err)
// end state:
// addr -> addr3 (basic)
// addr2 -> addr3 (basic2), addr4(basic)
// addr4 -> addr (basic2)
// then lots of queries
cases := map[string]struct {
grantee sdk.AccAddress
granter sdk.AccAddress
allowance types.FeeAllowanceI
}{
"addr revoked": {
granter: suite.addrs[0],
grantee: suite.addrs[1],
},
"addr revoked and added": {
granter: suite.addrs[0],
grantee: suite.addrs[2],
allowance: basic,
},
"addr never there": {
granter: suite.addrs[0],
grantee: suite.addrs[3],
},
"addr modified": {
granter: suite.addrs[1],
grantee: suite.addrs[2],
allowance: basic2,
},
}
for name, tc := range cases {
tc := tc
suite.Run(name, func() {
allow := k.GetFeeAllowance(ctx, tc.granter, tc.grantee)
if tc.allowance == nil {
suite.Nil(allow)
return
}
suite.NotNil(allow)
suite.Equal(tc.allowance, allow)
})
}
grant1, err := types.NewFeeAllowanceGrant(suite.addrs[3], suite.addrs[0], basic2)
suite.NoError(err)
grant2, err := types.NewFeeAllowanceGrant(suite.addrs[1], suite.addrs[2], basic2)
suite.NoError(err)
grant3, err := types.NewFeeAllowanceGrant(suite.addrs[0], suite.addrs[2], basic)
suite.NoError(err)
allCases := map[string]struct {
grantee sdk.AccAddress
grants []types.FeeAllowanceGrant
}{
"addr2 has none": {
grantee: suite.addrs[1],
},
"addr has one": {
grantee: suite.addrs[0],
grants: []types.FeeAllowanceGrant{
grant1,
},
},
"addr3 has two": {
grantee: suite.addrs[2],
grants: []types.FeeAllowanceGrant{
grant3,
grant2,
},
},
}
for name, tc := range allCases {
tc := tc
suite.Run(name, func() {
var grants []types.FeeAllowanceGrant
err := k.IterateAllGranteeFeeAllowances(ctx, tc.grantee, func(grant types.FeeAllowanceGrant) bool {
grants = append(grants, grant)
return false
})
suite.NoError(err)
suite.Equal(tc.grants, grants)
})
}
}
func (suite *KeeperTestSuite) TestUseGrantedFee() {
ctx := suite.ctx
k := suite.app.FeeGrantKeeper
// some helpers
atom := sdk.NewCoins(sdk.NewInt64Coin("atom", 555))
eth := sdk.NewCoins(sdk.NewInt64Coin("eth", 123))
future := &types.BasicFeeAllowance{
SpendLimit: atom,
Expiration: types.ExpiresAtHeight(5678),
}
expired := &types.BasicFeeAllowance{
SpendLimit: eth,
Expiration: types.ExpiresAtHeight(55),
}
// for testing limits of the contract
hugeAtom := sdk.NewCoins(sdk.NewInt64Coin("atom", 9999))
_ = hugeAtom
smallAtom := sdk.NewCoins(sdk.NewInt64Coin("atom", 1))
_ = smallAtom
futureAfterSmall := &types.BasicFeeAllowance{
SpendLimit: sdk.NewCoins(sdk.NewInt64Coin("atom", 554)),
Expiration: types.ExpiresAtHeight(5678),
}
// then lots of queries
cases := map[string]struct {
grantee sdk.AccAddress
granter sdk.AccAddress
fee sdk.Coins
allowed bool
final types.FeeAllowanceI
}{
"use entire pot": {
granter: suite.addrs[0],
grantee: suite.addrs[1],
fee: atom,
allowed: true,
final: nil,
},
"expired and removed": {
granter: suite.addrs[0],
grantee: suite.addrs[2],
fee: eth,
allowed: false,
final: nil,
},
"too high": {
granter: suite.addrs[0],
grantee: suite.addrs[1],
fee: hugeAtom,
allowed: false,
final: future,
},
"use a little": {
granter: suite.addrs[0],
grantee: suite.addrs[1],
fee: smallAtom,
allowed: true,
final: futureAfterSmall,
},
}
for name, tc := range cases {
tc := tc
suite.Run(name, func() {
// let's set up some initial state here
// addr -> addr2 (future)
// addr -> addr3 (expired)
err := k.GrantFeeAllowance(ctx, suite.addrs[0], suite.addrs[1], future)
suite.Require().NoError(err)
err = k.GrantFeeAllowance(ctx, suite.addrs[0], suite.addrs[3], expired)
suite.Require().NoError(err)
err = k.UseGrantedFees(ctx, tc.granter, tc.grantee, tc.fee)
if tc.allowed {
suite.NoError(err)
} else {
suite.Error(err)
}
loaded := k.GetFeeAllowance(ctx, tc.granter, tc.grantee)
suite.Equal(tc.final, loaded)
})
}
}

View File

@ -0,0 +1,72 @@
package keeper
import (
"context"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
)
type msgServer struct {
Keeper
}
// NewMsgServerImpl returns an implementation of the feegrant MsgServer interface
// for the provided Keeper.
func NewMsgServerImpl(k Keeper) types.MsgServer {
return &msgServer{
Keeper: k,
}
}
var _ types.MsgServer = msgServer{}
func (k msgServer) GrantFeeAllowance(goCtx context.Context, msg *types.MsgGrantFeeAllowance) (*types.MsgGrantFeeAllowanceResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
grantee, err := sdk.AccAddressFromBech32(msg.Grantee)
if err != nil {
return nil, err
}
granter, err := sdk.AccAddressFromBech32(msg.Granter)
if err != nil {
return nil, err
}
// Checking for duplicate entry
f := k.Keeper.GetFeeAllowance(ctx, granter, grantee)
if f != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "fee allowance already exists")
}
err = k.Keeper.GrantFeeAllowance(ctx, granter, grantee, msg.GetFeeAllowanceI())
if err != nil {
return nil, err
}
return &types.MsgGrantFeeAllowanceResponse{}, nil
}
func (k msgServer) RevokeFeeAllowance(goCtx context.Context, msg *types.MsgRevokeFeeAllowance) (*types.MsgRevokeFeeAllowanceResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
grantee, err := sdk.AccAddressFromBech32(msg.Grantee)
if err != nil {
return nil, err
}
granter, err := sdk.AccAddressFromBech32(msg.Granter)
if err != nil {
return nil, err
}
err = k.Keeper.RevokeFeeAllowance(ctx, granter, grantee)
if err != nil {
return nil, err
}
return &types.MsgRevokeFeeAllowanceResponse{}, nil
}

210
x/feegrant/module.go Normal file
View File

@ -0,0 +1,210 @@
package feegrant
import (
"context"
"encoding/json"
"math/rand"
"github.com/gorilla/mux"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/spf13/cobra"
abci "github.com/tendermint/tendermint/abci/types"
sdkclient "github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
cdctypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/types/module"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/x/feegrant/client/cli"
"github.com/cosmos/cosmos-sdk/x/feegrant/keeper"
"github.com/cosmos/cosmos-sdk/x/feegrant/simulation"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
)
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
_ module.AppModuleSimulation = AppModule{}
)
// ----------------------------------------------------------------------------
// AppModuleBasic
// ----------------------------------------------------------------------------
// AppModuleBasic defines the basic application module used by the feegrant module.
type AppModuleBasic struct {
cdc codec.Marshaler
}
// Name returns the feegrant module's name.
func (AppModuleBasic) Name() string {
return types.ModuleName
}
// RegisterServices registers a gRPC query service to respond to the
// module-specific gRPC queries.
func (am AppModule) RegisterServices(cfg module.Configurator) {
types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper))
types.RegisterQueryServer(cfg.QueryServer(), am.keeper)
}
// RegisterLegacyAminoCodec registers the feegrant module's types for the given codec.
func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
}
// RegisterInterfaces registers the feegrant module's interface types
func (AppModuleBasic) RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
types.RegisterInterfaces(registry)
}
// LegacyQuerierHandler returns the feegrant module sdk.Querier.
func (am AppModule) LegacyQuerierHandler(legacyQuerierCdc *codec.LegacyAmino) sdk.Querier {
return nil
}
// DefaultGenesis returns default genesis state as raw bytes for the feegrant
// module.
func (AppModuleBasic) DefaultGenesis(cdc codec.JSONMarshaler) json.RawMessage {
return cdc.MustMarshalJSON(types.DefaultGenesisState())
}
// ValidateGenesis performs genesis state validation for the feegrant module.
func (a AppModuleBasic) ValidateGenesis(cdc codec.JSONMarshaler, config sdkclient.TxEncodingConfig, bz json.RawMessage) error {
var data types.GenesisState
if err := cdc.UnmarshalJSON(bz, &data); err != nil {
sdkerrors.Wrapf(err, "failed to unmarshal %s genesis state", types.ModuleName)
}
return types.ValidateGenesis(data)
}
// RegisterRESTRoutes registers the REST routes for the feegrant module.
func (AppModuleBasic) RegisterRESTRoutes(ctx sdkclient.Context, rtr *mux.Router) {}
// RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the feegrant module.
func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx sdkclient.Context, mux *runtime.ServeMux) {
types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx))
}
// GetTxCmd returns the root tx command for the feegrant module.
func (AppModuleBasic) GetTxCmd() *cobra.Command {
return cli.GetTxCmd()
}
// GetQueryCmd returns no root query command for the feegrant module.
func (AppModuleBasic) GetQueryCmd() *cobra.Command {
return cli.GetQueryCmd()
}
// ----------------------------------------------------------------------------
// AppModule
// ----------------------------------------------------------------------------
// AppModule implements an application module for the feegrant module.
type AppModule struct {
AppModuleBasic
keeper keeper.Keeper
accountKeeper types.AccountKeeper
bankKeeper types.BankKeeper
registry cdctypes.InterfaceRegistry
}
// NewAppModule creates a new AppModule object
func NewAppModule(cdc codec.Marshaler, ak types.AccountKeeper, bk types.BankKeeper, keeper keeper.Keeper, registry cdctypes.InterfaceRegistry) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{cdc: cdc},
keeper: keeper,
accountKeeper: ak,
bankKeeper: bk,
registry: registry,
}
}
// Name returns the feegrant module's name.
func (AppModule) Name() string {
return types.ModuleName
}
// RegisterInvariants registers the feegrant module invariants.
func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {}
// Route returns the message routing key for the feegrant module.
func (am AppModule) Route() sdk.Route {
return sdk.NewRoute(types.RouterKey, nil)
}
// NewHandler returns an sdk.Handler for the feegrant module.
func (am AppModule) NewHandler() sdk.Handler {
return nil
}
// QuerierRoute returns the feegrant module's querier route name.
func (AppModule) QuerierRoute() string {
return ""
}
// InitGenesis performs genesis initialization for the feegrant module. It returns
// no validator updates.
func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONMarshaler, bz json.RawMessage) []abci.ValidatorUpdate {
var gs types.GenesisState
cdc.MustUnmarshalJSON(bz, &gs)
InitGenesis(ctx, am.keeper, &gs)
return []abci.ValidatorUpdate{}
}
// ExportGenesis returns the exported genesis state as raw bytes for the feegrant
// module.
func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONMarshaler) json.RawMessage {
gs, err := ExportGenesis(ctx, am.keeper)
if err != nil {
panic(err)
}
return cdc.MustMarshalJSON(gs)
}
// BeginBlock returns the begin blocker for the feegrant module.
func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {}
// EndBlock returns the end blocker for the feegrant module. It returns no validator
// updates.
func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}
//____________________________________________________________________________
// AppModuleSimulation functions
// GenerateGenesisState creates a randomized GenState of the feegrant module.
func (AppModule) GenerateGenesisState(simState *module.SimulationState) {
simulation.RandomizedGenState(simState)
}
// ProposalContents returns all the feegrant content functions used to
// simulate governance proposals.
func (AppModule) ProposalContents(simState module.SimulationState) []simtypes.WeightedProposalContent {
return nil
}
// RandomizedParams creates randomized feegrant param changes for the simulator.
func (AppModule) RandomizedParams(r *rand.Rand) []simtypes.ParamChange {
return nil
}
// RegisterStoreDecoder registers a decoder for feegrant module's types
func (am AppModule) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
sdr[types.StoreKey] = simulation.NewDecodeStore(am.cdc)
}
// WeightedOperations returns all the feegrant module operations with their respective weights.
func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation {
protoCdc := codec.NewProtoCodec(am.registry)
return simulation.WeightedOperations(
simState.AppParams, simState.Cdc, am.accountKeeper, am.bankKeeper, am.keeper, protoCdc,
)
}

View File

@ -0,0 +1,26 @@
package simulation
import (
"bytes"
"fmt"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/types/kv"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
)
// NewDecodeStore returns a decoder function closure that unmarshals the KVPair's
// Value to the corresponding feegrant type.
func NewDecodeStore(cdc codec.Marshaler) func(kvA, kvB kv.Pair) string {
return func(kvA, kvB kv.Pair) string {
switch {
case bytes.Equal(kvA.Key[:1], types.FeeAllowanceKeyPrefix):
var grantA, grantB types.FeeAllowanceGrant
cdc.MustUnmarshalBinaryBare(kvA.Value, &grantA)
cdc.MustUnmarshalBinaryBare(kvB.Value, &grantB)
return fmt.Sprintf("%v\n%v", grantA, grantB)
default:
panic(fmt.Sprintf("invalid feegrant key %X", kvA.Key))
}
}
}

View File

@ -0,0 +1,62 @@
package simulation_test
import (
"fmt"
"testing"
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
"github.com/cosmos/cosmos-sdk/simapp"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/kv"
"github.com/cosmos/cosmos-sdk/x/feegrant/simulation"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
"github.com/stretchr/testify/require"
)
var (
granterPk = ed25519.GenPrivKey().PubKey()
granterAddr = sdk.AccAddress(granterPk.Address())
granteePk = ed25519.GenPrivKey().PubKey()
granteeAddr = sdk.AccAddress(granterPk.Address())
)
func TestDecodeStore(t *testing.T) {
cdc := simapp.MakeTestEncodingConfig().Marshaler
dec := simulation.NewDecodeStore(cdc)
grant, err := types.NewFeeAllowanceGrant(granterAddr, granteeAddr, &types.BasicFeeAllowance{
SpendLimit: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(100))),
})
require.NoError(t, err)
grantBz, err := cdc.MarshalBinaryBare(&grant)
require.NoError(t, err)
kvPairs := kv.Pairs{
Pairs: []kv.Pair{
{Key: []byte(types.FeeAllowanceKeyPrefix), Value: grantBz},
{Key: []byte{0x99}, Value: []byte{0x99}},
},
}
tests := []struct {
name string
expectedLog string
}{
{"Grant", fmt.Sprintf("%v\n%v", grant, grant)},
{"other", ""},
}
for i, tt := range tests {
i, tt := i, tt
t.Run(tt.name, func(t *testing.T) {
switch i {
case len(tests) - 1:
require.Panics(t, func() { dec(kvPairs.Pairs[i], kvPairs.Pairs[i]) }, tt.name)
default:
require.Equal(t, tt.expectedLog, dec(kvPairs.Pairs[i], kvPairs.Pairs[i]), tt.name)
}
})
}
}

View File

@ -0,0 +1,38 @@
package simulation
import (
"encoding/json"
"fmt"
"math/rand"
"github.com/cosmos/cosmos-sdk/types/module"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
)
// Simulation parameter constants
const feegrant = "feegrant"
// GenFeeGrants returns an empty slice of evidences.
func GenFeeGrants(_ *rand.Rand, _ []simtypes.Account) []types.FeeAllowanceGrant {
return []types.FeeAllowanceGrant{}
}
// RandomizedGenState generates a random GenesisState for feegrant
func RandomizedGenState(simState *module.SimulationState) {
var feegrants []types.FeeAllowanceGrant
simState.AppParams.GetOrGenerate(
simState.Cdc, feegrant, &feegrants, simState.Rand,
func(r *rand.Rand) { feegrants = GenFeeGrants(r, simState.Accounts) },
)
feegrantGenesis := types.NewGenesisState(feegrants)
bz, err := json.MarshalIndent(&feegrantGenesis, "", " ")
if err != nil {
panic(err)
}
fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, bz)
simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(feegrantGenesis)
}

View File

@ -0,0 +1,39 @@
package simulation_test
import (
"encoding/json"
"math/rand"
"testing"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/types/module"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/x/feegrant/simulation"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
"github.com/stretchr/testify/require"
)
func TestRandomizedGenState(t *testing.T) {
interfaceRegistry := codectypes.NewInterfaceRegistry()
cdc := codec.NewProtoCodec(interfaceRegistry)
s := rand.NewSource(1)
r := rand.New(s)
simState := module.SimulationState{
AppParams: make(simtypes.AppParams),
Cdc: cdc,
Rand: r,
NumBonded: 3,
Accounts: simtypes.RandomAccounts(r, 3),
InitialStake: 1000,
GenState: make(map[string]json.RawMessage),
}
simulation.RandomizedGenState(&simState)
var feegrantGenesis types.GenesisState
simState.Cdc.MustUnmarshalJSON(simState.GenState[types.ModuleName], &feegrantGenesis)
require.Len(t, feegrantGenesis.FeeAllowances, 0)
}

View File

@ -0,0 +1,200 @@
package simulation
import (
"context"
"math/rand"
"time"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/simapp/helpers"
simappparams "github.com/cosmos/cosmos-sdk/simapp/params"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/msgservice"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/x/feegrant/keeper"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
"github.com/cosmos/cosmos-sdk/x/simulation"
)
// Simulation operation weights constants
const (
OpWeightMsgGrantFeeAllowance = "op_weight_msg_grant_fee_allowance"
OpWeightMsgRevokeFeeAllowance = "op_weight_msg_grant_revoke_allowance"
TypeMsgGrantFeeAllowance = "/cosmos.feegrant.v1beta1.Msg/GrantFeeAllowance"
TypeMsgRevokeFeeAllowance = "/cosmos.feegrant.v1beta1.Msg/RevokeFeeAllowance"
)
func WeightedOperations(
appParams simtypes.AppParams, cdc codec.JSONMarshaler,
ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper,
protoCdc *codec.ProtoCodec,
) simulation.WeightedOperations {
var (
weightMsgGrantFeeAllowance int
weightMsgRevokeFeeAllowance int
)
appParams.GetOrGenerate(cdc, OpWeightMsgGrantFeeAllowance, &weightMsgGrantFeeAllowance, nil,
func(_ *rand.Rand) {
weightMsgGrantFeeAllowance = simappparams.DefaultWeightGrantFeeAllowance
},
)
appParams.GetOrGenerate(cdc, OpWeightMsgRevokeFeeAllowance, &weightMsgRevokeFeeAllowance, nil,
func(_ *rand.Rand) {
weightMsgRevokeFeeAllowance = simappparams.DefaultWeightRevokeFeeAllowance
},
)
return simulation.WeightedOperations{
simulation.NewWeightedOperation(
weightMsgGrantFeeAllowance,
SimulateMsgGrantFeeAllowance(ak, bk, k, protoCdc),
),
simulation.NewWeightedOperation(
weightMsgRevokeFeeAllowance,
SimulateMsgRevokeFeeAllowance(ak, bk, k, protoCdc),
),
}
}
// SimulateMsgGrantFeeAllowance generates MsgGrantFeeAllowance with random values.
func SimulateMsgGrantFeeAllowance(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper, protoCdc *codec.ProtoCodec) simtypes.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
granter, _ := simtypes.RandomAcc(r, accs)
grantee, _ := simtypes.RandomAcc(r, accs)
if grantee.Address.String() == granter.Address.String() {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgGrantFeeAllowance, "grantee and granter cannot be same"), nil, nil
}
f := k.GetFeeAllowance(ctx, granter.Address, grantee.Address)
if f != nil {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgGrantFeeAllowance, "fee allowance exists"), nil, nil
}
account := ak.GetAccount(ctx, granter.Address)
spendableCoins := bk.SpendableCoins(ctx, account.GetAddress())
fees, err := simtypes.RandomFees(r, ctx, spendableCoins)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgGrantFeeAllowance, err.Error()), nil, err
}
spendableCoins = spendableCoins.Sub(fees)
if spendableCoins.Empty() {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgGrantFeeAllowance, "unable to grant empty coins as SpendLimit"), nil, nil
}
msg, err := types.NewMsgGrantFeeAllowance(&types.BasicFeeAllowance{
SpendLimit: spendableCoins,
Expiration: types.ExpiresAtTime(ctx.BlockTime().Add(30 * time.Hour)),
}, granter.Address, grantee.Address)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgGrantFeeAllowance, err.Error()), nil, err
}
txGen := simappparams.MakeTestEncodingConfig().TxConfig
svcMsgClientConn := &msgservice.ServiceMsgClientConn{}
feegrantMsgClient := types.NewMsgClient(svcMsgClientConn)
_, err = feegrantMsgClient.GrantFeeAllowance(context.Background(), msg)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgGrantFeeAllowance, err.Error()), nil, err
}
tx, err := helpers.GenTx(
txGen,
svcMsgClientConn.GetMsgs(),
fees,
helpers.DefaultGenTxGas,
chainID,
[]uint64{account.GetAccountNumber()},
[]uint64{account.GetSequence()},
granter.PrivKey,
)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, TypeMsgGrantFeeAllowance, "unable to generate mock tx"), nil, err
}
_, _, err = app.Deliver(txGen.TxEncoder(), tx)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, svcMsgClientConn.GetMsgs()[0].Type(), "unable to deliver tx"), nil, err
}
return simtypes.NewOperationMsg(svcMsgClientConn.GetMsgs()[0], true, "", protoCdc), nil, err
}
}
// SimulateMsgRevokeFeeAllowance generates a MsgRevokeFeeAllowance with random values.
func SimulateMsgRevokeFeeAllowance(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper, protoCdc *codec.ProtoCodec) simtypes.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
hasGrant := false
var granterAddr sdk.AccAddress
var granteeAddr sdk.AccAddress
k.IterateAllFeeAllowances(ctx, func(grant types.FeeAllowanceGrant) bool {
granter, err := sdk.AccAddressFromBech32(grant.Granter)
if err != nil {
panic(err)
}
grantee, err := sdk.AccAddressFromBech32(grant.Grantee)
if err != nil {
panic(err)
}
granterAddr = granter
granteeAddr = grantee
hasGrant = true
return true
})
if !hasGrant {
return simtypes.NoOpMsg(types.ModuleName, TypeMsgRevokeFeeAllowance, "no grants"), nil, nil
}
granter, ok := simtypes.FindAccount(accs, granterAddr)
if !ok {
return simtypes.NoOpMsg(types.ModuleName, TypeMsgRevokeFeeAllowance, "Account not found"), nil, nil
}
account := ak.GetAccount(ctx, granter.Address)
spendableCoins := bk.SpendableCoins(ctx, account.GetAddress())
fees, err := simtypes.RandomFees(r, ctx, spendableCoins)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, TypeMsgRevokeFeeAllowance, err.Error()), nil, err
}
msg := types.NewMsgRevokeFeeAllowance(granterAddr, granteeAddr)
txGen := simappparams.MakeTestEncodingConfig().TxConfig
svcMsgClientConn := &msgservice.ServiceMsgClientConn{}
feegrantMsgClient := types.NewMsgClient(svcMsgClientConn)
_, err = feegrantMsgClient.RevokeFeeAllowance(context.Background(), &msg)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgGrantFeeAllowance, err.Error()), nil, err
}
tx, err := helpers.GenTx(
txGen,
svcMsgClientConn.GetMsgs(),
fees,
helpers.DefaultGenTxGas,
chainID,
[]uint64{account.GetAccountNumber()},
[]uint64{account.GetSequence()},
granter.PrivKey,
)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, TypeMsgRevokeFeeAllowance, err.Error()), nil, err
}
_, _, err = app.Deliver(txGen.TxEncoder(), tx)
return simtypes.NewOperationMsg(svcMsgClientConn.GetMsgs()[0], true, "", protoCdc), nil, err
}
}

View File

@ -0,0 +1,173 @@
package simulation_test
import (
"math/rand"
"testing"
"time"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/simapp"
simappparams "github.com/cosmos/cosmos-sdk/simapp/params"
sdk "github.com/cosmos/cosmos-sdk/types"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/x/feegrant/simulation"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
)
type SimTestSuite struct {
suite.Suite
ctx sdk.Context
app *simapp.SimApp
protoCdc *codec.ProtoCodec
}
func (suite *SimTestSuite) SetupTest() {
checkTx := false
app := simapp.Setup(checkTx)
suite.app = app
suite.ctx = app.BaseApp.NewContext(checkTx, tmproto.Header{})
suite.protoCdc = codec.NewProtoCodec(suite.app.InterfaceRegistry())
}
func (suite *SimTestSuite) getTestingAccounts(r *rand.Rand, n int) []simtypes.Account {
app, ctx := suite.app, suite.ctx
accounts := simtypes.RandomAccounts(r, n)
require := suite.Require()
initAmt := sdk.TokensFromConsensusPower(200)
initCoins := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, initAmt))
// add coins to the accounts
for _, account := range accounts {
acc := app.AccountKeeper.NewAccountWithAddress(ctx, account.Address)
app.AccountKeeper.SetAccount(ctx, acc)
err := app.BankKeeper.SetBalances(ctx, account.Address, initCoins)
require.NoError(err)
}
return accounts
}
func (suite *SimTestSuite) TestWeightedOperations() {
app, ctx := suite.app, suite.ctx
require := suite.Require()
ctx.WithChainID("test-chain")
cdc := app.AppCodec()
appParams := make(simtypes.AppParams)
weightesOps := simulation.WeightedOperations(
appParams, cdc, app.AccountKeeper,
app.BankKeeper, app.FeeGrantKeeper,
suite.protoCdc,
)
s := rand.NewSource(1)
r := rand.New(s)
accs := suite.getTestingAccounts(r, 3)
expected := []struct {
weight int
opMsgRoute string
opMsgName string
}{
{
simappparams.DefaultWeightGrantFeeAllowance,
types.ModuleName,
simulation.TypeMsgGrantFeeAllowance,
},
{
simappparams.DefaultWeightRevokeFeeAllowance,
types.ModuleName,
simulation.TypeMsgRevokeFeeAllowance,
},
}
for i, w := range weightesOps {
operationMsg, _, _ := w.Op()(r, app.BaseApp, ctx, accs, ctx.ChainID())
// the following checks are very much dependent from the ordering of the output given
// by WeightedOperations. if the ordering in WeightedOperations changes some tests
// will fail
require.Equal(expected[i].weight, w.Weight(), "weight should be the same")
require.Equal(expected[i].opMsgRoute, operationMsg.Route, "route should be the same")
require.Equal(expected[i].opMsgName, operationMsg.Name, "operation Msg name should be the same")
}
}
func (suite *SimTestSuite) TestSimulateMsgGrantFeeAllowance() {
app, ctx := suite.app, suite.ctx
require := suite.Require()
s := rand.NewSource(1)
r := rand.New(s)
accounts := suite.getTestingAccounts(r, 3)
// begin a new block
app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: app.LastBlockHeight() + 1, AppHash: app.LastCommitID().Hash}})
// execute operation
op := simulation.SimulateMsgGrantFeeAllowance(app.AccountKeeper, app.BankKeeper, app.FeeGrantKeeper, suite.protoCdc)
operationMsg, futureOperations, err := op(r, app.BaseApp, ctx, accounts, "")
require.NoError(err)
var msg types.MsgGrantFeeAllowance
suite.app.AppCodec().UnmarshalJSON(operationMsg.Msg, &msg)
require.True(operationMsg.OK)
require.Equal("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r", msg.Granter)
require.Equal("cosmos1p8wcgrjr4pjju90xg6u9cgq55dxwq8j7u4x9a0", msg.Grantee)
require.Len(futureOperations, 0)
}
func (suite *SimTestSuite) TestSimulateMsgRevokeFeeAllowance() {
app, ctx := suite.app, suite.ctx
require := suite.Require()
s := rand.NewSource(1)
r := rand.New(s)
accounts := suite.getTestingAccounts(r, 3)
// begin a new block
app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: suite.app.LastBlockHeight() + 1, AppHash: suite.app.LastCommitID().Hash}})
feeAmt := sdk.TokensFromConsensusPower(200000)
feeCoins := sdk.NewCoins(sdk.NewCoin("foo", feeAmt))
granter, grantee := accounts[0], accounts[1]
err := app.FeeGrantKeeper.GrantFeeAllowance(
ctx,
granter.Address,
grantee.Address,
&types.BasicFeeAllowance{
SpendLimit: feeCoins,
Expiration: types.ExpiresAtTime(ctx.BlockTime().Add(30 * time.Hour)),
},
)
require.NoError(err)
// execute operation
op := simulation.SimulateMsgRevokeFeeAllowance(app.AccountKeeper, app.BankKeeper, app.FeeGrantKeeper, suite.protoCdc)
operationMsg, futureOperations, err := op(r, app.BaseApp, ctx, accounts, "")
require.NoError(err)
var msg types.MsgRevokeFeeAllowance
suite.app.AppCodec().UnmarshalJSON(operationMsg.Msg, &msg)
require.True(operationMsg.OK)
require.Equal(granter.Address.String(), msg.Granter)
require.Equal(grantee.Address.String(), msg.Grantee)
require.Len(futureOperations, 0)
}
func TestSimTestSuite(t *testing.T) {
suite.Run(t, new(SimTestSuite))
}

View File

@ -0,0 +1,61 @@
package types
import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
var _ FeeAllowanceI = (*BasicFeeAllowance)(nil)
// Accept can use fee payment requested as well as timestamp/height of the current block
// to determine whether or not to process this. This is checked in
// Keeper.UseGrantedFees and the return values should match how it is handled there.
//
// If it returns an error, the fee payment is rejected, otherwise it is accepted.
// The FeeAllowance implementation is expected to update it's internal state
// and will be saved again after an acceptance.
//
// If remove is true (regardless of the error), the FeeAllowance will be deleted from storage
// (eg. when it is used up). (See call to RevokeFeeAllowance in Keeper.UseGrantedFees)
func (a *BasicFeeAllowance) Accept(fee sdk.Coins, blockTime time.Time, blockHeight int64) (bool, error) {
if a.Expiration.IsExpired(&blockTime, blockHeight) {
return true, sdkerrors.Wrap(ErrFeeLimitExpired, "basic allowance")
}
if a.SpendLimit != nil {
left, invalid := a.SpendLimit.SafeSub(fee)
if invalid {
return false, sdkerrors.Wrap(ErrFeeLimitExceeded, "basic allowance")
}
a.SpendLimit = left
return left.IsZero(), nil
}
return false, nil
}
// PrepareForExport will adjust the expiration based on export time. In particular,
// it will subtract the dumpHeight from any height-based expiration to ensure that
// the elapsed number of blocks this allowance is valid for is fixed.
func (a *BasicFeeAllowance) PrepareForExport(dumpTime time.Time, dumpHeight int64) FeeAllowanceI {
return &BasicFeeAllowance{
SpendLimit: a.SpendLimit,
Expiration: a.Expiration.PrepareForExport(dumpTime, dumpHeight),
}
}
// ValidateBasic implements FeeAllowance and enforces basic sanity checks
func (a BasicFeeAllowance) ValidateBasic() error {
if a.SpendLimit != nil {
if !a.SpendLimit.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "send amount is invalid: %s", a.SpendLimit)
}
if !a.SpendLimit.IsAllPositive() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, "spend limit must be positive")
}
}
return a.Expiration.ValidateBasic()
}

View File

@ -0,0 +1,140 @@
package types_test
import (
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBasicFeeValidAllow(t *testing.T) {
eth := sdk.NewCoins(sdk.NewInt64Coin("eth", 10))
atom := sdk.NewCoins(sdk.NewInt64Coin("atom", 555))
smallAtom := sdk.NewCoins(sdk.NewInt64Coin("atom", 43))
bigAtom := sdk.NewCoins(sdk.NewInt64Coin("atom", 1000))
leftAtom := sdk.NewCoins(sdk.NewInt64Coin("atom", 512))
cases := map[string]struct {
allow *types.BasicFeeAllowance
// all other checks are ignored if valid=false
fee sdk.Coins
blockTime time.Time
blockHeight int64
valid bool
accept bool
remove bool
remains sdk.Coins
}{
"empty": {
allow: &types.BasicFeeAllowance{},
valid: true,
accept: true,
},
"small fee without expire": {
allow: &types.BasicFeeAllowance{
SpendLimit: atom,
},
valid: true,
fee: smallAtom,
accept: true,
remove: false,
remains: leftAtom,
},
"all fee without expire": {
allow: &types.BasicFeeAllowance{
SpendLimit: smallAtom,
},
valid: true,
fee: smallAtom,
accept: true,
remove: true,
},
"wrong fee": {
allow: &types.BasicFeeAllowance{
SpendLimit: smallAtom,
},
valid: true,
fee: eth,
accept: false,
},
"non-expired": {
allow: &types.BasicFeeAllowance{
SpendLimit: atom,
Expiration: types.ExpiresAtHeight(100),
},
valid: true,
fee: smallAtom,
blockHeight: 85,
accept: true,
remove: false,
remains: leftAtom,
},
"expired": {
allow: &types.BasicFeeAllowance{
SpendLimit: atom,
Expiration: types.ExpiresAtHeight(100),
},
valid: true,
fee: smallAtom,
blockHeight: 121,
accept: false,
remove: true,
},
"fee more than allowed": {
allow: &types.BasicFeeAllowance{
SpendLimit: atom,
Expiration: types.ExpiresAtHeight(100),
},
valid: true,
fee: bigAtom,
blockHeight: 85,
accept: false,
},
"with out spend limit": {
allow: &types.BasicFeeAllowance{
Expiration: types.ExpiresAtHeight(100),
},
valid: true,
fee: bigAtom,
blockHeight: 85,
accept: true,
},
"expired no spend limit": {
allow: &types.BasicFeeAllowance{
Expiration: types.ExpiresAtHeight(100),
},
valid: true,
fee: bigAtom,
blockHeight: 120,
accept: false,
},
}
for name, stc := range cases {
tc := stc // to make scopelint happy
t.Run(name, func(t *testing.T) {
err := tc.allow.ValidateBasic()
if !tc.valid {
require.Error(t, err)
return
}
require.NoError(t, err)
// now try to deduct
remove, err := tc.allow.Accept(tc.fee, tc.blockTime, tc.blockHeight)
if !tc.accept {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tc.remove, remove)
if !remove {
assert.Equal(t, tc.allow.SpendLimit, tc.remains)
}
})
}
}

24
x/feegrant/types/codec.go Normal file
View File

@ -0,0 +1,24 @@
package types
import (
"github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/msgservice"
)
// RegisterInterfaces registers the interfaces types with the interface registry
func RegisterInterfaces(registry types.InterfaceRegistry) {
registry.RegisterImplementations((*sdk.MsgRequest)(nil),
&MsgGrantFeeAllowance{},
&MsgRevokeFeeAllowance{},
)
registry.RegisterInterface(
"cosmos.feegrant.v1beta1.FeeAllowanceI",
(*FeeAllowanceI)(nil),
&BasicFeeAllowance{},
&PeriodicFeeAllowance{},
)
msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc)
}

View File

@ -0,0 +1,21 @@
package types
import (
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// Codes for governance errors
const (
DefaultCodespace = ModuleName
)
var (
// ErrFeeLimitExceeded error if there are not enough allowance to cover the fees
ErrFeeLimitExceeded = sdkerrors.Register(DefaultCodespace, 2, "fee limit exceeded")
// ErrFeeLimitExpired error if the allowance has expired
ErrFeeLimitExpired = sdkerrors.Register(DefaultCodespace, 3, "fee allowance expired")
// ErrInvalidDuration error if the Duration is invalid or doesn't match the expiration
ErrInvalidDuration = sdkerrors.Register(DefaultCodespace, 4, "invalid duration")
// ErrNoAllowance error if there is no allowance for that pair
ErrNoAllowance = sdkerrors.Register(DefaultCodespace, 5, "no allowance")
)

View File

@ -0,0 +1,13 @@
package types
// evidence module events
const (
EventTypeUseFeeGrant = "use_feegrant"
EventTypeRevokeFeeGrant = "revoke_feegrant"
EventTypeSetFeeGrant = "set_feegrant"
AttributeKeyGranter = "granter"
AttributeKeyGrantee = "grantee"
AttributeValueCategory = ModuleName
)

View File

@ -0,0 +1,23 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
auth "github.com/cosmos/cosmos-sdk/x/auth/types"
// supply "github.com/cosmos/cosmos-sdk/x/supply/exported"
)
// AccountKeeper defines the expected auth Account Keeper (noalias)
type AccountKeeper interface {
GetModuleAddress(moduleName string) sdk.AccAddress
GetModuleAccount(ctx sdk.Context, moduleName string) auth.ModuleAccountI
NewAccountWithAddress(ctx sdk.Context, addr sdk.AccAddress) auth.AccountI
GetAccount(ctx sdk.Context, addr sdk.AccAddress) auth.AccountI
SetAccount(ctx sdk.Context, acc auth.AccountI)
}
// BankKeeper defines the expected supply Keeper (noalias)
type BankKeeper interface {
SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
}

View File

@ -0,0 +1,138 @@
package types
import (
"time"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// ExpiresAtTime creates an expiration at the given time
func ExpiresAtTime(t time.Time) ExpiresAt {
return ExpiresAt{
Sum: &ExpiresAt_Time{
Time: &t,
},
}
}
// ExpiresAtHeight creates an expiration at the given height
func ExpiresAtHeight(h int64) ExpiresAt {
return ExpiresAt{
&ExpiresAt_Height{
Height: h,
},
}
}
// ValidateBasic performs basic sanity checks.
// Note that empty expiration is allowed
func (e ExpiresAt) ValidateBasic() error {
if e.HasDefinedTime() && e.GetHeight() != 0 {
return sdkerrors.Wrap(ErrInvalidDuration, "both time and height are set")
}
if e.GetHeight() < 0 {
return sdkerrors.Wrap(ErrInvalidDuration, "negative height")
}
return nil
}
// Undefined returns true for an uninitialized struct
func (e ExpiresAt) Undefined() bool {
return (e.GetTime() == nil || e.GetTime().Unix() <= 0) && e.GetHeight() == 0
}
// HasDefinedTime returns true if `ExpiresAt` has valid time
func (e ExpiresAt) HasDefinedTime() bool {
t := e.GetTime()
return t != nil && t.Unix() > 0
}
// FastForward produces a new Expiration with the time or height set to the
// new value, depending on what was set on the original expiration
func (e ExpiresAt) FastForward(t time.Time, h int64) ExpiresAt {
if e.HasDefinedTime() {
return ExpiresAtTime(t)
}
return ExpiresAtHeight(h)
}
// IsExpired returns if the time or height is *equal to* or greater
// than the defined expiration point. Note that it is expired upon
// an exact match.
//
// Note a "zero" ExpiresAt is never expired
func (e ExpiresAt) IsExpired(t *time.Time, h int64) bool {
if e.HasDefinedTime() && t.After(*e.GetTime()) {
return true
}
return e.GetHeight() != 0 && h >= e.GetHeight()
}
// IsCompatible returns true iff the two use the same units.
// If false, they cannot be added.
func (e ExpiresAt) IsCompatible(d Duration) bool {
if e.HasDefinedTime() {
return d.GetDuration() != nil && d.GetDuration().Seconds() > float64(0)
}
return d.GetBlocks() > 0
}
// Step will increase the expiration point by one Duration
// It returns an error if the Duration is incompatible
func (e ExpiresAt) Step(d Duration) (ExpiresAt, error) {
if !e.IsCompatible(d) {
return ExpiresAt{}, sdkerrors.Wrap(ErrInvalidDuration, "expiration time and provided duration have different units")
}
if e.HasDefinedTime() {
return ExpiresAtTime(e.GetTime().Add(*d.GetDuration())), nil
}
return ExpiresAtHeight(e.GetHeight() + int64(d.GetBlocks())), nil
}
// MustStep is like Step, but panics on error
func (e ExpiresAt) MustStep(d Duration) ExpiresAt {
res, err := e.Step(d)
if err != nil {
panic(err)
}
return res
}
// PrepareForExport will deduct the dumpHeight from the expiration, so when this is
// reloaded after a hard fork, the actual number of allowed blocks is constant
func (e ExpiresAt) PrepareForExport(dumpTime time.Time, dumpHeight int64) ExpiresAt {
if e.GetHeight() != 0 {
return ExpiresAtHeight(e.GetHeight() - dumpHeight)
}
return ExpiresAt{}
}
// ClockDuration creates an Duration by clock time
func ClockDuration(d time.Duration) Duration {
return Duration{Sum: &Duration_Duration{
Duration: &d,
}}
}
// BlockDuration creates an Duration by block height
func BlockDuration(h uint64) Duration {
return Duration{Sum: &Duration_Blocks{
Blocks: h,
}}
}
// ValidateBasic performs basic sanity checks
// Note that exactly one must be set and it must be positive
func (d Duration) ValidateBasic() error {
if d.GetBlocks() == 0 && d.GetDuration() == nil {
return sdkerrors.Wrap(ErrInvalidDuration, "neither time and height are set")
}
if d.GetBlocks() != 0 && d.GetDuration() != nil && d.GetDuration().Seconds() != float64(0) {
return sdkerrors.Wrap(ErrInvalidDuration, "both time and height are set")
}
if d.GetDuration() != nil && d.GetDuration().Seconds() < 0 {
return sdkerrors.Wrap(ErrInvalidDuration, "negative clock step")
}
return nil
}

View File

@ -0,0 +1,161 @@
package types_test
import (
"testing"
"time"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestExpiresAt(t *testing.T) {
now := time.Now()
cases := map[string]struct {
example types.ExpiresAt
valid bool
zero bool
before types.ExpiresAt
after types.ExpiresAt
}{
"basic": {
example: types.ExpiresAtHeight(100),
valid: true,
before: types.ExpiresAtHeight(50),
after: types.ExpiresAtHeight(122),
},
"zero": {
example: types.ExpiresAt{},
zero: true,
valid: true,
before: types.ExpiresAtHeight(1),
},
"match height": {
example: types.ExpiresAtHeight(1000),
valid: true,
before: types.ExpiresAtHeight(999),
after: types.ExpiresAtHeight(1000),
},
"match time": {
example: types.ExpiresAtTime(now),
valid: true,
before: types.ExpiresAtTime(now.Add(-1 * time.Second)),
after: types.ExpiresAtTime(now.Add(1 * time.Second)),
},
}
for name, stc := range cases {
tc := stc // to make scopelint happy
t.Run(name, func(t *testing.T) {
err := tc.example.ValidateBasic()
assert.Equal(t, tc.zero, tc.example.Undefined())
if !tc.valid {
require.Error(t, err)
return
}
require.NoError(t, err)
if !tc.before.Undefined() {
assert.Equal(t, false, tc.example.IsExpired(tc.before.GetTime(), tc.before.GetHeight()))
}
if !tc.after.Undefined() {
assert.Equal(t, true, tc.example.IsExpired(tc.after.GetTime(), tc.after.GetHeight()))
}
})
}
}
func TestDurationValid(t *testing.T) {
now := time.Now()
cases := map[string]struct {
period types.Duration
valid bool
compatible types.ExpiresAt
incompatible types.ExpiresAt
}{
"basic height": {
period: types.BlockDuration(100),
valid: true,
compatible: types.ExpiresAtHeight(50),
incompatible: types.ExpiresAtTime(now),
},
"basic time": {
period: types.ClockDuration(time.Hour),
valid: true,
compatible: types.ExpiresAtTime(now),
incompatible: types.ExpiresAtHeight(50),
},
"zero": {
period: types.Duration{},
valid: false,
},
"negative clock": {
period: types.ClockDuration(-1 * time.Hour),
valid: false,
},
}
for name, stc := range cases {
tc := stc // to make scopelint happy
t.Run(name, func(t *testing.T) {
err := tc.period.ValidateBasic()
if !tc.valid {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, true, tc.compatible.IsCompatible(tc.period))
assert.Equal(t, false, tc.incompatible.IsCompatible(tc.period))
})
}
}
func TestDurationStep(t *testing.T) {
now := time.Now()
cases := map[string]struct {
expires types.ExpiresAt
period types.Duration
valid bool
result types.ExpiresAt
}{
"add height": {
expires: types.ExpiresAtHeight(789),
period: types.BlockDuration(100),
valid: true,
result: types.ExpiresAtHeight(889),
},
"add time": {
expires: types.ExpiresAtTime(now),
period: types.ClockDuration(time.Hour),
valid: true,
result: types.ExpiresAtTime(now.Add(time.Hour)),
},
"mismatch": {
expires: types.ExpiresAtHeight(789),
period: types.ClockDuration(time.Hour),
valid: false,
},
}
for name, stc := range cases {
tc := stc // to make scopelint happy
t.Run(name, func(t *testing.T) {
err := tc.period.ValidateBasic()
require.NoError(t, err)
err = tc.expires.ValidateBasic()
require.NoError(t, err)
next, err := tc.expires.Step(tc.period)
if !tc.valid {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tc.result, next)
})
}
}

File diff suppressed because it is too large Load Diff

32
x/feegrant/types/fees.go Normal file
View File

@ -0,0 +1,32 @@
package types
import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// FeeAllowance implementations are tied to a given fee delegator and delegatee,
// and are used to enforce fee grant limits.
type FeeAllowanceI interface {
// Accept can use fee payment requested as well as timestamp/height of the current block
// to determine whether or not to process this. This is checked in
// Keeper.UseGrantedFees and the return values should match how it is handled there.
//
// If it returns an error, the fee payment is rejected, otherwise it is accepted.
// The FeeAllowance implementation is expected to update it's internal state
// and will be saved again after an acceptance.
//
// If remove is true (regardless of the error), the FeeAllowance will be deleted from storage
// (eg. when it is used up). (See call to RevokeFeeAllowance in Keeper.UseGrantedFees)
Accept(fee sdk.Coins, blockTime time.Time, blockHeight int64) (remove bool, err error)
// If we export fee allowances the timing info will be quite off (eg. go from height 100000 to 0)
// This callback allows the fee-allowance to change it's state and return a copy that is adjusted
// given the time and height of the actual dump (may safely return self if no changes needed)
PrepareForExport(dumpTime time.Time, dumpHeight int64) FeeAllowanceI
// ValidateBasic should evaluate this FeeAllowance for internal consistency.
// Don't allow negative amounts, or negative periods for example.
ValidateBasic() error
}

View File

@ -0,0 +1,40 @@
package types
import "github.com/cosmos/cosmos-sdk/codec/types"
var _ types.UnpackInterfacesMessage = GenesisState{}
// NewGenesisState creates new GenesisState object
func NewGenesisState(entries []FeeAllowanceGrant) *GenesisState {
return &GenesisState{
FeeAllowances: entries,
}
}
// ValidateGenesis ensures all grants in the genesis state are valid
func ValidateGenesis(data GenesisState) error {
for _, f := range data.FeeAllowances {
err := f.GetFeeGrant().ValidateBasic()
if err != nil {
return err
}
}
return nil
}
// DefaultGenesisState returns default state for feegrant module.
func DefaultGenesisState() *GenesisState {
return &GenesisState{}
}
// UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces
func (data GenesisState) UnpackInterfaces(unpacker types.AnyUnpacker) error {
for _, f := range data.FeeAllowances {
err := f.UnpackInterfaces(unpacker)
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,333 @@
// Code generated by protoc-gen-gogo. DO NOT EDIT.
// source: cosmos/feegrant/v1beta1/genesis.proto
package types
import (
fmt "fmt"
_ "github.com/gogo/protobuf/gogoproto"
proto "github.com/gogo/protobuf/proto"
io "io"
math "math"
math_bits "math/bits"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
// GenesisState contains a set of fee allowances, persisted from the store
type GenesisState struct {
FeeAllowances []FeeAllowanceGrant `protobuf:"bytes,1,rep,name=fee_allowances,json=feeAllowances,proto3" json:"fee_allowances"`
}
func (m *GenesisState) Reset() { *m = GenesisState{} }
func (m *GenesisState) String() string { return proto.CompactTextString(m) }
func (*GenesisState) ProtoMessage() {}
func (*GenesisState) Descriptor() ([]byte, []int) {
return fileDescriptor_ac719d2d0954d1bf, []int{0}
}
func (m *GenesisState) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *GenesisState) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
if deterministic {
return xxx_messageInfo_GenesisState.Marshal(b, m, deterministic)
} else {
b = b[:cap(b)]
n, err := m.MarshalToSizedBuffer(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
}
func (m *GenesisState) XXX_Merge(src proto.Message) {
xxx_messageInfo_GenesisState.Merge(m, src)
}
func (m *GenesisState) XXX_Size() int {
return m.Size()
}
func (m *GenesisState) XXX_DiscardUnknown() {
xxx_messageInfo_GenesisState.DiscardUnknown(m)
}
var xxx_messageInfo_GenesisState proto.InternalMessageInfo
func (m *GenesisState) GetFeeAllowances() []FeeAllowanceGrant {
if m != nil {
return m.FeeAllowances
}
return nil
}
func init() {
proto.RegisterType((*GenesisState)(nil), "cosmos.feegrant.v1beta1.GenesisState")
}
func init() {
proto.RegisterFile("cosmos/feegrant/v1beta1/genesis.proto", fileDescriptor_ac719d2d0954d1bf)
}
var fileDescriptor_ac719d2d0954d1bf = []byte{
// 221 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x4d, 0xce, 0x2f, 0xce,
0xcd, 0x2f, 0xd6, 0x4f, 0x4b, 0x4d, 0x4d, 0x2f, 0x4a, 0xcc, 0x2b, 0xd1, 0x2f, 0x33, 0x4c, 0x4a,
0x2d, 0x49, 0x34, 0xd4, 0x4f, 0x4f, 0xcd, 0x4b, 0x2d, 0xce, 0x2c, 0xd6, 0x2b, 0x28, 0xca, 0x2f,
0xc9, 0x17, 0x12, 0x87, 0x28, 0xd3, 0x83, 0x29, 0xd3, 0x83, 0x2a, 0x93, 0x12, 0x49, 0xcf, 0x4f,
0xcf, 0x07, 0xab, 0xd1, 0x07, 0xb1, 0x20, 0xca, 0xa5, 0xd4, 0x70, 0x99, 0x0a, 0xd7, 0x0f, 0x56,
0xa7, 0x94, 0xce, 0xc5, 0xe3, 0x0e, 0xb1, 0x27, 0xb8, 0x24, 0xb1, 0x24, 0x55, 0x28, 0x9c, 0x8b,
0x2f, 0x2d, 0x35, 0x35, 0x3e, 0x31, 0x27, 0x27, 0xbf, 0x3c, 0x31, 0x2f, 0x39, 0xb5, 0x58, 0x82,
0x51, 0x81, 0x59, 0x83, 0xdb, 0x48, 0x4b, 0x0f, 0x87, 0xfd, 0x7a, 0x6e, 0xa9, 0xa9, 0x8e, 0x30,
0xd5, 0xee, 0x20, 0x19, 0x27, 0x96, 0x13, 0xf7, 0xe4, 0x19, 0x82, 0x78, 0xd3, 0x90, 0x24, 0x8a,
0x9d, 0xdc, 0x4f, 0x3c, 0x92, 0x63, 0xbc, 0xf0, 0x48, 0x8e, 0xf1, 0xc1, 0x23, 0x39, 0xc6, 0x09,
0x8f, 0xe5, 0x18, 0x2e, 0x3c, 0x96, 0x63, 0xb8, 0xf1, 0x58, 0x8e, 0x21, 0x4a, 0x37, 0x3d, 0xb3,
0x24, 0xa3, 0x34, 0x49, 0x2f, 0x39, 0x3f, 0x57, 0x1f, 0xea, 0x6a, 0x08, 0xa5, 0x5b, 0x9c, 0x92,
0xad, 0x5f, 0x81, 0xf0, 0x42, 0x49, 0x65, 0x41, 0x6a, 0x71, 0x12, 0x1b, 0xd8, 0xe1, 0xc6, 0x80,
0x00, 0x00, 0x00, 0xff, 0xff, 0xaf, 0x53, 0x6e, 0xc5, 0x38, 0x01, 0x00, 0x00,
}
func (m *GenesisState) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalToSizedBuffer(dAtA[:size])
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *GenesisState) MarshalTo(dAtA []byte) (int, error) {
size := m.Size()
return m.MarshalToSizedBuffer(dAtA[:size])
}
func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i := len(dAtA)
_ = i
var l int
_ = l
if len(m.FeeAllowances) > 0 {
for iNdEx := len(m.FeeAllowances) - 1; iNdEx >= 0; iNdEx-- {
{
size, err := m.FeeAllowances[iNdEx].MarshalToSizedBuffer(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = encodeVarintGenesis(dAtA, i, uint64(size))
}
i--
dAtA[i] = 0xa
}
}
return len(dAtA) - i, nil
}
func encodeVarintGenesis(dAtA []byte, offset int, v uint64) int {
offset -= sovGenesis(v)
base := offset
for v >= 1<<7 {
dAtA[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
dAtA[offset] = uint8(v)
return base
}
func (m *GenesisState) Size() (n int) {
if m == nil {
return 0
}
var l int
_ = l
if len(m.FeeAllowances) > 0 {
for _, e := range m.FeeAllowances {
l = e.Size()
n += 1 + l + sovGenesis(uint64(l))
}
}
return n
}
func sovGenesis(x uint64) (n int) {
return (math_bits.Len64(x|1) + 6) / 7
}
func sozGenesis(x uint64) (n int) {
return sovGenesis(uint64((x << 1) ^ uint64((int64(x) >> 63))))
}
func (m *GenesisState) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenesis
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: GenesisState: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: GenesisState: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field FeeAllowances", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenesis
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthGenesis
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthGenesis
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.FeeAllowances = append(m.FeeAllowances, FeeAllowanceGrant{})
if err := m.FeeAllowances[len(m.FeeAllowances)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipGenesis(dAtA[iNdEx:])
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
return ErrInvalidLengthGenesis
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func skipGenesis(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0
depth := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowGenesis
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
wireType := int(wire & 0x7)
switch wireType {
case 0:
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowGenesis
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
iNdEx++
if dAtA[iNdEx-1] < 0x80 {
break
}
}
case 1:
iNdEx += 8
case 2:
var length int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowGenesis
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
length |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if length < 0 {
return 0, ErrInvalidLengthGenesis
}
iNdEx += length
case 3:
depth++
case 4:
if depth == 0 {
return 0, ErrUnexpectedEndOfGroupGenesis
}
depth--
case 5:
iNdEx += 4
default:
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
}
if iNdEx < 0 {
return 0, ErrInvalidLengthGenesis
}
if depth == 0 {
return iNdEx, nil
}
}
return 0, io.ErrUnexpectedEOF
}
var (
ErrInvalidLengthGenesis = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowGenesis = fmt.Errorf("proto: integer overflow")
ErrUnexpectedEndOfGroupGenesis = fmt.Errorf("proto: unexpected end of group")
)

92
x/feegrant/types/grant.go Normal file
View File

@ -0,0 +1,92 @@
package types
import (
"time"
"github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
proto "github.com/gogo/protobuf/proto"
)
var (
_ types.UnpackInterfacesMessage = &FeeAllowanceGrant{}
)
// NewFeeAllowanceGrant creates a new FeeAllowanceGrant.
//nolint:interfacer
func NewFeeAllowanceGrant(granter, grantee sdk.AccAddress, feeAllowance FeeAllowanceI) (FeeAllowanceGrant, error) {
msg, ok := feeAllowance.(proto.Message)
if !ok {
return FeeAllowanceGrant{}, sdkerrors.Wrapf(sdkerrors.ErrPackAny, "cannot proto marshal %T", feeAllowance)
}
any, err := types.NewAnyWithValue(msg)
if err != nil {
return FeeAllowanceGrant{}, err
}
return FeeAllowanceGrant{
Granter: granter.String(),
Grantee: grantee.String(),
Allowance: any,
}, nil
}
// ValidateBasic performs basic validation on
// FeeAllowanceGrant
func (a FeeAllowanceGrant) ValidateBasic() error {
if a.Granter == "" {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "missing granter address")
}
if a.Grantee == "" {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "missing grantee address")
}
if a.Grantee == a.Granter {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "cannot self-grant fee authorization")
}
return a.GetFeeGrant().ValidateBasic()
}
// GetFeeGrant unpacks allowance
func (a FeeAllowanceGrant) GetFeeGrant() FeeAllowanceI {
allowance, ok := a.Allowance.GetCachedValue().(FeeAllowanceI)
if !ok {
return nil
}
return allowance
}
// UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces
func (a FeeAllowanceGrant) UnpackInterfaces(unpacker types.AnyUnpacker) error {
var allowance FeeAllowanceI
return unpacker.UnpackAny(a.Allowance, &allowance)
}
// PrepareForExport will make all needed changes to the allowance to prepare to be
// re-imported at height 0, and return a copy of this grant.
func (a FeeAllowanceGrant) PrepareForExport(dumpTime time.Time, dumpHeight int64) FeeAllowanceGrant {
feegrant := a.GetFeeGrant().PrepareForExport(dumpTime, dumpHeight)
if feegrant == nil {
return FeeAllowanceGrant{}
}
granter, err := sdk.AccAddressFromBech32(a.Granter)
if err != nil {
return FeeAllowanceGrant{}
}
grantee, err := sdk.AccAddressFromBech32(a.Grantee)
if err != nil {
return FeeAllowanceGrant{}
}
grant, err := NewFeeAllowanceGrant(granter, grantee, feegrant)
if err != nil {
return FeeAllowanceGrant{}
}
return grant
}

View File

@ -0,0 +1,99 @@
package types_test
import (
"testing"
"github.com/cosmos/cosmos-sdk/simapp"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGrant(t *testing.T) {
app := simapp.Setup(false)
addr, err := sdk.AccAddressFromBech32("cosmos1qk93t4j0yyzgqgt6k5qf8deh8fq6smpn3ntu3x")
require.NoError(t, err)
addr2, err := sdk.AccAddressFromBech32("cosmos1p9qh4ldfd6n0qehujsal4k7g0e37kel90rc4ts")
require.NoError(t, err)
atom := sdk.NewCoins(sdk.NewInt64Coin("atom", 555))
goodGrant, err := types.NewFeeAllowanceGrant(addr2, addr, &types.BasicFeeAllowance{
SpendLimit: atom,
Expiration: types.ExpiresAtHeight(100),
})
require.NoError(t, err)
noGranteeGrant, err := types.NewFeeAllowanceGrant(addr2, nil, &types.BasicFeeAllowance{
SpendLimit: atom,
Expiration: types.ExpiresAtHeight(100),
})
require.NoError(t, err)
noGranterGrant, err := types.NewFeeAllowanceGrant(nil, addr, &types.BasicFeeAllowance{
SpendLimit: atom,
Expiration: types.ExpiresAtHeight(100),
})
require.NoError(t, err)
selfGrant, err := types.NewFeeAllowanceGrant(addr2, addr2, &types.BasicFeeAllowance{
SpendLimit: atom,
Expiration: types.ExpiresAtHeight(100),
})
require.NoError(t, err)
badAllowanceGrant, err := types.NewFeeAllowanceGrant(addr2, addr, &types.BasicFeeAllowance{
SpendLimit: atom,
Expiration: types.ExpiresAtHeight(-1),
})
require.NoError(t, err)
cdc := app.AppCodec()
// RegisterLegacyAminoCodec(cdc)
cases := map[string]struct {
grant types.FeeAllowanceGrant
valid bool
}{
"good": {
grant: goodGrant,
valid: true,
},
"no grantee": {
grant: noGranteeGrant,
},
"no granter": {
grant: noGranterGrant,
},
"self-grant": {
grant: selfGrant,
},
"bad allowance": {
grant: badAllowanceGrant,
},
}
for name, tc := range cases {
tc := tc
t.Run(name, func(t *testing.T) {
err := tc.grant.ValidateBasic()
if !tc.valid {
require.Error(t, err)
return
}
require.NoError(t, err)
// if it is valid, let's try to serialize, deserialize, and make sure it matches
bz, err := cdc.MarshalBinaryBare(&tc.grant)
require.NoError(t, err)
var loaded types.FeeAllowanceGrant
err = cdc.UnmarshalBinaryBare(bz, &loaded)
require.NoError(t, err)
err = loaded.ValidateBasic()
require.NoError(t, err)
assert.Equal(t, tc.grant, loaded)
})
}
}

35
x/feegrant/types/key.go Normal file
View File

@ -0,0 +1,35 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
const (
// ModuleName is the module name constant used in many places
ModuleName = "feegrant"
// StoreKey is the store key string for supply
StoreKey = ModuleName
// RouterKey is the message route for supply
RouterKey = ModuleName
// QuerierRoute is the querier route for supply
QuerierRoute = ModuleName
)
var (
// FeeAllowanceKeyPrefix is the set of the kvstore for fee allowance data
FeeAllowanceKeyPrefix = []byte{0x00}
)
// FeeAllowanceKey is the canonical key to store a grant from granter to grantee
// We store by grantee first to allow searching by everyone who granted to you
func FeeAllowanceKey(granter sdk.AccAddress, grantee sdk.AccAddress) []byte {
return append(append(FeeAllowanceKeyPrefix, grantee.Bytes()...), granter.Bytes()...)
}
// FeeAllowancePrefixByGrantee returns a prefix to scan for all grants to this given address.
func FeeAllowancePrefixByGrantee(grantee sdk.AccAddress) []byte {
return append(FeeAllowanceKeyPrefix, grantee.Bytes()...)
}

101
x/feegrant/types/msgs.go Normal file
View File

@ -0,0 +1,101 @@
package types
import (
"github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/gogo/protobuf/proto"
)
var (
_, _ sdk.MsgRequest = &MsgGrantFeeAllowance{}, &MsgRevokeFeeAllowance{}
_ types.UnpackInterfacesMessage = &MsgGrantFeeAllowance{}
)
// feegrant message types
const (
TypeMsgGrantFeeAllowance = "grant_fee_allowance"
TypeMsgRevokeFeeAllowance = "revoke_fee_allowance"
)
// NewMsgGrantFeeAllowance creates a new MsgGrantFeeAllowance.
//nolint:interfacer
func NewMsgGrantFeeAllowance(feeAllowance FeeAllowanceI, granter, grantee sdk.AccAddress) (*MsgGrantFeeAllowance, error) {
msg, ok := feeAllowance.(proto.Message)
if !ok {
return nil, sdkerrors.Wrapf(sdkerrors.ErrPackAny, "cannot proto marshal %T", msg)
}
any, err := types.NewAnyWithValue(msg)
if err != nil {
return nil, err
}
return &MsgGrantFeeAllowance{
Granter: granter.String(),
Grantee: grantee.String(),
Allowance: any,
}, nil
}
// ValidateBasic implements the sdk.Msg interface.
func (msg MsgGrantFeeAllowance) ValidateBasic() error {
if msg.Granter == "" {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "missing granter address")
}
if msg.Grantee == "" {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "missing grantee address")
}
if msg.Grantee == msg.Granter {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "cannot self-grant fee authorization")
}
return msg.GetFeeAllowanceI().ValidateBasic()
}
func (msg MsgGrantFeeAllowance) GetSigners() []sdk.AccAddress {
granter, err := sdk.AccAddressFromBech32(msg.Granter)
if err != nil {
panic(err)
}
return []sdk.AccAddress{granter}
}
// GetFeeAllowanceI returns unpacked FeeAllowance
func (msg MsgGrantFeeAllowance) GetFeeAllowanceI() FeeAllowanceI {
allowance, ok := msg.Allowance.GetCachedValue().(FeeAllowanceI)
if !ok {
return nil
}
return allowance
}
// UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces
func (msg MsgGrantFeeAllowance) UnpackInterfaces(unpacker types.AnyUnpacker) error {
var allowance FeeAllowanceI
return unpacker.UnpackAny(msg.Allowance, &allowance)
}
//nolint:interfacer
func NewMsgRevokeFeeAllowance(granter sdk.AccAddress, grantee sdk.AccAddress) MsgRevokeFeeAllowance {
return MsgRevokeFeeAllowance{Granter: granter.String(), Grantee: grantee.String()}
}
func (msg MsgRevokeFeeAllowance) ValidateBasic() error {
if msg.Granter == "" {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "missing granter address")
}
if msg.Grantee == "" {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "missing grantee address")
}
return nil
}
func (msg MsgRevokeFeeAllowance) GetSigners() []sdk.AccAddress {
granter, err := sdk.AccAddressFromBech32(msg.Granter)
if err != nil {
panic(err)
}
return []sdk.AccAddress{granter}
}

View File

@ -0,0 +1,120 @@
package types
import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
var _ FeeAllowanceI = (*PeriodicFeeAllowance)(nil)
// Accept can use fee payment requested as well as timestamp/height of the current block
// to determine whether or not to process this. This is checked in
// Keeper.UseGrantedFees and the return values should match how it is handled there.
//
// If it returns an error, the fee payment is rejected, otherwise it is accepted.
// The FeeAllowance implementation is expected to update it's internal state
// and will be saved again after an acceptance.
//
// If remove is true (regardless of the error), the FeeAllowance will be deleted from storage
// (eg. when it is used up). (See call to RevokeFeeAllowance in Keeper.UseGrantedFees)
func (a *PeriodicFeeAllowance) Accept(fee sdk.Coins, blockTime time.Time, blockHeight int64) (bool, error) {
if a.Basic.Expiration.IsExpired(&blockTime, blockHeight) {
return true, sdkerrors.Wrap(ErrFeeLimitExpired, "absolute limit")
}
a.tryResetPeriod(blockTime, blockHeight)
// deduct from both the current period and the max amount
var isNeg bool
a.PeriodCanSpend, isNeg = a.PeriodCanSpend.SafeSub(fee)
if isNeg {
return false, sdkerrors.Wrap(ErrFeeLimitExceeded, "period limit")
}
if a.Basic.SpendLimit != nil {
a.Basic.SpendLimit, isNeg = a.Basic.SpendLimit.SafeSub(fee)
if isNeg {
return false, sdkerrors.Wrap(ErrFeeLimitExceeded, "absolute limit")
}
return a.Basic.SpendLimit.IsZero(), nil
}
return false, nil
}
// tryResetPeriod will check if the PeriodReset has been hit. If not, it is a no-op.
// If we hit the reset period, it will top up the PeriodCanSpend amount to
// min(PeriodicSpendLimit, a.Basic.SpendLimit) so it is never more than the maximum allowed.
// It will also update the PeriodReset. If we are within one Period, it will update from the
// last PeriodReset (eg. if you always do one tx per day, it will always reset the same time)
// If we are more then one period out (eg. no activity in a week), reset is one Period from the execution of this method
func (a *PeriodicFeeAllowance) tryResetPeriod(blockTime time.Time, blockHeight int64) {
if !a.PeriodReset.Undefined() && !a.PeriodReset.IsExpired(&blockTime, blockHeight) {
return
}
// set CanSpend to the lesser of PeriodSpendLimit and the TotalLimit
if _, isNeg := a.Basic.SpendLimit.SafeSub(a.PeriodSpendLimit); isNeg {
a.PeriodCanSpend = a.Basic.SpendLimit
} else {
a.PeriodCanSpend = a.PeriodSpendLimit
}
// If we are within the period, step from expiration (eg. if you always do one tx per day, it will always reset the same time)
// If we are more then one period out (eg. no activity in a week), reset is one period from this time
a.PeriodReset = a.PeriodReset.MustStep(a.Period)
if a.PeriodReset.IsExpired(&blockTime, blockHeight) {
a.PeriodReset = a.PeriodReset.FastForward(blockTime, blockHeight).MustStep(a.Period)
}
}
// PrepareForExport will adjust the expiration based on export time. In particular,
// it will subtract the dumpHeight from any height-based expiration to ensure that
// the elapsed number of blocks this allowance is valid for is fixed.
// (For PeriodReset and Basic.Expiration)
func (a *PeriodicFeeAllowance) PrepareForExport(dumpTime time.Time, dumpHeight int64) FeeAllowanceI {
return &PeriodicFeeAllowance{
Basic: BasicFeeAllowance{
SpendLimit: a.Basic.SpendLimit,
Expiration: a.Basic.Expiration.PrepareForExport(dumpTime, dumpHeight),
},
PeriodSpendLimit: a.PeriodSpendLimit,
PeriodCanSpend: a.PeriodCanSpend,
Period: a.Period,
PeriodReset: a.PeriodReset.PrepareForExport(dumpTime, dumpHeight),
}
}
// ValidateBasic implements FeeAllowance and enforces basic sanity checks
func (a PeriodicFeeAllowance) ValidateBasic() error {
if err := a.Basic.ValidateBasic(); err != nil {
return err
}
if !a.PeriodSpendLimit.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "spend amount is invalid: %s", a.PeriodSpendLimit)
}
if !a.PeriodSpendLimit.IsAllPositive() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, "spend limit must be positive")
}
if !a.PeriodCanSpend.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "can spend amount is invalid: %s", a.PeriodCanSpend)
}
// We allow 0 for CanSpend
if a.PeriodCanSpend.IsAnyNegative() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, "can spend must not be negative")
}
// ensure PeriodSpendLimit can be subtracted from total (same coin types)
if a.Basic.SpendLimit != nil && !a.PeriodSpendLimit.DenomsSubsetOf(a.Basic.SpendLimit) {
return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, "period spend limit has different currency than basic spend limit")
}
// check times
if err := a.Period.ValidateBasic(); err != nil {
return err
}
return a.PeriodReset.ValidateBasic()
}

View File

@ -0,0 +1,205 @@
package types_test
import (
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPeriodicFeeValidAllow(t *testing.T) {
atom := sdk.NewCoins(sdk.NewInt64Coin("atom", 555))
smallAtom := sdk.NewCoins(sdk.NewInt64Coin("atom", 43))
leftAtom := sdk.NewCoins(sdk.NewInt64Coin("atom", 512))
oneAtom := sdk.NewCoins(sdk.NewInt64Coin("atom", 1))
eth := sdk.NewCoins(sdk.NewInt64Coin("eth", 1))
cases := map[string]struct {
allow types.PeriodicFeeAllowance
// all other checks are ignored if valid=false
fee sdk.Coins
blockTime time.Time
blockHeight int64
valid bool
accept bool
remove bool
remains sdk.Coins
remainsPeriod sdk.Coins
periodReset types.ExpiresAt
}{
"empty": {
allow: types.PeriodicFeeAllowance{},
valid: false,
},
"only basic": {
allow: types.PeriodicFeeAllowance{
Basic: types.BasicFeeAllowance{
SpendLimit: atom,
Expiration: types.ExpiresAtHeight(100),
},
},
valid: false,
},
"empty basic": {
allow: types.PeriodicFeeAllowance{
Period: types.BlockDuration(10),
PeriodSpendLimit: smallAtom,
PeriodReset: types.ExpiresAtHeight(70),
},
blockHeight: 75,
valid: true,
accept: true,
remove: false,
periodReset: types.ExpiresAtHeight(80),
},
"mismatched currencies": {
allow: types.PeriodicFeeAllowance{
Basic: types.BasicFeeAllowance{
SpendLimit: atom,
Expiration: types.ExpiresAtHeight(100),
},
Period: types.BlockDuration(10),
PeriodSpendLimit: eth,
},
valid: false,
},
"first time": {
allow: types.PeriodicFeeAllowance{
Basic: types.BasicFeeAllowance{
SpendLimit: atom,
Expiration: types.ExpiresAtHeight(100),
},
Period: types.BlockDuration(10),
PeriodSpendLimit: smallAtom,
},
valid: true,
fee: smallAtom,
blockHeight: 75,
accept: true,
remove: false,
remainsPeriod: nil,
remains: leftAtom,
periodReset: types.ExpiresAtHeight(85),
},
"same period": {
allow: types.PeriodicFeeAllowance{
Basic: types.BasicFeeAllowance{
SpendLimit: atom,
Expiration: types.ExpiresAtHeight(100),
},
Period: types.BlockDuration(10),
PeriodReset: types.ExpiresAtHeight(80),
PeriodSpendLimit: leftAtom,
PeriodCanSpend: smallAtom,
},
valid: true,
fee: smallAtom,
blockHeight: 75,
accept: true,
remove: false,
remainsPeriod: nil,
remains: leftAtom,
periodReset: types.ExpiresAtHeight(80),
},
"step one period": {
allow: types.PeriodicFeeAllowance{
Basic: types.BasicFeeAllowance{
SpendLimit: atom,
Expiration: types.ExpiresAtHeight(100),
},
Period: types.BlockDuration(10),
PeriodReset: types.ExpiresAtHeight(70),
PeriodSpendLimit: leftAtom,
},
valid: true,
fee: leftAtom,
blockHeight: 75,
accept: true,
remove: false,
remainsPeriod: nil,
remains: smallAtom,
periodReset: types.ExpiresAtHeight(80), // one step from last reset, not now
},
"step limited by global allowance": {
allow: types.PeriodicFeeAllowance{
Basic: types.BasicFeeAllowance{
SpendLimit: smallAtom,
Expiration: types.ExpiresAtHeight(100),
},
Period: types.BlockDuration(10),
PeriodReset: types.ExpiresAtHeight(70),
PeriodSpendLimit: atom,
},
valid: true,
fee: oneAtom,
blockHeight: 75,
accept: true,
remove: false,
remainsPeriod: smallAtom.Sub(oneAtom),
remains: smallAtom.Sub(oneAtom),
periodReset: types.ExpiresAtHeight(80), // one step from last reset, not now
},
"expired": {
allow: types.PeriodicFeeAllowance{
Basic: types.BasicFeeAllowance{
SpendLimit: atom,
Expiration: types.ExpiresAtHeight(100),
},
Period: types.BlockDuration(10),
PeriodSpendLimit: smallAtom,
},
valid: true,
fee: smallAtom,
blockHeight: 101,
accept: false,
remove: true,
},
"over period limit": {
allow: types.PeriodicFeeAllowance{
Basic: types.BasicFeeAllowance{
SpendLimit: atom,
Expiration: types.ExpiresAtHeight(100),
},
Period: types.BlockDuration(10),
PeriodReset: types.ExpiresAtHeight(80),
PeriodSpendLimit: leftAtom,
PeriodCanSpend: smallAtom,
},
valid: true,
fee: leftAtom,
blockHeight: 70,
accept: false,
remove: true,
},
}
for name, stc := range cases {
tc := stc // to make scopelint happy
t.Run(name, func(t *testing.T) {
err := tc.allow.ValidateBasic()
if !tc.valid {
require.Error(t, err)
return
}
require.NoError(t, err)
// now try to deduct
remove, err := tc.allow.Accept(tc.fee, tc.blockTime, tc.blockHeight)
if !tc.accept {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tc.remove, remove)
if !remove {
assert.Equal(t, tc.remains, tc.allow.Basic.SpendLimit)
assert.Equal(t, tc.remainsPeriod, tc.allow.PeriodCanSpend)
assert.Equal(t, tc.periodReset, tc.allow.PeriodReset)
}
})
}
}

1172
x/feegrant/types/query.pb.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,322 @@
// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
// source: cosmos/feegrant/v1beta1/query.proto
/*
Package types is a reverse proxy.
It translates gRPC into RESTful JSON APIs.
*/
package types
import (
"context"
"io"
"net/http"
"github.com/golang/protobuf/descriptor"
"github.com/golang/protobuf/proto"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/grpc-ecosystem/grpc-gateway/utilities"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/status"
)
// Suppress "imported and not used" errors
var _ codes.Code
var _ io.Reader
var _ status.Status
var _ = runtime.String
var _ = utilities.NewDoubleArray
var _ = descriptor.ForMessage
func request_Query_FeeAllowance_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QueryFeeAllowanceRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["granter"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "granter")
}
protoReq.Granter, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "granter", err)
}
val, ok = pathParams["grantee"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "grantee")
}
protoReq.Grantee, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "grantee", err)
}
msg, err := client.FeeAllowance(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Query_FeeAllowance_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QueryFeeAllowanceRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["granter"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "granter")
}
protoReq.Granter, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "granter", err)
}
val, ok = pathParams["grantee"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "grantee")
}
protoReq.Grantee, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "grantee", err)
}
msg, err := server.FeeAllowance(ctx, &protoReq)
return msg, metadata, err
}
var (
filter_Query_FeeAllowances_0 = &utilities.DoubleArray{Encoding: map[string]int{"grantee": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}
)
func request_Query_FeeAllowances_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QueryFeeAllowancesRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["grantee"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "grantee")
}
protoReq.Grantee, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "grantee", err)
}
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Query_FeeAllowances_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.FeeAllowances(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Query_FeeAllowances_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QueryFeeAllowancesRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["grantee"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "grantee")
}
protoReq.Grantee, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "grantee", err)
}
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Query_FeeAllowances_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.FeeAllowances(ctx, &protoReq)
return msg, metadata, err
}
// RegisterQueryHandlerServer registers the http handlers for service Query to "mux".
// UnaryRPC :call QueryServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
// Note that using this registration option will cause many gRPC library features (such as grpc.SendHeader, etc) to stop working. Consider using RegisterQueryHandlerFromEndpoint instead.
func RegisterQueryHandlerServer(ctx context.Context, mux *runtime.ServeMux, server QueryServer) error {
mux.Handle("GET", pattern_Query_FeeAllowance_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Query_FeeAllowance_0(rctx, inboundMarshaler, server, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Query_FeeAllowance_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Query_FeeAllowances_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Query_FeeAllowances_0(rctx, inboundMarshaler, server, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Query_FeeAllowances_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
// RegisterQueryHandlerFromEndpoint is same as RegisterQueryHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterQueryHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.Dial(endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
return RegisterQueryHandler(ctx, mux, conn)
}
// RegisterQueryHandler registers the http handlers for service Query to "mux".
// The handlers forward requests to the grpc endpoint over "conn".
func RegisterQueryHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
return RegisterQueryHandlerClient(ctx, mux, NewQueryClient(conn))
}
// RegisterQueryHandlerClient registers the http handlers for service Query
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "QueryClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "QueryClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "QueryClient" to call the correct interceptors.
func RegisterQueryHandlerClient(ctx context.Context, mux *runtime.ServeMux, client QueryClient) error {
mux.Handle("GET", pattern_Query_FeeAllowance_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Query_FeeAllowance_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Query_FeeAllowance_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Query_FeeAllowances_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Query_FeeAllowances_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Query_FeeAllowances_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
var (
pattern_Query_FeeAllowance_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4, 1, 0, 4, 1, 5, 5}, []string{"cosmos", "feegrant", "v1beta1", "fee_allowance", "granter", "grantee"}, "", runtime.AssumeColonVerbOpt(true)))
pattern_Query_FeeAllowances_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"cosmos", "feegrant", "v1beta1", "fee_allowances", "grantee"}, "", runtime.AssumeColonVerbOpt(true)))
)
var (
forward_Query_FeeAllowance_0 = runtime.ForwardResponseMessage
forward_Query_FeeAllowances_0 = runtime.ForwardResponseMessage
)

1033
x/feegrant/types/tx.pb.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@ import (
"github.com/cosmos/cosmos-sdk/x/mint/types"
)
// NewDecodeStore returns a decoder function closure that umarshals the KVPair's
// NewDecodeStore returns a decoder function closure that unmarshals the KVPair's
// Value to the corresponding mint type.
func NewDecodeStore(cdc codec.Marshaler) func(kvA, kvB kv.Pair) string {
return func(kvA, kvB kv.Pair) string {