766 lines
22 KiB
Go
766 lines
22 KiB
Go
package lockup
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/cosmos/gogoproto/proto"
|
|
|
|
"cosmossdk.io/collections"
|
|
collcodec "cosmossdk.io/collections/codec"
|
|
"cosmossdk.io/core/address"
|
|
"cosmossdk.io/core/header"
|
|
errorsmod "cosmossdk.io/errors"
|
|
"cosmossdk.io/math"
|
|
"cosmossdk.io/x/accounts/accountstd"
|
|
lockuptypes "cosmossdk.io/x/accounts/defaults/lockup/v1"
|
|
banktypes "cosmossdk.io/x/bank/types"
|
|
distrtypes "cosmossdk.io/x/distribution/types"
|
|
stakingtypes "cosmossdk.io/x/staking/types"
|
|
|
|
"github.com/cosmos/cosmos-sdk/codec"
|
|
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
|
)
|
|
|
|
var (
|
|
OriginalLockingPrefix = collections.NewPrefix(0)
|
|
DelegatedFreePrefix = collections.NewPrefix(1)
|
|
DelegatedLockingPrefix = collections.NewPrefix(2)
|
|
EndTimePrefix = collections.NewPrefix(3)
|
|
StartTimePrefix = collections.NewPrefix(4)
|
|
LockingPeriodsPrefix = collections.NewPrefix(5)
|
|
OwnerPrefix = collections.NewPrefix(6)
|
|
UnbondEntriesPrefix = collections.NewPrefix(7)
|
|
)
|
|
|
|
var (
|
|
CONTINUOUS_LOCKING_ACCOUNT = "continuous-locking-account"
|
|
DELAYED_LOCKING_ACCOUNT = "delayed-locking-account"
|
|
PERIODIC_LOCKING_ACCOUNT = "periodic-locking-account"
|
|
PERMANENT_LOCKING_ACCOUNT = "permanent-locking-account"
|
|
)
|
|
|
|
type getLockedCoinsFunc = func(ctx context.Context, time time.Time, denoms ...string) (sdk.Coins, error)
|
|
|
|
// newBaseLockup creates a new BaseLockup object.
|
|
func newBaseLockup(d accountstd.Dependencies) *BaseLockup {
|
|
BaseLockup := &BaseLockup{
|
|
Owner: collections.NewItem(d.SchemaBuilder, OwnerPrefix, "owner", collections.BytesValue),
|
|
OriginalLocking: collections.NewMap(d.SchemaBuilder, OriginalLockingPrefix, "original_locking", collections.StringKey, sdk.IntValue),
|
|
DelegatedFree: collections.NewMap(d.SchemaBuilder, DelegatedFreePrefix, "delegated_free", collections.StringKey, sdk.IntValue),
|
|
DelegatedLocking: collections.NewMap(d.SchemaBuilder, DelegatedLockingPrefix, "delegated_locking", collections.StringKey, sdk.IntValue),
|
|
UnbondEntries: collections.NewMap(d.SchemaBuilder, UnbondEntriesPrefix, "unbond_entries", collections.StringKey, codec.CollValue[lockuptypes.UnbondingEntries](d.LegacyStateCodec)),
|
|
addressCodec: d.AddressCodec,
|
|
headerService: d.Environment.HeaderService,
|
|
EndTime: collections.NewItem(d.SchemaBuilder, EndTimePrefix, "end_time", collcodec.KeyToValueCodec[time.Time](sdk.TimeKey)),
|
|
}
|
|
|
|
return BaseLockup
|
|
}
|
|
|
|
type BaseLockup struct {
|
|
// Owner is the address of the account owner.
|
|
Owner collections.Item[[]byte]
|
|
OriginalLocking collections.Map[string, math.Int]
|
|
DelegatedFree collections.Map[string, math.Int]
|
|
DelegatedLocking collections.Map[string, math.Int]
|
|
// map val address to unbonding entries
|
|
UnbondEntries collections.Map[string, lockuptypes.UnbondingEntries]
|
|
addressCodec address.Codec
|
|
headerService header.Service
|
|
// lockup end time.
|
|
EndTime collections.Item[time.Time]
|
|
}
|
|
|
|
func (bva *BaseLockup) Init(ctx context.Context, msg *lockuptypes.MsgInitLockupAccount) (
|
|
*lockuptypes.MsgInitLockupAccountResponse, error,
|
|
) {
|
|
owner, err := bva.addressCodec.StringToBytes(msg.Owner)
|
|
if err != nil {
|
|
return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid 'owner' address: %s", err)
|
|
}
|
|
err = bva.Owner.Set(ctx, owner)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
funds := accountstd.Funds(ctx)
|
|
|
|
sortedAmt := funds.Sort()
|
|
for _, coin := range sortedAmt {
|
|
err = bva.OriginalLocking.Set(ctx, coin.Denom, coin.Amount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
bondDenom, err := getStakingDenom(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set initial value for all locked token
|
|
err = bva.DelegatedFree.Set(ctx, bondDenom, math.ZeroInt())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set initial value for all locked token
|
|
err = bva.DelegatedLocking.Set(ctx, bondDenom, math.ZeroInt())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = bva.EndTime.Set(ctx, msg.EndTime)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &lockuptypes.MsgInitLockupAccountResponse{}, nil
|
|
}
|
|
|
|
func (bva *BaseLockup) Delegate(
|
|
ctx context.Context, msg *lockuptypes.MsgDelegate, getLockedCoinsFunc getLockedCoinsFunc,
|
|
) (
|
|
*lockuptypes.MsgExecuteMessagesResponse, error,
|
|
) {
|
|
err := bva.checkSender(ctx, msg.Sender)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
whoami := accountstd.Whoami(ctx)
|
|
delegatorAddress, err := bva.addressCodec.BytesToString(whoami)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hs := bva.headerService.HeaderInfo(ctx)
|
|
|
|
balance, err := bva.getBalance(ctx, delegatorAddress, msg.Amount.Denom)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
lockedCoins, err := getLockedCoinsFunc(ctx, hs.Time, msg.Amount.Denom)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// refresh ubd entries to make sure delegation locking amount is up to date
|
|
err = bva.checkUnbondingEntriesMature(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = bva.TrackDelegation(
|
|
ctx,
|
|
sdk.Coins{*balance},
|
|
lockedCoins,
|
|
sdk.Coins{msg.Amount},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
msgDelegate := &stakingtypes.MsgDelegate{
|
|
DelegatorAddress: delegatorAddress,
|
|
ValidatorAddress: msg.ValidatorAddress,
|
|
Amount: msg.Amount,
|
|
}
|
|
resp, err := sendMessage(ctx, msgDelegate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &lockuptypes.MsgExecuteMessagesResponse{Responses: resp}, nil
|
|
}
|
|
|
|
func (bva *BaseLockup) Undelegate(
|
|
ctx context.Context, msg *lockuptypes.MsgUndelegate,
|
|
) (
|
|
*lockuptypes.MsgExecuteMessagesResponse, error,
|
|
) {
|
|
err := bva.checkSender(ctx, msg.Sender)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
whoami := accountstd.Whoami(ctx)
|
|
delegatorAddress, err := bva.addressCodec.BytesToString(whoami)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
msgUndelegate := &stakingtypes.MsgUndelegate{
|
|
DelegatorAddress: delegatorAddress,
|
|
ValidatorAddress: msg.ValidatorAddress,
|
|
Amount: msg.Amount,
|
|
}
|
|
resp, err := sendMessage(ctx, msgUndelegate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
header := bva.headerService.HeaderInfo(ctx)
|
|
|
|
msgUndelegateResp, err := accountstd.UnpackAny[stakingtypes.MsgUndelegateResponse](resp[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
isNewEntry := true
|
|
entries, err := bva.UnbondEntries.Get(ctx, msg.ValidatorAddress)
|
|
if err != nil {
|
|
if errorsmod.IsOf(err, collections.ErrNotFound) {
|
|
entries = lockuptypes.UnbondingEntries{
|
|
Entries: []*lockuptypes.UnbondingEntry{},
|
|
}
|
|
} else {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
for i, entry := range entries.Entries {
|
|
if entry.CreationHeight == header.Height && entry.EndTime.Equal(msgUndelegateResp.CompletionTime) {
|
|
entry.Amount = entry.Amount.Add(msg.Amount)
|
|
|
|
// update the entry
|
|
entries.Entries[i] = entry
|
|
isNewEntry = false
|
|
break
|
|
}
|
|
}
|
|
|
|
if isNewEntry {
|
|
entries.Entries = append(entries.Entries, &lockuptypes.UnbondingEntry{
|
|
EndTime: msgUndelegateResp.CompletionTime,
|
|
Amount: msgUndelegateResp.Amount,
|
|
ValidatorAddress: msg.ValidatorAddress,
|
|
CreationHeight: header.Height,
|
|
})
|
|
}
|
|
|
|
err = bva.UnbondEntries.Set(ctx, msg.ValidatorAddress, entries)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &lockuptypes.MsgExecuteMessagesResponse{Responses: resp}, nil
|
|
}
|
|
|
|
func (bva *BaseLockup) WithdrawReward(
|
|
ctx context.Context, msg *lockuptypes.MsgWithdrawReward,
|
|
) (
|
|
*lockuptypes.MsgExecuteMessagesResponse, error,
|
|
) {
|
|
err := bva.checkSender(ctx, msg.Sender)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
whoami := accountstd.Whoami(ctx)
|
|
delegatorAddress, err := bva.addressCodec.BytesToString(whoami)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
msgWithdraw := &distrtypes.MsgWithdrawDelegatorReward{
|
|
DelegatorAddress: delegatorAddress,
|
|
ValidatorAddress: msg.ValidatorAddress,
|
|
}
|
|
responses, err := sendMessage(ctx, msgWithdraw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &lockuptypes.MsgExecuteMessagesResponse{Responses: responses}, nil
|
|
}
|
|
|
|
func (bva *BaseLockup) SendCoins(
|
|
ctx context.Context, msg *lockuptypes.MsgSend, getLockedCoinsFunc getLockedCoinsFunc,
|
|
) (
|
|
*lockuptypes.MsgExecuteMessagesResponse, error,
|
|
) {
|
|
err := bva.checkSender(ctx, msg.Sender)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
whoami := accountstd.Whoami(ctx)
|
|
fromAddress, err := bva.addressCodec.BytesToString(whoami)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hs := bva.headerService.HeaderInfo(ctx)
|
|
|
|
if err := msg.Amount.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
lockedCoins, err := getLockedCoinsFunc(ctx, hs.Time, msg.Amount.Denoms()...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = bva.checkTokensSendable(ctx, fromAddress, msg.Amount, lockedCoins)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
msgSend := &banktypes.MsgSend{
|
|
FromAddress: fromAddress,
|
|
ToAddress: msg.ToAddress,
|
|
Amount: msg.Amount,
|
|
}
|
|
resp, err := sendMessage(ctx, msgSend)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &lockuptypes.MsgExecuteMessagesResponse{Responses: resp}, nil
|
|
}
|
|
|
|
func (bva *BaseLockup) checkSender(ctx context.Context, sender string) error {
|
|
owner, err := bva.Owner.Get(ctx)
|
|
if err != nil {
|
|
return sdkerrors.ErrInvalidAddress.Wrapf("invalid owner address: %s", err.Error())
|
|
}
|
|
senderBytes, err := bva.addressCodec.StringToBytes(sender)
|
|
if err != nil {
|
|
return sdkerrors.ErrInvalidAddress.Wrapf("invalid sender address: %s", err.Error())
|
|
}
|
|
if !bytes.Equal(owner, senderBytes) {
|
|
return errors.New("sender is not the owner of this vesting account")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func sendMessage(ctx context.Context, msg proto.Message) ([]*codectypes.Any, error) {
|
|
asAny, err := accountstd.PackAny(msg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return accountstd.ExecModuleAnys(ctx, []*codectypes.Any{asAny})
|
|
}
|
|
|
|
func getStakingDenom(ctx context.Context) (string, error) {
|
|
// Query account balance for the sent denom
|
|
resp, err := accountstd.QueryModule[*stakingtypes.QueryParamsResponse](ctx, &stakingtypes.QueryParamsRequest{})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return resp.Params.BondDenom, nil
|
|
}
|
|
|
|
// checkUnbondingEntriesMature iterates through all the unbonding entries and check if any of the entries are matured and handled.
|
|
func (bva *BaseLockup) checkUnbondingEntriesMature(ctx context.Context) error {
|
|
whoami := accountstd.Whoami(ctx)
|
|
delAddr, err := bva.addressCodec.BytesToString(whoami)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
currentTime := bva.headerService.HeaderInfo(ctx).Time
|
|
|
|
removeKeys := []string{}
|
|
err = bva.UnbondEntries.Walk(ctx, nil, func(key string, value lockuptypes.UnbondingEntries) (stop bool, err error) {
|
|
for i := 0; i < len(value.Entries); i++ {
|
|
entry := value.Entries[i]
|
|
// if not mature then skip
|
|
if entry.EndTime.After(currentTime) {
|
|
return false, nil
|
|
}
|
|
|
|
stakingUnbonding, err := bva.getUnbondingEntries(ctx, delAddr, key)
|
|
if err != nil {
|
|
// if ubd delegation is empty then skip the next iteration check
|
|
if !errorsmod.IsOf(err, stakingtypes.ErrNoUnbondingDelegation) {
|
|
return true, err
|
|
}
|
|
}
|
|
|
|
found := false
|
|
// check if the entry is still exist in the unbonding entries
|
|
for _, e := range stakingUnbonding {
|
|
if e.CompletionTime.Equal(entry.EndTime) && e.CreationHeight == entry.CreationHeight {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// if not found or ubd delegation is empty then assume ubd entry is being handled
|
|
if !found {
|
|
err = bva.TrackUndelegation(ctx, sdk.NewCoins(entry.Amount))
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
|
|
// remove entry
|
|
value.Entries = append(value.Entries[:i], value.Entries[i+1:]...)
|
|
i--
|
|
}
|
|
}
|
|
|
|
if len(value.Entries) == 0 {
|
|
removeKeys = append(removeKeys, key)
|
|
} else {
|
|
err = bva.UnbondEntries.Set(ctx, key, value)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
})
|
|
|
|
for _, key := range removeKeys {
|
|
err = bva.UnbondEntries.Remove(ctx, key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// TrackDelegation tracks a delegation amount for any given lockup account type
|
|
// given the amount of coins currently being locked and the current account balance
|
|
// of the delegation denominations.
|
|
//
|
|
// CONTRACT: The account's coins, delegation coins, locked coins, and delegated
|
|
// locking coins must be sorted.
|
|
func (bva *BaseLockup) TrackDelegation(
|
|
ctx context.Context, balance, lockedCoins, amount sdk.Coins,
|
|
) error {
|
|
bondDenom, err := getStakingDenom(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
delAmt := amount.AmountOf(bondDenom)
|
|
baseAmt := balance.AmountOf(bondDenom)
|
|
|
|
// return error if the delegation amount is zero or if the base coins does not
|
|
// exceed the desired delegation amount.
|
|
if delAmt.IsZero() || baseAmt.LT(delAmt) {
|
|
return sdkerrors.ErrInvalidCoins.Wrap("delegation attempt with zero coins for staking denom or insufficient funds")
|
|
}
|
|
|
|
lockedAmt := lockedCoins.AmountOf(bondDenom)
|
|
delLockingAmt, err := bva.DelegatedLocking.Get(ctx, bondDenom)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
delFreeAmt, err := bva.DelegatedFree.Get(ctx, bondDenom)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// compute x and y per the specification, where:
|
|
// X := min(max(V - DV, 0), D)
|
|
// Y := D - X
|
|
x := math.MinInt(math.MaxInt(lockedAmt.Sub(delLockingAmt), math.ZeroInt()), delAmt)
|
|
y := delAmt.Sub(x)
|
|
|
|
delLockingCoin := sdk.NewCoin(bondDenom, delLockingAmt)
|
|
delFreeCoin := sdk.NewCoin(bondDenom, delFreeAmt)
|
|
if !x.IsZero() {
|
|
xCoin := sdk.NewCoin(bondDenom, x)
|
|
newDelLocking := delLockingCoin.Add(xCoin)
|
|
err = bva.DelegatedLocking.Set(ctx, bondDenom, newDelLocking.Amount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if !y.IsZero() {
|
|
yCoin := sdk.NewCoin(bondDenom, y)
|
|
newDelFree := delFreeCoin.Add(yCoin)
|
|
err = bva.DelegatedFree.Set(ctx, bondDenom, newDelFree.Amount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// TrackUndelegation tracks an undelegation amount by setting the necessary
|
|
// values by which delegated locking and delegated free need to decrease and
|
|
// by which amount the base coins need to increase.
|
|
//
|
|
// NOTE: The undelegation (bond refund) amount may exceed the delegated
|
|
// locking (bond) amount due to the way undelegation truncates the bond refund,
|
|
// which can increase the validator's exchange rate (tokens/shares) slightly if
|
|
// the undelegated tokens are non-integral.
|
|
//
|
|
// CONTRACT: The account's coins and undelegation coins must be sorted.
|
|
func (bva *BaseLockup) TrackUndelegation(ctx context.Context, amount sdk.Coins) error {
|
|
bondDenom, err := getStakingDenom(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
delAmt := amount.AmountOf(bondDenom)
|
|
// return error if the undelegation amount is zero
|
|
if delAmt.IsZero() {
|
|
return sdkerrors.ErrInvalidCoins.Wrap("undelegation attempt with zero coins for staking denom")
|
|
}
|
|
delFreeAmt, err := bva.DelegatedFree.Get(ctx, bondDenom)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
delLockingAmt, err := bva.DelegatedLocking.Get(ctx, bondDenom)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// compute x and y per the specification, where:
|
|
// X := min(DF, D)
|
|
// Y := min(DV, D - X)
|
|
x := math.MinInt(delFreeAmt, delAmt)
|
|
y := math.MinInt(delLockingAmt, delAmt.Sub(x))
|
|
|
|
delLockingCoin := sdk.NewCoin(bondDenom, delLockingAmt)
|
|
delFreeCoin := sdk.NewCoin(bondDenom, delFreeAmt)
|
|
if !x.IsZero() {
|
|
xCoin := sdk.NewCoin(bondDenom, x)
|
|
newDelFree := delFreeCoin.Sub(xCoin)
|
|
err = bva.DelegatedFree.Set(ctx, bondDenom, newDelFree.Amount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if !y.IsZero() {
|
|
yCoin := sdk.NewCoin(bondDenom, y)
|
|
newDelLocking := delLockingCoin.Sub(yCoin)
|
|
err = bva.DelegatedLocking.Set(ctx, bondDenom, newDelLocking.Amount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (bva BaseLockup) getBalance(ctx context.Context, sender, denom string) (*sdk.Coin, error) {
|
|
// Query account balance for the sent denom
|
|
resp, err := accountstd.QueryModule[*banktypes.QueryBalanceResponse](ctx, &banktypes.QueryBalanceRequest{Address: sender, Denom: denom})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return resp.Balance, nil
|
|
}
|
|
|
|
func (bva BaseLockup) getUnbondingEntries(ctx context.Context, delAddr, valAddr string) ([]stakingtypes.UnbondingDelegationEntry, error) {
|
|
resp, err := accountstd.QueryModule[*stakingtypes.QueryUnbondingDelegationResponse](
|
|
ctx, &stakingtypes.QueryUnbondingDelegationRequest{DelegatorAddr: delAddr, ValidatorAddr: valAddr},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return resp.Unbond.Entries, nil
|
|
}
|
|
|
|
func (bva BaseLockup) checkTokensSendable(ctx context.Context, sender string, amount, lockedCoins sdk.Coins) error {
|
|
// Check if any sent tokens is exceeds lockup account balances
|
|
for _, coin := range amount {
|
|
balance, err := bva.getBalance(ctx, sender, coin.Denom)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lockedAmt := lockedCoins.AmountOf(coin.Denom)
|
|
|
|
// get lockedCoin from that are not bonded for the sent denom
|
|
notBondedLockedCoin, err := bva.GetNotBondedLockedCoin(ctx, sdk.NewCoin(coin.Denom, lockedAmt), coin.Denom)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
spendable, hasNeg := sdk.Coins{*balance}.SafeSub(notBondedLockedCoin)
|
|
if hasNeg {
|
|
return errorsmod.Wrapf(sdkerrors.ErrInsufficientFunds,
|
|
"locked amount exceeds account balance funds: %s > %s", notBondedLockedCoin, balance)
|
|
}
|
|
|
|
if _, hasNeg := spendable.SafeSub(coin); hasNeg {
|
|
if len(spendable) == 0 {
|
|
spendable = sdk.Coins{sdk.NewCoin(coin.Denom, math.ZeroInt())}
|
|
}
|
|
return errorsmod.Wrapf(
|
|
sdkerrors.ErrInsufficientFunds,
|
|
"spendable balance %s is smaller than %s",
|
|
spendable, coin,
|
|
)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IterateCoinEntries iterates over all the CoinEntries entries.
|
|
func (bva BaseLockup) IterateCoinEntries(
|
|
ctx context.Context,
|
|
entries collections.Map[string, math.Int],
|
|
cb func(denom string, value math.Int) (bool, error),
|
|
) error {
|
|
err := entries.Walk(ctx, nil, func(key string, value math.Int) (stop bool, err error) {
|
|
return cb(key, value)
|
|
})
|
|
return err
|
|
}
|
|
|
|
// GetNotBondedLockedCoin returns the coin that are not spendable that are not bonded by denom
|
|
// for a lockup account. If the coin by the provided denom are not locked, an coin with zero amount is returned.
|
|
func (bva BaseLockup) GetNotBondedLockedCoin(ctx context.Context, lockedCoin sdk.Coin, denom string) (sdk.Coin, error) {
|
|
// refresh the unbonding entries
|
|
err := bva.checkUnbondingEntriesMature(ctx)
|
|
if err != nil {
|
|
return sdk.Coin{}, err
|
|
}
|
|
|
|
bondDenom, err := getStakingDenom(ctx)
|
|
if err != nil {
|
|
return sdk.Coin{}, err
|
|
}
|
|
|
|
// if not bond denom then return the full locked coin
|
|
if bondDenom != denom {
|
|
return lockedCoin, nil
|
|
}
|
|
|
|
delegatedLockingAmt, err := bva.DelegatedLocking.Get(ctx, denom)
|
|
if err != nil {
|
|
return sdk.Coin{}, err
|
|
}
|
|
|
|
x := math.MinInt(lockedCoin.Amount, delegatedLockingAmt)
|
|
lockedAmt := lockedCoin.Amount.Sub(x)
|
|
|
|
return sdk.NewCoin(denom, lockedAmt), nil
|
|
}
|
|
|
|
// QueryLockupAccountBaseInfo returns a lockup account's info
|
|
func (bva BaseLockup) QueryLockupAccountBaseInfo(ctx context.Context, _ *lockuptypes.QueryLockupAccountInfoRequest) (
|
|
*lockuptypes.QueryLockupAccountInfoResponse, error,
|
|
) {
|
|
owner, err := bva.Owner.Get(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ownerAddress, err := bva.addressCodec.BytesToString(owner)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
endTime, err := bva.EndTime.Get(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
originalLocking := sdk.Coins{}
|
|
err = bva.IterateCoinEntries(ctx, bva.OriginalLocking, func(key string, value math.Int) (stop bool, err error) {
|
|
originalLocking = append(originalLocking, sdk.NewCoin(key, value))
|
|
return false, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bondDenom, err := getStakingDenom(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
delegatedLockingAmt, err := bva.DelegatedLocking.Get(ctx, bondDenom)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
delegatedLocking := sdk.NewCoins(sdk.NewCoin(bondDenom, delegatedLockingAmt))
|
|
|
|
delegatedFreeAmt, err := bva.DelegatedFree.Get(ctx, bondDenom)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
delegatedFree := sdk.NewCoins(sdk.NewCoin(bondDenom, delegatedFreeAmt))
|
|
|
|
return &lockuptypes.QueryLockupAccountInfoResponse{
|
|
Owner: ownerAddress,
|
|
OriginalLocking: originalLocking,
|
|
DelegatedLocking: delegatedLocking,
|
|
DelegatedFree: delegatedFree,
|
|
EndTime: &endTime,
|
|
}, nil
|
|
}
|
|
|
|
func (bva BaseLockup) QueryUnbondingEntries(ctx context.Context, req *lockuptypes.QueryUnbondingEntriesRequest) (
|
|
*lockuptypes.QueryUnbondingEntriesResponse, error,
|
|
) {
|
|
entries, err := bva.UnbondEntries.Get(ctx, req.ValidatorAddress)
|
|
if err != nil {
|
|
if !errorsmod.IsOf(err, collections.ErrNotFound) {
|
|
return nil, err
|
|
}
|
|
|
|
entries = lockuptypes.UnbondingEntries{
|
|
Entries: []*lockuptypes.UnbondingEntry{},
|
|
}
|
|
}
|
|
|
|
return &lockuptypes.QueryUnbondingEntriesResponse{
|
|
UnbondingEntries: entries.Entries,
|
|
}, nil
|
|
}
|
|
|
|
func (bva BaseLockup) QuerySpendableTokens(ctx context.Context, lockedCoins sdk.Coins) (
|
|
*lockuptypes.QuerySpendableAmountResponse, error,
|
|
) {
|
|
whoami := accountstd.Whoami(ctx)
|
|
accAddr, err := bva.addressCodec.BytesToString(whoami)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
spendables := sdk.Coins{}
|
|
for _, denom := range lockedCoins.Denoms() {
|
|
balance, err := bva.getBalance(ctx, accAddr, denom)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
lockedAmt := lockedCoins.AmountOf(balance.Denom)
|
|
|
|
// get lockedCoin from that are not bonded for the sent denom
|
|
notBondedLockedCoin, err := bva.GetNotBondedLockedCoin(ctx, sdk.NewCoin(balance.Denom, lockedAmt), balance.Denom)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
spendable, hasNeg := sdk.Coins{*balance}.SafeSub(notBondedLockedCoin)
|
|
if hasNeg {
|
|
spendable = sdk.Coins{sdk.NewCoin(balance.Denom, math.ZeroInt())}
|
|
}
|
|
|
|
spendables = spendables.Add(spendable...)
|
|
}
|
|
|
|
return &lockuptypes.QuerySpendableAmountResponse{
|
|
SpendableTokens: spendables,
|
|
}, nil
|
|
}
|
|
|
|
func (bva BaseLockup) RegisterExecuteHandlers(builder *accountstd.ExecuteBuilder) {
|
|
accountstd.RegisterExecuteHandler(builder, bva.Undelegate)
|
|
accountstd.RegisterExecuteHandler(builder, bva.WithdrawReward)
|
|
}
|
|
|
|
func (bva BaseLockup) RegisterQueryHandlers(builder *accountstd.QueryBuilder) {
|
|
accountstd.RegisterQueryHandler(builder, bva.QueryUnbondingEntries)
|
|
}
|