package bank import ( "fmt" "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/params" ) var _ Keeper = (*BaseKeeper)(nil) // Keeper defines a module interface that facilitates the transfer of coins // between accounts. type Keeper interface { SendKeeper SetCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) sdk.Error SubtractCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error) AddCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error) InputOutputCoins(ctx sdk.Context, inputs []Input, outputs []Output) (sdk.Tags, sdk.Error) DelegateCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) UndelegateCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) } // BaseKeeper manages transfers between accounts. It implements the Keeper interface. type BaseKeeper struct { BaseSendKeeper ak auth.AccountKeeper paramSpace params.Subspace } // NewBaseKeeper returns a new BaseKeeper func NewBaseKeeper(ak auth.AccountKeeper, paramSpace params.Subspace, codespace sdk.CodespaceType) BaseKeeper { ps := paramSpace.WithKeyTable(ParamKeyTable()) return BaseKeeper{ BaseSendKeeper: NewBaseSendKeeper(ak, ps, codespace), ak: ak, paramSpace: ps, } } // SetCoins sets the coins at the addr. func (keeper BaseKeeper) SetCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) sdk.Error { return setCoins(ctx, keeper.ak, addr, amt) } // SubtractCoins subtracts amt from the coins at the addr. func (keeper BaseKeeper) SubtractCoins( ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins, ) (sdk.Coins, sdk.Tags, sdk.Error) { return subtractCoins(ctx, keeper.ak, addr, amt) } // AddCoins adds amt to the coins at the addr. func (keeper BaseKeeper) AddCoins( ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins, ) (sdk.Coins, sdk.Tags, sdk.Error) { return addCoins(ctx, keeper.ak, addr, amt) } // InputOutputCoins handles a list of inputs and outputs func (keeper BaseKeeper) InputOutputCoins( ctx sdk.Context, inputs []Input, outputs []Output, ) (sdk.Tags, sdk.Error) { return inputOutputCoins(ctx, keeper.ak, inputs, outputs) } // DelegateCoins performs delegation by deducting amt coins from an account with // address addr. For vesting accounts, delegations amounts are tracked for both // vesting and vested coins. func (keeper BaseKeeper) DelegateCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) { return delegateCoins(ctx, keeper.ak, addr, amt) } // UndelegateCoins performs undelegation by crediting amt coins to an account with // address addr. For vesting accounts, undelegation amounts are tracked for both // vesting and vested coins. func (keeper BaseKeeper) UndelegateCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) { return undelegateCoins(ctx, keeper.ak, addr, amt) } // SendKeeper defines a module interface that facilitates the transfer of coins // between accounts without the possibility of creating coins. type SendKeeper interface { ViewKeeper SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) GetSendEnabled(ctx sdk.Context) bool SetSendEnabled(ctx sdk.Context, enabled 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 ak auth.AccountKeeper paramSpace params.Subspace } // NewBaseSendKeeper returns a new BaseSendKeeper. func NewBaseSendKeeper(ak auth.AccountKeeper, paramSpace params.Subspace, codespace sdk.CodespaceType) BaseSendKeeper { return BaseSendKeeper{ BaseViewKeeper: NewBaseViewKeeper(ak, codespace), ak: ak, paramSpace: paramSpace, } } // SendCoins moves coins from one account to another func (keeper BaseSendKeeper) SendCoins( ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins, ) (sdk.Tags, sdk.Error) { return sendCoins(ctx, keeper.ak, fromAddr, toAddr, amt) } // GetSendEnabled returns the current SendEnabled // nolint: errcheck func (keeper BaseSendKeeper) GetSendEnabled(ctx sdk.Context) bool { var enabled bool keeper.paramSpace.Get(ctx, ParamStoreKeySendEnabled, &enabled) return enabled } // SetSendEnabled sets the send enabled func (keeper BaseSendKeeper) SetSendEnabled(ctx sdk.Context, enabled bool) { keeper.paramSpace.Set(ctx, ParamStoreKeySendEnabled, &enabled) } var _ ViewKeeper = (*BaseViewKeeper)(nil) // ViewKeeper defines a module interface that facilitates read only access to // account balances. type ViewKeeper interface { GetCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins HasCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) bool Codespace() sdk.CodespaceType } // BaseViewKeeper implements a read only keeper implementation of ViewKeeper. type BaseViewKeeper struct { ak auth.AccountKeeper codespace sdk.CodespaceType } // NewBaseViewKeeper returns a new BaseViewKeeper. func NewBaseViewKeeper(ak auth.AccountKeeper, codespace sdk.CodespaceType) BaseViewKeeper { return BaseViewKeeper{ak: ak, codespace: codespace} } // GetCoins returns the coins at the addr. func (keeper BaseViewKeeper) GetCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins { return getCoins(ctx, keeper.ak, addr) } // HasCoins returns whether or not an account has at least amt coins. func (keeper BaseViewKeeper) HasCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) bool { return hasCoins(ctx, keeper.ak, addr, amt) } // Codespace returns the keeper's codespace. func (keeper BaseViewKeeper) Codespace() sdk.CodespaceType { return keeper.codespace } func getCoins(ctx sdk.Context, am auth.AccountKeeper, addr sdk.AccAddress) sdk.Coins { acc := am.GetAccount(ctx, addr) if acc == nil { return sdk.Coins{} } return acc.GetCoins() } func setCoins(ctx sdk.Context, am auth.AccountKeeper, addr sdk.AccAddress, amt sdk.Coins) sdk.Error { acc := am.GetAccount(ctx, addr) if acc == nil { acc = am.NewAccountWithAddress(ctx, addr) } err := acc.SetCoins(amt) if err != nil { // Handle w/ #870 panic(err) } am.SetAccount(ctx, acc) return nil } // HasCoins returns whether or not an account has at least amt coins. func hasCoins(ctx sdk.Context, am auth.AccountKeeper, addr sdk.AccAddress, amt sdk.Coins) bool { return getCoins(ctx, am, addr).IsAllGTE(amt) } func getAccount(ctx sdk.Context, ak auth.AccountKeeper, addr sdk.AccAddress) auth.Account { return ak.GetAccount(ctx, addr) } func setAccount(ctx sdk.Context, ak auth.AccountKeeper, acc auth.Account) { ak.SetAccount(ctx, acc) } // subtractCoins subtracts amt coins from an account with the given address addr. // // CONTRACT: If the account is a vesting account, the amount has to be spendable. func subtractCoins(ctx sdk.Context, ak auth.AccountKeeper, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error) { oldCoins, spendableCoins := sdk.Coins{}, sdk.Coins{} acc := getAccount(ctx, ak, addr) if acc != nil { oldCoins = acc.GetCoins() spendableCoins = acc.SpendableCoins(ctx.BlockHeader().Time) } // For non-vesting accounts, spendable coins will simply be the original coins. // So the check here is sufficient instead of subtracting from oldCoins. _, hasNeg := spendableCoins.SafeMinus(amt) if hasNeg { return amt, nil, sdk.ErrInsufficientCoins( fmt.Sprintf("insufficient account funds; %s < %s", spendableCoins, amt), ) } newCoins := oldCoins.Minus(amt) // should not panic as spendable coins was already checked err := setCoins(ctx, ak, addr, newCoins) tags := sdk.NewTags(TagKeySender, addr.String()) return newCoins, tags, err } // AddCoins adds amt to the coins at the addr. func addCoins(ctx sdk.Context, am auth.AccountKeeper, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error) { oldCoins := getCoins(ctx, am, addr) newCoins := oldCoins.Plus(amt) if newCoins.IsAnyNegative() { return amt, nil, sdk.ErrInsufficientCoins( fmt.Sprintf("insufficient account funds; %s < %s", oldCoins, amt), ) } err := setCoins(ctx, am, addr, newCoins) tags := sdk.NewTags(TagKeyRecipient, addr.String()) return newCoins, tags, err } // SendCoins moves coins from one account to another func sendCoins(ctx sdk.Context, am auth.AccountKeeper, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) { // Safety check ensuring that when sending coins the keeper must maintain the // supply invariant. if !amt.IsValid() { return nil, sdk.ErrInvalidCoins(amt.String()) } _, subTags, err := subtractCoins(ctx, am, fromAddr, amt) if err != nil { return nil, err } _, addTags, err := addCoins(ctx, am, toAddr, amt) if err != nil { return nil, err } return subTags.AppendTags(addTags), nil } // InputOutputCoins handles a list of inputs and outputs // NOTE: Make sure to revert state changes from tx on error func inputOutputCoins(ctx sdk.Context, am auth.AccountKeeper, inputs []Input, outputs []Output) (sdk.Tags, sdk.Error) { // Safety check ensuring that when sending coins the keeper must maintain the // supply invariant. if err := ValidateInputsOutputs(inputs, outputs); err != nil { return nil, err } allTags := sdk.EmptyTags() for _, in := range inputs { _, tags, err := subtractCoins(ctx, am, in.Address, in.Coins) if err != nil { return nil, err } allTags = allTags.AppendTags(tags) } for _, out := range outputs { _, tags, err := addCoins(ctx, am, out.Address, out.Coins) if err != nil { return nil, err } allTags = allTags.AppendTags(tags) } return allTags, nil } func delegateCoins( ctx sdk.Context, ak auth.AccountKeeper, addr sdk.AccAddress, amt sdk.Coins, ) (sdk.Tags, sdk.Error) { acc := getAccount(ctx, ak, addr) if acc == nil { return nil, sdk.ErrUnknownAddress(fmt.Sprintf("account %s does not exist", addr)) } oldCoins := acc.GetCoins() _, hasNeg := oldCoins.SafeMinus(amt) if hasNeg { return nil, sdk.ErrInsufficientCoins( fmt.Sprintf("insufficient account funds; %s < %s", oldCoins, amt), ) } if err := trackDelegation(acc, ctx.BlockHeader().Time, amt); err != nil { return nil, sdk.ErrInternal(fmt.Sprintf("failed to track delegation: %v", err)) } setAccount(ctx, ak, acc) return sdk.NewTags( sdk.TagAction, TagActionDelegateCoins, sdk.TagDelegator, []byte(addr.String()), ), nil } func undelegateCoins( ctx sdk.Context, ak auth.AccountKeeper, addr sdk.AccAddress, amt sdk.Coins, ) (sdk.Tags, sdk.Error) { acc := getAccount(ctx, ak, addr) if acc == nil { return nil, sdk.ErrUnknownAddress(fmt.Sprintf("account %s does not exist", addr)) } if err := trackUndelegation(acc, amt); err != nil { return nil, sdk.ErrInternal(fmt.Sprintf("failed to track undelegation: %v", err)) } setAccount(ctx, ak, acc) return sdk.NewTags( sdk.TagAction, TagActionUndelegateCoins, sdk.TagDelegator, []byte(addr.String()), ), nil } func trackDelegation(acc auth.Account, blockTime time.Time, amount sdk.Coins) error { vacc, ok := acc.(auth.VestingAccount) if ok { vacc.TrackDelegation(blockTime, amount) return nil } return acc.SetCoins(acc.GetCoins().Minus(amount)) } func trackUndelegation(acc auth.Account, amount sdk.Coins) error { vacc, ok := acc.(auth.VestingAccount) if ok { vacc.TrackUndelegation(amount) return nil } return acc.SetCoins(acc.GetCoins().Plus(amount)) }