Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bank/v2): Send function #21606

Merged
merged 23 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 214 additions & 0 deletions x/bank/v2/keeper/keeper.go
julienrbrt marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
package keeper

import (
"context"

"cosmossdk.io/collections"
"cosmossdk.io/collections/indexes"
"cosmossdk.io/core/address"
appmodulev2 "cosmossdk.io/core/appmodule/v2"
"cosmossdk.io/core/event"
errorsmod "cosmossdk.io/errors"
"cosmossdk.io/math"
"cosmossdk.io/x/bank/v2/types"

"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)

// Keeper defines the bank/v2 module keeper.
Expand All @@ -18,6 +26,8 @@ type Keeper struct {
addressCodec address.Codec
schema collections.Schema
params collections.Item[types.Params]
balances *collections.IndexedMap[collections.Pair[[]byte, string], math.Int, BalancesIndexes]
supply collections.Map[string, math.Int]
}

func NewKeeper(authority []byte, addressCodec address.Codec, env appmodulev2.Environment, cdc codec.BinaryCodec) *Keeper {
Expand All @@ -28,6 +38,8 @@ func NewKeeper(authority []byte, addressCodec address.Codec, env appmodulev2.Env
authority: authority,
addressCodec: addressCodec, // TODO(@julienrbrt): Should we add address codec to the environment?
params: collections.NewItem(sb, types.ParamsKey, "params", codec.CollValue[types.Params](cdc)),
balances: collections.NewIndexedMap(sb, types.BalancesPrefix, "balances", collections.PairKeyCodec(collections.BytesKey, collections.StringKey), sdk.IntValue, newBalancesIndexes(sb)),
supply: collections.NewMap(sb, types.SupplyKey, "supply", collections.StringKey, sdk.IntValue),
}

schema, err := sb.Build()
Expand All @@ -38,3 +50,205 @@ func NewKeeper(authority []byte, addressCodec address.Codec, env appmodulev2.Env

return k
}

// MintCoins creates new coins from thin air and adds it to the module account.
// An error is returned if the module account does not exist or is unauthorized.
func (k Keeper) MintCoins(ctx context.Context, addr []byte, amounts sdk.Coins) error {
// TODO: Mint restriction & permission

if !amounts.IsValid() {
return errorsmod.Wrap(sdkerrors.ErrInvalidCoins, amounts.String())
}

err := k.addCoins(ctx, addr, amounts)
if err != nil {
return err
}

for _, amount := range amounts {
supply := k.GetSupply(ctx, amount.GetDenom())
supply = supply.Add(amount)
k.setSupply(ctx, supply)
}

addrStr, err := k.addressCodec.BytesToString(addr)
if err != nil {
return err
}

// emit mint event
return k.EventService.EventManager(ctx).EmitKV(
types.EventTypeCoinMint,
event.NewAttribute(types.AttributeKeyMinter, addrStr),
event.NewAttribute(sdk.AttributeKeyAmount, amounts.String()),
)
}
Comment on lines +54 to +85
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Address the TODO comment.

The MintCoins function looks good overall. It correctly validates the amounts parameter, updates the supply, and emits a minting event. However, I wanted to remind you about the TODO comment regarding the missing mint restriction and permission logic.

Do you want me to implement the mint restriction and permission logic or open a GitHub issue to track this task?


// SendCoins transfers amt coins from a sending account to a receiving account.
// Function take sender & receipient as []byte.
// They can be sdk address or module name.
// An error is returned upon failure.
func (k Keeper) SendCoins(ctx context.Context, from, to []byte, amt sdk.Coins) error {
if !amt.IsValid() {
return errorsmod.Wrap(sdkerrors.ErrInvalidCoins, amt.String())
}

var err error
// TODO: Send restriction

err = k.subUnlockedCoins(ctx, from, amt)
if err != nil {
return err
}

err = k.addCoins(ctx, to, amt)
if err != nil {
return err
}

fromAddrString, err := k.addressCodec.BytesToString(from)
if err != nil {
return err
}
toAddrString, err := k.addressCodec.BytesToString(to)
if err != nil {
return err
}

return k.EventService.EventManager(ctx).EmitKV(
types.EventTypeTransfer,
event.NewAttribute(types.AttributeKeyRecipient, toAddrString),
event.NewAttribute(types.AttributeKeySender, fromAddrString),
event.NewAttribute(sdk.AttributeKeyAmount, amt.String()),
)
}
Comment on lines +87 to +124
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Address the TODO comment.

The SendCoins function looks good overall. It correctly validates the amt parameter, updates the balances of the sender and recipient accounts, and emits a transfer event. However, I wanted to remind you about the TODO comment regarding the missing send restriction logic.

Do you want me to implement the send restriction logic or open a GitHub issue to track this task?


// GetSupply retrieves the Supply from store
func (k Keeper) GetSupply(ctx context.Context, denom string) sdk.Coin {
amt, err := k.supply.Get(ctx, denom)
if err != nil {
return sdk.NewCoin(denom, math.ZeroInt())
}
return sdk.NewCoin(denom, amt)
}

// GetBalance returns the balance of a specific denomination for a given account
// by address.
func (k Keeper) GetBalance(ctx context.Context, addr []byte, denom string) sdk.Coin {
amt, err := k.balances.Get(ctx, collections.Join(addr, denom))
if err != nil {
return sdk.NewCoin(denom, math.ZeroInt())
}
return sdk.NewCoin(denom, amt)
}

// subUnlockedCoins removes the unlocked amt coins of the given account.
// An error is returned if the resulting balance is negative.
//
// CONTRACT: The provided amount (amt) must be valid, non-negative coins.
//
// A coin_spent event is emitted after the operation.
func (k Keeper) subUnlockedCoins(ctx context.Context, addr []byte, amt sdk.Coins) error {
for _, coin := range amt {
balance := k.GetBalance(ctx, addr, coin.Denom)
spendable := sdk.Coins{balance}

_, hasNeg := spendable.SafeSub(coin)
if hasNeg {
if len(spendable) == 0 {
spendable = sdk.Coins{sdk.Coin{Denom: coin.Denom, Amount: math.ZeroInt()}}
}
return errorsmod.Wrapf(
sdkerrors.ErrInsufficientFunds,
"spendable balance %s is smaller than %s",
spendable, coin,
)
}

newBalance := balance.Sub(coin)

if err := k.setBalance(ctx, addr, newBalance); err != nil {
return err
}
}

addrStr, err := k.addressCodec.BytesToString(addr)
if err != nil {
return err
}

return k.EventService.EventManager(ctx).EmitKV(
types.EventTypeCoinSpent,
event.NewAttribute(types.AttributeKeySpender, addrStr),
event.NewAttribute(sdk.AttributeKeyAmount, amt.String()),
)
}

// addCoins increases the balance of the given address by the specified amount.
//
// CONTRACT: The provided amount (amt) must be valid, non-negative coins.
//
// It emits a coin_received event after the operation.
func (k Keeper) addCoins(ctx context.Context, addr []byte, amt sdk.Coins) error {
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
}
}

addrStr, err := k.addressCodec.BytesToString(addr)
if err != nil {
return err
}

return k.EventService.EventManager(ctx).EmitKV(
types.EventTypeCoinReceived,
event.NewAttribute(types.AttributeKeyReceiver, addrStr),
event.NewAttribute(sdk.AttributeKeyAmount, amt.String()),
)
}

// setSupply sets the supply for the given coin
func (k Keeper) setSupply(ctx context.Context, coin sdk.Coin) {
// Bank invariants and IBC requires to remove zero coins.
if coin.IsZero() {
_ = k.supply.Remove(ctx, coin.Denom)
} else {
_ = k.supply.Set(ctx, coin.Denom, coin.Amount)
}
}

// setBalance sets the coin balance for an account by address.
func (k Keeper) setBalance(ctx context.Context, addr []byte, balance sdk.Coin) error {
if !balance.IsValid() {
return errorsmod.Wrap(sdkerrors.ErrInvalidCoins, balance.String())
}

// x/bank invariants prohibit persistence of zero balances
if balance.IsZero() {
err := k.balances.Remove(ctx, collections.Join(addr, balance.Denom))
if err != nil {
return err
}
return nil
}
return k.balances.Set(ctx, collections.Join(addr, balance.Denom), balance.Amount)
}

func newBalancesIndexes(sb *collections.SchemaBuilder) BalancesIndexes {
return BalancesIndexes{
Denom: indexes.NewReversePair[math.Int](
sb, types.DenomAddressPrefix, "address_by_denom_index",
collections.PairKeyCodec(collections.BytesKey, collections.StringKey),
indexes.WithReversePairUncheckedValue(), // denom to address indexes were stored as Key: Join(denom, address) Value: []byte{0}, this will migrate the value to []byte{} in a lazy way.
),
}
}

type BalancesIndexes struct {
Denom *indexes.ReversePair[[]byte, string, math.Int]
}
Loading
Loading