324 lines
9.6 KiB
Go
324 lines
9.6 KiB
Go
package keeper
|
|
|
|
import (
|
|
"github.com/cosmos/cosmos-sdk/codec"
|
|
"github.com/cosmos/cosmos-sdk/store/prefix"
|
|
storetypes "github.com/cosmos/cosmos-sdk/store/types"
|
|
"github.com/cosmos/cosmos-sdk/telemetry"
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
"github.com/cosmos/cosmos-sdk/types/address"
|
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
|
"github.com/cosmos/cosmos-sdk/x/bank/types"
|
|
paramtypes "github.com/cosmos/cosmos-sdk/x/params/types"
|
|
)
|
|
|
|
// SendKeeper defines a module interface that facilitates the transfer of coins
|
|
// between accounts without the possibility of creating coins.
|
|
type SendKeeper interface {
|
|
ViewKeeper
|
|
|
|
InputOutputCoins(ctx sdk.Context, inputs []types.Input, outputs []types.Output) error
|
|
SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error
|
|
|
|
GetParams(ctx sdk.Context) types.Params
|
|
SetParams(ctx sdk.Context, params types.Params)
|
|
|
|
IsSendEnabledCoin(ctx sdk.Context, coin sdk.Coin) bool
|
|
IsSendEnabledCoins(ctx sdk.Context, coins ...sdk.Coin) error
|
|
|
|
BlockedAddr(addr sdk.AccAddress) bool
|
|
}
|
|
|
|
var _ SendKeeper = (*BaseSendKeeper)(nil)
|
|
|
|
// BaseSendKeeper only allows transfers between accounts without the possibility of
|
|
// creating coins. It implements the SendKeeper interface.
|
|
type BaseSendKeeper struct {
|
|
BaseViewKeeper
|
|
|
|
cdc codec.BinaryCodec
|
|
ak types.AccountKeeper
|
|
storeKey storetypes.StoreKey
|
|
paramSpace paramtypes.Subspace
|
|
|
|
// list of addresses that are restricted from receiving transactions
|
|
blockedAddrs map[string]bool
|
|
}
|
|
|
|
func NewBaseSendKeeper(
|
|
cdc codec.BinaryCodec, storeKey storetypes.StoreKey, ak types.AccountKeeper, paramSpace paramtypes.Subspace, blockedAddrs map[string]bool,
|
|
) BaseSendKeeper {
|
|
|
|
return BaseSendKeeper{
|
|
BaseViewKeeper: NewBaseViewKeeper(cdc, storeKey, ak),
|
|
cdc: cdc,
|
|
ak: ak,
|
|
storeKey: storeKey,
|
|
paramSpace: paramSpace,
|
|
blockedAddrs: blockedAddrs,
|
|
}
|
|
}
|
|
|
|
// GetParams returns the total set of bank parameters.
|
|
func (k BaseSendKeeper) GetParams(ctx sdk.Context) (params types.Params) {
|
|
k.paramSpace.GetParamSet(ctx, ¶ms)
|
|
return params
|
|
}
|
|
|
|
// SetParams sets the total set of bank parameters.
|
|
func (k BaseSendKeeper) SetParams(ctx sdk.Context, params types.Params) {
|
|
k.paramSpace.SetParamSet(ctx, ¶ms)
|
|
}
|
|
|
|
// InputOutputCoins performs multi-send functionality. It accepts a series of
|
|
// inputs that correspond to a series of outputs. It returns an error if the
|
|
// inputs and outputs don't lineup or if any single transfer of tokens fails.
|
|
func (k BaseSendKeeper) InputOutputCoins(ctx sdk.Context, inputs []types.Input, outputs []types.Output) error {
|
|
// Safety check ensuring that when sending coins the keeper must maintain the
|
|
// Check supply invariant and validity of Coins.
|
|
if err := types.ValidateInputsOutputs(inputs, outputs); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, in := range inputs {
|
|
inAddress, err := sdk.AccAddressFromBech32(in.Address)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = k.subUnlockedCoins(ctx, inAddress, in.Coins)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx.EventManager().EmitEvent(
|
|
sdk.NewEvent(
|
|
sdk.EventTypeMessage,
|
|
sdk.NewAttribute(types.AttributeKeySender, in.Address),
|
|
),
|
|
)
|
|
}
|
|
|
|
for _, out := range outputs {
|
|
outAddress, err := sdk.AccAddressFromBech32(out.Address)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = k.addCoins(ctx, outAddress, out.Coins)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx.EventManager().EmitEvent(
|
|
sdk.NewEvent(
|
|
types.EventTypeTransfer,
|
|
sdk.NewAttribute(types.AttributeKeyRecipient, out.Address),
|
|
sdk.NewAttribute(sdk.AttributeKeyAmount, out.Coins.String()),
|
|
),
|
|
)
|
|
|
|
// Create account if recipient does not exist.
|
|
//
|
|
// NOTE: This should ultimately be removed in favor a more flexible approach
|
|
// such as delegated fee messages.
|
|
accExists := k.ak.HasAccount(ctx, outAddress)
|
|
if !accExists {
|
|
defer telemetry.IncrCounter(1, "new", "account")
|
|
k.ak.SetAccount(ctx, k.ak.NewAccountWithAddress(ctx, outAddress))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SendCoins transfers amt coins from a sending account to a receiving account.
|
|
// An error is returned upon failure.
|
|
func (k BaseSendKeeper) SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error {
|
|
err := k.subUnlockedCoins(ctx, fromAddr, amt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = k.addCoins(ctx, toAddr, amt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create account if recipient does not exist.
|
|
//
|
|
// NOTE: This should ultimately be removed in favor a more flexible approach
|
|
// such as delegated fee messages.
|
|
accExists := k.ak.HasAccount(ctx, toAddr)
|
|
if !accExists {
|
|
defer telemetry.IncrCounter(1, "new", "account")
|
|
k.ak.SetAccount(ctx, k.ak.NewAccountWithAddress(ctx, toAddr))
|
|
}
|
|
|
|
ctx.EventManager().EmitEvents(sdk.Events{
|
|
sdk.NewEvent(
|
|
types.EventTypeTransfer,
|
|
sdk.NewAttribute(types.AttributeKeyRecipient, toAddr.String()),
|
|
sdk.NewAttribute(types.AttributeKeySender, fromAddr.String()),
|
|
sdk.NewAttribute(sdk.AttributeKeyAmount, amt.String()),
|
|
),
|
|
sdk.NewEvent(
|
|
sdk.EventTypeMessage,
|
|
sdk.NewAttribute(types.AttributeKeySender, fromAddr.String()),
|
|
),
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// subUnlockedCoins removes the unlocked amt coins of the given account. An error is
|
|
// returned if the resulting balance is negative or the initial amount is invalid.
|
|
// A coin_spent event is emitted after.
|
|
func (k BaseSendKeeper) subUnlockedCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) error {
|
|
if !amt.IsValid() {
|
|
return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, amt.String())
|
|
}
|
|
|
|
lockedCoins := k.LockedCoins(ctx, addr)
|
|
|
|
for _, coin := range amt {
|
|
balance := k.GetBalance(ctx, addr, coin.Denom)
|
|
locked := sdk.NewCoin(coin.Denom, lockedCoins.AmountOf(coin.Denom))
|
|
spendable := balance.Sub(locked)
|
|
|
|
_, hasNeg := sdk.Coins{spendable}.SafeSub(sdk.Coins{coin})
|
|
if hasNeg {
|
|
return sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "%s is smaller than %s", spendable, coin)
|
|
}
|
|
|
|
newBalance := balance.Sub(coin)
|
|
|
|
err := k.setBalance(ctx, addr, newBalance)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// emit coin spent event
|
|
ctx.EventManager().EmitEvent(
|
|
types.NewCoinSpentEvent(addr, amt),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
// addCoins increase the addr balance by the given amt. Fails if the provided amt is invalid.
|
|
// It emits a coin received event.
|
|
func (k BaseSendKeeper) addCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) error {
|
|
if !amt.IsValid() {
|
|
return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, amt.String())
|
|
}
|
|
|
|
for _, coin := range amt {
|
|
balance := k.GetBalance(ctx, addr, coin.Denom)
|
|
newBalance := balance.Add(coin)
|
|
|
|
err := k.setBalance(ctx, addr, newBalance)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// emit coin received event
|
|
ctx.EventManager().EmitEvent(
|
|
types.NewCoinReceivedEvent(addr, amt),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// initBalances sets the balance (multiple coins) for an account by address.
|
|
// An error is returned upon failure.
|
|
func (k BaseSendKeeper) initBalances(ctx sdk.Context, addr sdk.AccAddress, balances sdk.Coins) error {
|
|
accountStore := k.getAccountStore(ctx, addr)
|
|
denomPrefixStores := make(map[string]prefix.Store) // memoize prefix stores
|
|
|
|
for i := range balances {
|
|
balance := balances[i]
|
|
if !balance.IsValid() {
|
|
return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, balance.String())
|
|
}
|
|
|
|
// x/bank invariants prohibit persistence of zero balances
|
|
if !balance.IsZero() {
|
|
amount, err := balance.Amount.Marshal()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
accountStore.Set([]byte(balance.Denom), amount)
|
|
|
|
denomPrefixStore, ok := denomPrefixStores[balance.Denom]
|
|
if !ok {
|
|
denomPrefixStore = k.getDenomAddressPrefixStore(ctx, balance.Denom)
|
|
denomPrefixStores[balance.Denom] = denomPrefixStore
|
|
}
|
|
|
|
// Store a reverse index from denomination to account address with a
|
|
// sentinel value.
|
|
denomAddrKey := address.MustLengthPrefix(addr)
|
|
if !denomPrefixStore.Has(denomAddrKey) {
|
|
denomPrefixStore.Set(denomAddrKey, []byte{0})
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setBalance sets the coin balance for an account by address.
|
|
func (k BaseSendKeeper) setBalance(ctx sdk.Context, addr sdk.AccAddress, balance sdk.Coin) error {
|
|
if !balance.IsValid() {
|
|
return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, balance.String())
|
|
}
|
|
|
|
accountStore := k.getAccountStore(ctx, addr)
|
|
denomPrefixStore := k.getDenomAddressPrefixStore(ctx, balance.Denom)
|
|
|
|
// x/bank invariants prohibit persistence of zero balances
|
|
if balance.IsZero() {
|
|
accountStore.Delete([]byte(balance.Denom))
|
|
denomPrefixStore.Delete(address.MustLengthPrefix(addr))
|
|
} else {
|
|
amount, err := balance.Amount.Marshal()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
accountStore.Set([]byte(balance.Denom), amount)
|
|
|
|
// Store a reverse index from denomination to account address with a
|
|
// sentinel value.
|
|
denomAddrKey := address.MustLengthPrefix(addr)
|
|
if !denomPrefixStore.Has(denomAddrKey) {
|
|
denomPrefixStore.Set(denomAddrKey, []byte{0})
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsSendEnabledCoins checks the coins provide and returns an ErrSendDisabled if
|
|
// any of the coins are not configured for sending. Returns nil if sending is enabled
|
|
// for all provided coin
|
|
func (k BaseSendKeeper) IsSendEnabledCoins(ctx sdk.Context, coins ...sdk.Coin) error {
|
|
for _, coin := range coins {
|
|
if !k.IsSendEnabledCoin(ctx, coin) {
|
|
return sdkerrors.Wrapf(types.ErrSendDisabled, "%s transfers are currently disabled", coin.Denom)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IsSendEnabledCoin returns the current SendEnabled status of the provided coin's denom
|
|
func (k BaseSendKeeper) IsSendEnabledCoin(ctx sdk.Context, coin sdk.Coin) bool {
|
|
return k.GetParams(ctx).SendEnabledDenom(coin.Denom)
|
|
}
|
|
|
|
// BlockedAddr checks if a given address is restricted from
|
|
// receiving funds.
|
|
func (k BaseSendKeeper) BlockedAddr(addr sdk.AccAddress) bool {
|
|
return k.blockedAddrs[addr.String()]
|
|
}
|