cosmos-sdk/x/bank/keeper/send.go

272 lines
8.2 KiB
Go

package keeper
import (
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
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
SubtractCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, error)
AddCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, error)
SetBalance(ctx sdk.Context, addr sdk.AccAddress, balance sdk.Coin) error
SetBalances(ctx sdk.Context, addr sdk.AccAddress, balances sdk.Coins) error
GetSendEnabled(ctx sdk.Context) bool
SetSendEnabled(ctx sdk.Context, enabled bool)
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.Marshaler
ak types.AccountKeeper
storeKey sdk.StoreKey
paramSpace paramtypes.Subspace
// list of addresses that are restricted from receiving transactions
blockedAddrs map[string]bool
}
func NewBaseSendKeeper(
cdc codec.Marshaler, storeKey sdk.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,
}
}
// 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 {
_, err := k.SubtractCoins(ctx, in.Address, in.Coins)
if err != nil {
return err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(types.AttributeKeySender, in.Address.String()),
),
)
}
for _, out := range outputs {
_, err := k.AddCoins(ctx, out.Address, out.Coins)
if err != nil {
return err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeTransfer,
sdk.NewAttribute(types.AttributeKeyRecipient, out.Address.String()),
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.
acc := k.ak.GetAccount(ctx, out.Address)
if acc == nil {
k.ak.SetAccount(ctx, k.ak.NewAccountWithAddress(ctx, out.Address))
}
}
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 {
ctx.EventManager().EmitEvents(sdk.Events{
sdk.NewEvent(
types.EventTypeTransfer,
sdk.NewAttribute(types.AttributeKeyRecipient, toAddr.String()),
sdk.NewAttribute(sdk.AttributeKeyAmount, amt.String()),
),
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(types.AttributeKeySender, fromAddr.String()),
),
})
_, err := k.SubtractCoins(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.
acc := k.ak.GetAccount(ctx, toAddr)
if acc == nil {
k.ak.SetAccount(ctx, k.ak.NewAccountWithAddress(ctx, toAddr))
}
return nil
}
// SubtractCoins removes amt coins the account by the given address. An error is
// returned if the resulting balance is negative or the initial amount is invalid.
func (k BaseSendKeeper) SubtractCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, error) {
if !amt.IsValid() {
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, amt.String())
}
resultCoins := sdk.NewCoins()
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 nil, sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "%s is smaller than %s", spendable, coin)
}
newBalance := balance.Sub(coin)
resultCoins = resultCoins.Add(newBalance)
err := k.SetBalance(ctx, addr, newBalance)
if err != nil {
return nil, err
}
}
return resultCoins, nil
}
// AddCoins adds amt to the account balance given by the provided address. An
// error is returned if the initial amount is invalid or if any resulting new
// balance is negative.
func (k BaseSendKeeper) AddCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, error) {
if !amt.IsValid() {
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, amt.String())
}
var resultCoins sdk.Coins
for _, coin := range amt {
balance := k.GetBalance(ctx, addr, coin.Denom)
newBalance := balance.Add(coin)
resultCoins = resultCoins.Add(newBalance)
err := k.SetBalance(ctx, addr, newBalance)
if err != nil {
return nil, err
}
}
return resultCoins, nil
}
// ClearBalances removes all balances for a given account by address.
func (k BaseSendKeeper) ClearBalances(ctx sdk.Context, addr sdk.AccAddress) {
keys := [][]byte{}
k.IterateAccountBalances(ctx, addr, func(balance sdk.Coin) bool {
keys = append(keys, []byte(balance.Denom))
return false
})
store := ctx.KVStore(k.storeKey)
balancesStore := prefix.NewStore(store, types.BalancesPrefix)
accountStore := prefix.NewStore(balancesStore, addr.Bytes())
for _, key := range keys {
accountStore.Delete(key)
}
}
// SetBalances sets the balance (multiple coins) for an account by address. It will
// clear out all balances prior to setting the new coins as to set existing balances
// to zero if they don't exist in amt. An error is returned upon failure.
func (k BaseSendKeeper) SetBalances(ctx sdk.Context, addr sdk.AccAddress, balances sdk.Coins) error {
k.ClearBalances(ctx, addr)
for _, balance := range balances {
err := k.SetBalance(ctx, addr, balance)
if err != nil {
return err
}
}
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())
}
store := ctx.KVStore(k.storeKey)
balancesStore := prefix.NewStore(store, types.BalancesPrefix)
accountStore := prefix.NewStore(balancesStore, addr.Bytes())
bz := k.cdc.MustMarshalBinaryBare(&balance)
accountStore.Set([]byte(balance.Denom), bz)
return nil
}
// GetSendEnabled returns the current SendEnabled
func (k BaseSendKeeper) GetSendEnabled(ctx sdk.Context) bool {
var enabled bool
k.paramSpace.Get(ctx, types.ParamStoreKeySendEnabled, &enabled)
return enabled
}
// SetSendEnabled sets the send enabled
func (k BaseSendKeeper) SetSendEnabled(ctx sdk.Context, enabled bool) {
k.paramSpace.Set(ctx, types.ParamStoreKeySendEnabled, &enabled)
}
// BlockedAddr checks if a given address is restricted from
// receiving funds.
func (k BaseSendKeeper) BlockedAddr(addr sdk.AccAddress) bool {
return k.blockedAddrs[addr.String()]
}