package keeper import ( "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/telemetry" 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 GetParams(ctx sdk.Context) types.Params SetParams(ctx sdk.Context, params types.Params) SendEnabledCoin(ctx sdk.Context, coin sdk.Coin) bool SendEnabledCoins(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.BinaryMarshaler 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.BinaryMarshaler, 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, } } // 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. acc := k.ak.GetAccount(ctx, outAddress) if acc == nil { 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 { 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()), ), }) 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. acc := k.ak.GetAccount(ctx, toAddr) if acc == nil { defer telemetry.IncrCounter(1, "new", "account") k.ak.SetAccount(ctx, k.ak.NewAccountWithAddress(ctx, toAddr)) } 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 } // 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 }) accountStore := k.getAccountStore(ctx, addr) 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()) } accountStore := k.getAccountStore(ctx, addr) bz := k.cdc.MustMarshalBinaryBare(&balance) accountStore.Set([]byte(balance.Denom), bz) return nil } // SendEnabledCoins 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) SendEnabledCoins(ctx sdk.Context, coins ...sdk.Coin) error { for _, coin := range coins { if !k.SendEnabledCoin(ctx, coin) { return sdkerrors.Wrapf(types.ErrSendDisabled, "%s transfers are currently disabled", coin.Denom) } } return nil } // SendEnabledCoin returns the current SendEnabled status of the provided coin's denom func (k BaseSendKeeper) SendEnabledCoin(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()] }