-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #17 from irisnet/ht/feegrant
Ht/feegrant
- Loading branch information
Showing
4 changed files
with
231 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package feegrant | ||
|
||
import ( | ||
"fmt" | ||
"github.com/gogo/protobuf/proto" | ||
|
||
"github.com/irisnet/core-sdk-go/common/codec/types" | ||
sdk "github.com/irisnet/core-sdk-go/types" | ||
) | ||
|
||
// TODO: Revisit this once we have propoer gas fee framework. | ||
// Tracking issues https://github.com/cosmos/cosmos-sdk/issues/9054, https://github.com/cosmos/cosmos-sdk/discussions/9072 | ||
const ( | ||
gasCostPerIteration = uint64(10) | ||
) | ||
|
||
var _ FeeAllowanceI = (*AllowedMsgAllowance)(nil) | ||
var _ types.UnpackInterfacesMessage = (*AllowedMsgAllowance)(nil) | ||
|
||
// UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces | ||
func (a *AllowedMsgAllowance) UnpackInterfaces(unpacker types.AnyUnpacker) error { | ||
var allowance FeeAllowanceI | ||
return unpacker.UnpackAny(a.Allowance, &allowance) | ||
} | ||
|
||
// NewAllowedMsgAllowance creates new filtered fee allowance. | ||
func NewAllowedMsgAllowance(allowance FeeAllowanceI, allowedMsgs []string) (*AllowedMsgAllowance, error) { | ||
msg, ok := allowance.(proto.Message) | ||
if !ok { | ||
return nil, sdk.Wrap(fmt.Errorf("failed packing protobuf message to Any : cannot proto marshal %T", msg)) | ||
} | ||
any, err := types.NewAnyWithValue(msg) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &AllowedMsgAllowance{ | ||
Allowance: any, | ||
AllowedMessages: allowedMsgs, | ||
}, nil | ||
} | ||
|
||
// GetAllowance returns allowed fee allowance. | ||
func (a *AllowedMsgAllowance) GetAllowance() (FeeAllowanceI, error) { | ||
allowance, ok := a.Allowance.GetCachedValue().(FeeAllowanceI) | ||
if !ok { | ||
return nil, sdk.Wrap(fmt.Errorf("message not allowed : %s", "failed to get allowance")) | ||
} | ||
|
||
return allowance, nil | ||
} | ||
|
||
// SetAllowance sets allowed fee allowance. | ||
func (a *AllowedMsgAllowance) SetAllowance(allowance FeeAllowanceI) error { | ||
var err error | ||
a.Allowance, err = types.NewAnyWithValue(allowance.(proto.Message)) | ||
if err != nil { | ||
return sdk.Wrap(fmt.Errorf("failed packing protobuf message to Any : cannot proto marshal %T", allowance)) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Accept method checks for the filtered messages has valid expiry | ||
func (a *AllowedMsgAllowance) Accept(ctx sdk.Context, fee sdk.Coins, msgs []sdk.Msg) (bool, error) { | ||
if !a.allMsgTypesAllowed(ctx, msgs) { | ||
return false, sdk.Wrap(fmt.Errorf("message not allowed : %s", "message does not exist in allowed messages")) | ||
} | ||
|
||
allowance, err := a.GetAllowance() | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
remove, err := allowance.Accept(ctx, fee, msgs) | ||
if err == nil && !remove { | ||
if err = a.SetAllowance(allowance); err != nil { | ||
return false, err | ||
} | ||
} | ||
return remove, err | ||
} | ||
|
||
func (a *AllowedMsgAllowance) allowedMsgsToMap(ctx sdk.Context) map[string]bool { | ||
msgsMap := make(map[string]bool, len(a.AllowedMessages)) | ||
for _, msg := range a.AllowedMessages { | ||
ctx.GasMeter().ConsumeGas(gasCostPerIteration, "check msg") | ||
msgsMap[msg] = true | ||
} | ||
|
||
return msgsMap | ||
} | ||
|
||
func (a *AllowedMsgAllowance) allMsgTypesAllowed(ctx sdk.Context, msgs []sdk.Msg) bool { | ||
msgsMap := a.allowedMsgsToMap(ctx) | ||
|
||
for _, msg := range msgs { | ||
ctx.GasMeter().ConsumeGas(gasCostPerIteration, "check msg") | ||
if !msgsMap[sdk.MsgTypeURL(msg)] { | ||
return false | ||
} | ||
} | ||
|
||
return true | ||
} | ||
|
||
// ValidateBasic implements FeeAllowance and enforces basic sanity checks | ||
func (a *AllowedMsgAllowance) ValidateBasic() error { | ||
if a.Allowance == nil { | ||
return sdk.Wrap(fmt.Errorf("no allowance : %s", "allowance should not be empty")) | ||
} | ||
if len(a.AllowedMessages) == 0 { | ||
return sdk.Wrap(fmt.Errorf("allowed messages are empty : %s", "allowed messages shouldn't be empty")) | ||
} | ||
|
||
allowance, err := a.GetAllowance() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return allowance.ValidateBasic() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
package feegrant | ||
|
||
import ( | ||
"fmt" | ||
sdk "github.com/irisnet/core-sdk-go/types" | ||
"time" | ||
) | ||
|
||
var _ FeeAllowanceI = (*PeriodicAllowance)(nil) | ||
|
||
func (m *PeriodicAllowance) Accept(ctx sdk.Context, fee sdk.Coins, _ []sdk.Msg) (remove bool, err error) { | ||
blockTime := ctx.BlockTime() | ||
|
||
if m.Basic.Expiration != nil && blockTime.After(*m.Basic.Expiration) { | ||
return true, sdk.Wrap(fmt.Errorf("fee allowance expired : %s", "absolute limit")) | ||
} | ||
|
||
m.tryResetPeriod(blockTime) | ||
|
||
// deduct from both the current period and the max amount | ||
var isNeg bool | ||
m.PeriodCanSpend, isNeg = m.PeriodCanSpend.SafeSub(fee) | ||
if isNeg { | ||
return false, sdk.Wrap(fmt.Errorf("fee limit exceeded : %s", "period limit")) | ||
} | ||
|
||
if m.Basic.SpendLimit != nil { | ||
m.Basic.SpendLimit, isNeg = m.Basic.SpendLimit.SafeSub(fee) | ||
if isNeg { | ||
return false, sdk.Wrap(fmt.Errorf("fee limit exceeded : %s", "absolute limit")) | ||
} | ||
|
||
return m.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(PeriodSpendLimit, 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 (m *PeriodicAllowance) tryResetPeriod(blockTime time.Time) { | ||
if blockTime.Before(m.PeriodReset) { | ||
return | ||
} | ||
|
||
// set PeriodCanSpend to the lesser of Basic.SpendLimit and PeriodSpendLimit | ||
if _, isNeg := m.Basic.SpendLimit.SafeSub(m.PeriodSpendLimit); isNeg && !m.Basic.SpendLimit.Empty() { | ||
m.PeriodCanSpend = m.Basic.SpendLimit | ||
} else { | ||
m.PeriodCanSpend = m.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 m week), reset is one period from this time | ||
m.PeriodReset = m.PeriodReset.Add(m.Period) | ||
if blockTime.After(m.PeriodReset) { | ||
m.PeriodReset = blockTime.Add(m.Period) | ||
} | ||
} | ||
|
||
func (m *PeriodicAllowance) ValidateBasic() error { | ||
if err := m.Basic.ValidateBasic(); err != nil { | ||
return err | ||
} | ||
|
||
if !m.PeriodSpendLimit.IsValid() { | ||
return sdk.Wrap(fmt.Errorf("invalid coins , spend amount is invalid: %v", m.PeriodSpendLimit)) | ||
} | ||
if !m.PeriodSpendLimit.IsAllPositive() { | ||
return sdk.Wrap(fmt.Errorf("invalid coins : %s", "spend limit must be positive")) | ||
} | ||
if !m.PeriodCanSpend.IsValid() { | ||
return sdk.Wrap(fmt.Errorf("invalid coins , can spend amount is invalid: %v", m.PeriodCanSpend)) | ||
} | ||
// We allow 0 for CanSpend | ||
if m.PeriodCanSpend.IsAnyNegative() { | ||
return sdk.Wrap(fmt.Errorf("invalid coins : %s", "can spend must not be negative")) | ||
} | ||
|
||
// ensure PeriodSpendLimit can be subtracted from total (same coin types) | ||
if m.Basic.SpendLimit != nil && !m.PeriodSpendLimit.DenomsSubsetOf(m.Basic.SpendLimit) { | ||
return sdk.Wrap(fmt.Errorf("invalid coins : %s", "period spend limit has different currency than basic spend limit")) | ||
} | ||
|
||
// check times | ||
if m.Period.Seconds() < 0 { | ||
return sdk.Wrap(fmt.Errorf("invalid duration : %s", "negative clock step")) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters