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:
parent
3b8e0f9387
commit
d97e7907f1
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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"];
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 {}
|
|
@ -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(),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -20,4 +20,8 @@ const (
|
|||
DefaultWeightCommunitySpendProposal int = 5
|
||||
DefaultWeightTextProposal int = 5
|
||||
DefaultWeightParamChangeProposal int = 5
|
||||
|
||||
// feegrant
|
||||
DefaultWeightGrantFeeAllowance int = 100
|
||||
DefaultWeightRevokeFeeAllowance int = 100
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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")
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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()...)
|
||||
}
|
|
@ -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}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
)
|
File diff suppressed because it is too large
Load Diff
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue