Skip to content

Commit

Permalink
Handle locked coins properly
Browse files Browse the repository at this point in the history
- Prevent dcr wallets from returning locked unspent utoxs in wallet.UnspentOutputs
- Display the correct locked, immature and spendable amounts on the wallet account page

Signed-off-by: Philemon Ukane <[email protected]>
  • Loading branch information
ukane-philemon committed Jan 17, 2024
1 parent ebbd965 commit 8d44387
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 59 deletions.
33 changes: 31 additions & 2 deletions libwallet/assets/btc/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,35 @@ func (asset *Asset) GetAccountBalance(accountNumber int32) (*sharedW.Balance, er
return nil, err
}

// Account for locked amount.
lockedAmount, err := asset.lockedAmount()
if err != nil {
return nil, err
}

return &sharedW.Balance{
Total: Amount(balance.Total),
Spendable: Amount(balance.Spendable),
Spendable: Amount(balance.Spendable - lockedAmount),
ImmatureReward: Amount(balance.ImmatureReward),
Locked: Amount(lockedAmount),
}, nil
}

// lockedAmount is the total value of locked outputs, as locked with
// LockUnspent.
func (asset *Asset) lockedAmount() (btcutil.Amount, error) {
lockedOutpoints := asset.Internal().BTC.LockedOutpoints()
var sum int64
for _, op := range lockedOutpoints {
tx, err := asset.GetTransactionRaw(op.Txid)
if err != nil {
return 0, err
}
sum += tx.Amount
}
return btcutil.Amount(sum), nil
}

// SpendableForAccount returns the spendable balance for the provided account
func (asset *Asset) SpendableForAccount(account int32) (int64, error) {
if !asset.WalletOpened() {
Expand All @@ -126,7 +148,14 @@ func (asset *Asset) SpendableForAccount(account int32) (int64, error) {
if err != nil {
return 0, utils.TranslateError(err)
}
return int64(bals.Spendable), nil

// Account for locked amount.
lockedAmount, err := asset.lockedAmount()
if err != nil {
return 0, err
}

return int64(bals.Spendable - lockedAmount), nil
}

// UnspentOutputs returns all the unspent outputs available for the provided
Expand Down
4 changes: 3 additions & 1 deletion libwallet/assets/btc/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,16 +524,18 @@ func (asset *Asset) GetWalletBalance() (*sharedW.Balance, error) {
return nil, err
}

var totalBalance, totalSpendable, totalImmatureReward int64
var totalBalance, totalSpendable, totalImmatureReward, totalLocked int64
for _, acc := range accountsResult.Accounts {
totalBalance += acc.Balance.Total.ToInt()
totalSpendable += acc.Balance.Spendable.ToInt()
totalImmatureReward += acc.Balance.ImmatureReward.ToInt()
totalLocked += acc.Balance.Locked.ToInt()
}

return &sharedW.Balance{
Total: Amount(totalBalance),
Spendable: Amount(totalSpendable),
ImmatureReward: Amount(totalImmatureReward),
Locked: Amount(totalLocked),
}, nil
}
47 changes: 45 additions & 2 deletions libwallet/assets/dcr/accounts.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dcr

import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
Expand All @@ -12,6 +13,7 @@ import (
sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet"
"github.com/crypto-power/cryptopower/libwallet/utils"
"github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/dcrd/dcrutil/v4"
)

func (asset *Asset) GetAccounts() (string, error) {
Expand Down Expand Up @@ -116,17 +118,44 @@ func (asset *Asset) GetAccountBalance(accountNumber int32) (*sharedW.Balance, er
return nil, err
}

lockedAmt, err := asset.lockedAmount(ctx, accountNumber)
if err != nil {
return nil, err
}

return &sharedW.Balance{
Total: Amount(balance.Total),
Spendable: Amount(balance.Spendable),
Spendable: Amount(balance.Spendable - lockedAmt),
ImmatureReward: Amount(balance.ImmatureCoinbaseRewards),
ImmatureStakeGeneration: Amount(balance.ImmatureStakeGeneration),
LockedByTickets: Amount(balance.LockedByTickets),
VotingAuthority: Amount(balance.VotingAuthority),
UnConfirmed: Amount(balance.Unconfirmed),
Locked: Amount(lockedAmt),
}, nil
}

// lockedAmount is the total value of locked outputs, as locked with
// LockUnspent.
func (asset *Asset) lockedAmount(ctx context.Context, acctNumber int32) (dcrutil.Amount, error) {
accountName, err := asset.AccountName(acctNumber)
if err != nil {
return dcrutil.Amount(0), err
}

lockedOutpoints, err := asset.Internal().DCR.LockedOutpoints(ctx, accountName)
if err != nil {
return 0, err
}

var sum float64
for _, op := range lockedOutpoints {
sum += op.Amount
}

return dcrutil.NewAmount(sum)
}

func (asset *Asset) SpendableForAccount(account int32) (int64, error) {
if !asset.WalletOpened() {
return -1, utils.ErrDCRNotInitialized
Expand All @@ -138,9 +167,18 @@ func (asset *Asset) SpendableForAccount(account int32) (int64, error) {
log.Error(err)
return 0, utils.TranslateError(err)
}
return int64(bals.Spendable), nil

lockedAmt, err := asset.lockedAmount(ctx, account)
if err != nil {
return 0, err
}

return int64(bals.Spendable - lockedAmt), nil
}

// UnspentOutputs returns unspent outputs that can be used for transactions.
// Unspent outputs that are locked by the wallet are not returned as valid
// unspent utxos.
func (asset *Asset) UnspentOutputs(account int32) ([]*sharedW.UnspentOutput, error) {
if !asset.WalletOpened() {
return nil, utils.ErrDCRNotInitialized
Expand All @@ -159,6 +197,11 @@ func (asset *Asset) UnspentOutputs(account int32) ([]*sharedW.UnspentOutput, err

unspentOutputs := make([]*sharedW.UnspentOutput, 0, len(unspents))
for _, utxo := range unspents {
hash := utxo.OutPoint.Hash
if asset.Internal().DCR.LockedOutpoint(&hash, utxo.OutPoint.Index) {
continue // utxo is locked.
}

addresses := addresshelper.PkScriptAddresses(asset.chainParams, utxo.Output.PkScript)

var confirmations int32
Expand Down
6 changes: 4 additions & 2 deletions libwallet/assets/dcr/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,16 +294,18 @@ func (asset *Asset) GetWalletBalance() (*sharedW.Balance, error) {
return nil, err
}

var totalBalance, totalSpendable, totalImmatureReward int64
var totalBalance, totalSpendable, totalImmatureReward, totalLocked int64
for _, acc := range accountsResult.Accounts {
totalBalance += acc.Balance.Total.ToInt()
totalSpendable += acc.Balance.Spendable.ToInt()
totalImmatureReward += acc.Balance.ImmatureReward.ToInt()
totalLocked += acc.Balance.Locked.ToInt()
}

return &sharedW.Balance{
Total: Amount(totalBalance),
Spendable: Amount(totalSpendable),
Spendable: Amount(totalSpendable - totalLocked),
ImmatureReward: Amount(totalImmatureReward),
Locked: Amount(totalLocked),
}, nil
}
33 changes: 31 additions & 2 deletions libwallet/assets/ltc/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,35 @@ func (asset *Asset) GetAccountBalance(accountNumber int32) (*sharedW.Balance, er
return nil, err
}

// Account for locked amount.
lockedAmount, err := asset.lockedAmount()
if err != nil {
return nil, err
}

return &sharedW.Balance{
Total: Amount(balance.Total),
Spendable: Amount(balance.Spendable),
Spendable: Amount(balance.Spendable - lockedAmount),
ImmatureReward: Amount(balance.ImmatureReward),
Locked: Amount(lockedAmount),
}, nil
}

// lockedAmount is the total value of locked outputs, as locked with
// LockUnspent.
func (asset *Asset) lockedAmount() (ltcutil.Amount, error) {
lockedOutpoints := asset.Internal().LTC.LockedOutpoints()
var sum int64
for _, op := range lockedOutpoints {
tx, err := asset.GetTransactionRaw(op.Txid)
if err != nil {
return 0, err
}
sum += tx.Amount
}
return ltcutil.Amount(sum), nil
}

// SpendableForAccount returns the spendable balance for the provided account
func (asset *Asset) SpendableForAccount(account int32) (int64, error) {
if !asset.WalletOpened() {
Expand All @@ -126,7 +148,14 @@ func (asset *Asset) SpendableForAccount(account int32) (int64, error) {
if err != nil {
return 0, utils.TranslateError(err)
}
return int64(bals.Spendable), nil

// Account for locked amount.
lockedAmount, err := asset.lockedAmount()
if err != nil {
return 0, err
}

return int64(bals.Spendable - lockedAmount), nil
}

// UnspentOutputs returns all the unspent outputs available for the provided
Expand Down
4 changes: 3 additions & 1 deletion libwallet/assets/ltc/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -551,16 +551,18 @@ func (asset *Asset) GetWalletBalance() (*sharedW.Balance, error) {
return nil, err
}

var totalBalance, totalSpendable, totalImmatureReward int64
var totalBalance, totalSpendable, totalImmatureReward, totalLocked int64
for _, acc := range accountsResult.Accounts {
totalBalance += acc.Balance.Total.ToInt()
totalSpendable += acc.Balance.Spendable.ToInt()
totalImmatureReward += acc.Balance.ImmatureReward.ToInt()
totalLocked += acc.Balance.Locked.ToInt()
}

return &sharedW.Balance{
Total: Amount(totalBalance),
Spendable: Amount(totalSpendable),
ImmatureReward: Amount(totalImmatureReward),
Locked: Amount(totalLocked),
}, nil
}
5 changes: 3 additions & 2 deletions libwallet/assets/wallet/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ type BlockInfo struct {

// FeeEstimate defines the fee estimate returned by the API.
type FeeEstimate struct {
// Number of confrmed blocks that show the average fee rate represented below.
// Number of confirmed blocks that show the average fee rate represented below.
ConfirmedBlocks int32
// Feerate shows estimate fee rate in Sat/kvB or Lit/kvB.
Feerate AssetAmount
Expand Down Expand Up @@ -95,10 +95,11 @@ type UnsignedTransaction struct {
}

type Balance struct {
// fields common to both DCR and BTC
// Fields common to all assets.
Total AssetAmount
Spendable AssetAmount
ImmatureReward AssetAmount
Locked AssetAmount

// DCR only fields
ImmatureStakeGeneration AssetAmount
Expand Down
85 changes: 40 additions & 45 deletions ui/page/accounts/accounts_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ func (pg *Page) addAccountBtnLayout(gtx C) D {

func (pg *Page) accountItemLayout(gtx C, account *sharedW.Account) D {
dp10 := values.MarginPadding10
bal := account.Balance
return cryptomaterial.LinearLayout{
Width: cryptomaterial.MatchParent,
Height: cryptomaterial.WrapContent,
Expand All @@ -202,61 +203,55 @@ func (pg *Page) accountItemLayout(gtx C, account *sharedW.Account) D {
Radius: cryptomaterial.Radius(8),
},
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return pg.accountBalanceLayout(gtx, false, account)
}),
layout.Rigid(pg.accountBalanceLayout(account.AccountName, account.Balance.Total, layout.Vertical)),
layout.Rigid(func(gtx C) D {
return layout.Inset{Top: dp10, Bottom: dp10}.Layout(gtx, pg.Theme.Separator().Layout)
}),
layout.Rigid(func(gtx C) D {
return pg.accountBalanceLayout(gtx, true, account)
locked := bal.Locked
if bal.LockedByTickets != nil {
locked = pg.wallet.ToAmount(locked.ToInt() + bal.LockedByTickets.ToInt())
}
immature := bal.ImmatureReward
if bal.ImmatureStakeGeneration != nil {
immature = pg.wallet.ToAmount(immature.ToInt() + bal.ImmatureStakeGeneration.ToInt())
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(pg.accountBalanceLayout(values.String(values.StrLabelSpendable), bal.Spendable, layout.Horizontal)),
layout.Rigid(pg.accountBalanceLayout(values.String(values.StrLocked), locked, layout.Horizontal)),
layout.Rigid(pg.accountBalanceLayout(values.String(values.StrImmature), immature, layout.Horizontal)),
)
}),
)
}

func (pg *Page) accountBalanceLayout(gtx C, spendableLayout bool, account *sharedW.Account) D {
var label, balanceTxt cryptomaterial.Label
var balanceAmt float64
if !spendableLayout {
label = pg.Theme.Label(pg.ConvertTextSize(values.TextSize18), account.AccountName)
label.Font.Weight = font.SemiBold
balanceTxt = pg.Theme.Label(pg.ConvertTextSize(values.TextSize18), account.Balance.Total.String())
balanceTxt.Font.Weight = font.SemiBold
balanceAmt = pg.wallet.ToAmount(account.Balance.Total.ToInt()).ToCoin()
} else {
label = pg.Theme.Label(pg.ConvertTextSize(values.TextSize16), values.String(values.StrAmountSpendable))
label.Font.Weight = font.SemiBold
label.Color = pg.Theme.Color.GrayText3
balanceTxt = pg.Theme.Label(pg.ConvertTextSize(values.TextSize16), account.Balance.Spendable.String())
balanceTxt.Font.Weight = font.SemiBold
balanceTxt.Color = pg.Theme.Color.GrayText3
balanceAmt = pg.wallet.ToAmount(account.Balance.Spendable.ToInt()).ToCoin()
}
func (pg *Page) accountBalanceLayout(title string, bal sharedW.AssetAmount, balAxis layout.Axis) func(gtx C) D {
label := pg.Theme.Label(pg.ConvertTextSize(values.TextSize16), title)
label.Font.Weight = font.SemiBold
balanceTxt := pg.Theme.Label(pg.ConvertTextSize(values.TextSize16), bal.String())
balanceTxt.Font.Weight = font.SemiBold
return func(gtx C) D {
return layout.Flex{Spacing: layout.SpaceBetween}.Layout(gtx,
layout.Rigid(label.Layout), // Title
layout.Flexed(1, func(gtx C) D { // Balances
return layout.E.Layout(gtx, func(gtx C) D {
return layout.Flex{Axis: balAxis, Alignment: layout.End}.Layout(gtx,
layout.Rigid(balanceTxt.Layout),
layout.Rigid(func(gtx C) D {
if !pg.usdExchangeSet || pg.exchangeRate <= 0 || bal.ToCoin() == 0 {
return D{}
}

balanceUSD := "($ 0.00)"
if pg.exchangeRate != -1 && pg.usdExchangeSet {
balanceUSD = fmt.Sprintf("(%v)", utils.FormatAsUSDString(pg.Printer, utils.CryptoToUSD(pg.exchangeRate, balanceAmt)))
balanceUSD := fmt.Sprintf(" (%v)", utils.FormatAsUSDString(pg.Printer, utils.CryptoToUSD(pg.exchangeRate, bal.ToCoin())))
usdAmtLabel := pg.Theme.Label(pg.ConvertTextSize(values.TextSize16), balanceUSD)
usdAmtLabel.Font.Weight = font.SemiBold
return usdAmtLabel.Layout(gtx)
}),
)
})
}),
)
}
return layout.Flex{Spacing: layout.SpaceBetween}.Layout(gtx,
layout.Rigid(label.Layout), // Title
layout.Flexed(1, func(gtx C) D { // Balances
var usdAmtLabel cryptomaterial.Label
if spendableLayout {
usdAmtLabel = pg.Theme.Label(pg.ConvertTextSize(values.TextSize16), balanceUSD)
usdAmtLabel.Font.Weight = font.SemiBold
usdAmtLabel.Color = pg.Theme.Color.GrayText3
} else {
usdAmtLabel = pg.Theme.Label(pg.ConvertTextSize(values.TextSize16), balanceUSD)
usdAmtLabel.Font.Weight = font.SemiBold
}
return layout.E.Layout(gtx, func(gtx C) D {
return layout.Flex{Axis: layout.Vertical, Alignment: layout.End}.Layout(gtx,
layout.Rigid(balanceTxt.Layout),
layout.Rigid(usdAmtLabel.Layout),
)
})
}),
)
}

// HandleUserInteractions is called just before Layout() to determine
Expand Down
1 change: 0 additions & 1 deletion ui/values/localizable/en.go
Original file line number Diff line number Diff line change
Expand Up @@ -836,7 +836,6 @@ const EN = `
"rfp" = "RFP"
"proposedFor" = "Proposed for "
"accounts" = "Accounts"
"amountSpendable" = "Amount Spendable"
"stakingInfo" = "Staking Info"
"timeLeft" = "Time Left"
"totalReward" = "Total Reward"
Expand Down
Loading

0 comments on commit 8d44387

Please sign in to comment.