Skip to content

Commit

Permalink
frontend: enable sats unit option for bitcoin accounts
Browse files Browse the repository at this point in the history
The main unit displayed in the app for bitcoin accounts is Bitcoin
(BTC), but sometimes it can be easier to think in terms of Satoshi
(sat), especially for lower amounts.

This allows the user to choose the preferred unit using a
switch in the expert settings section of the frontend.
  • Loading branch information
Beerosagos committed Oct 25, 2022
1 parent 1e1e2c6 commit f5a2a82
Show file tree
Hide file tree
Showing 28 changed files with 525 additions and 68 deletions.
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ linters-settings:
- unlambda
- unslice
gocyclo:
min-complexity: 27
min-complexity: 28
maligned:
# print struct with more effective memory layout or not, false by default
suggest-new: true
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- Fix unsufficient gas funds error message on erc20 transactions
- Display trailing zeroes for BTC/LTC amount formatting
- Fix broken links on Android 11+
- Add 'sat' unit for Bitcoin accounts, available in Settings view

## 4.34.0
- Bundle BitBox02 firmware version v9.12.0
Expand Down
1 change: 1 addition & 0 deletions backend/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ func (backend *Backend) createAndAddAccount(
},
GetSaveFilename: backend.environment.GetSaveFilename,
UnsafeSystemOpen: backend.environment.SystemOpen,
BtcCurrencyUnit: backend.config.AppConfig().Backend.BtcUnit,
}

switch specificCoin := coin.(type) {
Expand Down
2 changes: 2 additions & 0 deletions backend/accounts/baseaccount.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ type AccountConfig struct {
GetSaveFilename func(suggestedFilename string) string
// Opens a file in a default application. The filename is not checked.
UnsafeSystemOpen func(filename string) error
// BtcCurrencyUnit is the unit which should be used to format fiat amounts values expressed in BTC..
BtcCurrencyUnit string
}

// BaseAccount is an account struct with common functionality to all coin accounts.
Expand Down
2 changes: 2 additions & 0 deletions backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,10 +351,12 @@ func (backend *Backend) Coin(code coinpkg.Code) (coinpkg.Coin, error) {
servers := backend.defaultElectrumXServers(code)
coin = btc.NewCoin(coinpkg.CodeTBTC, "Bitcoin Testnet", "TBTC", &chaincfg.TestNet3Params, dbFolder, servers,
"https://blockstream.info/testnet/tx/", backend.socksProxy)
coin.SetFormatUnit(backend.config.AppConfig().Backend.BtcUnit)
case code == coinpkg.CodeBTC:
servers := backend.defaultElectrumXServers(code)
coin = btc.NewCoin(coinpkg.CodeBTC, "Bitcoin", "BTC", &chaincfg.MainNetParams, dbFolder, servers,
"https://blockstream.info/tx/", backend.socksProxy)
coin.SetFormatUnit(backend.config.AppConfig().Backend.BtcUnit)
case code == coinpkg.CodeTLTC:
servers := backend.defaultElectrumXServers(code)
coin = btc.NewCoin(coinpkg.CodeTLTC, "Litecoin Testnet", "TLTC", &ltc.TestNet4Params, dbFolder, servers,
Expand Down
17 changes: 13 additions & 4 deletions backend/chart.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import (

"github.com/digitalbitbox/bitbox-wallet-app/backend/accounts"
"github.com/digitalbitbox/bitbox-wallet-app/backend/accounts/errors"
"github.com/digitalbitbox/bitbox-wallet-app/backend/coins/btc/util"
"github.com/digitalbitbox/bitbox-wallet-app/backend/coins/coin"
"github.com/digitalbitbox/bitbox-wallet-app/backend/rates"
"github.com/digitalbitbox/bitbox-wallet-app/util/errp"
)

Expand Down Expand Up @@ -124,6 +126,8 @@ func (backend *Backend) ChartData() (*Chart, error) {
}
isUpToDate := time.Since(until) < 2*time.Hour

formatBtcAsSat := util.FormatBtcAsSat(backend.Config().AppConfig().Backend.BtcUnit)

currentTotal := new(big.Rat)
currentTotalMissing := false
// Total number of transactions across all active accounts.
Expand Down Expand Up @@ -254,7 +258,7 @@ func (backend *Backend) ChartData() (*Chart, error) {
result[i] = ChartEntry{
Time: entry.Time,
Value: floatValue,
FormattedValue: coin.FormatAsCurrency(entry.RatValue, fiat),
FormattedValue: coin.FormatAsCurrency(entry.RatValue, fiat, formatBtcAsSat),
}
i++
}
Expand All @@ -270,7 +274,7 @@ func (backend *Backend) ChartData() (*Chart, error) {
result = append(result, ChartEntry{
Time: time.Now().Unix(),
Value: total,
FormattedValue: coin.FormatAsCurrency(currentTotal, fiat),
FormattedValue: coin.FormatAsCurrency(currentTotal, fiat, formatBtcAsSat),
})
}

Expand All @@ -292,18 +296,23 @@ func (backend *Backend) ChartData() (*Chart, error) {
chartDataMissing = false
}

chartFiat := fiat
if fiat == rates.BTC.String() {
chartFiat = backend.Config().AppConfig().Backend.BtcUnit
}

var chartTotal *float64
var formattedChartTotal string
if !currentTotalMissing {
tot, _ := currentTotal.Float64()
chartTotal = &tot
formattedChartTotal = coin.FormatAsCurrency(currentTotal, fiat)
formattedChartTotal = coin.FormatAsCurrency(currentTotal, fiat, formatBtcAsSat)
}
return &Chart{
DataMissing: chartDataMissing,
DataDaily: toSortedSlice(chartEntriesDaily, fiat),
DataHourly: toSortedSlice(chartEntriesHourly, fiat),
Fiat: fiat,
Fiat: chartFiat,
Total: chartTotal,
FormattedTotal: formattedChartTotal,
IsUpToDate: isUpToDate,
Expand Down
47 changes: 42 additions & 5 deletions backend/coins/btc/coin.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@ import (

// Coin models a Bitcoin-related coin.
type Coin struct {
initOnce sync.Once
code coinpkg.Code
name string
unit string
initOnce sync.Once
code coinpkg.Code
name string
// unit is the main unit of the coin, e.g. 'BTC'
unit string
// formatUnit keeps track of the unit used, e.g. 'BTC' or 'sat' depening on if sat mode is enabled
formatUnit string
net *chaincfg.Params
dbFolder string
makeBlockchain func() blockchain.Interface
Expand Down Expand Up @@ -74,6 +77,7 @@ func NewCoin(
code: code,
name: name,
unit: unit,
formatUnit: unit,
net: net,
dbFolder: dbFolder,
blockExplorerTxPrefix: blockExplorerTxPrefix,
Expand Down Expand Up @@ -150,13 +154,33 @@ func (coin *Coin) Unit(bool) string {
return coin.unit
}

// SetFormatUnit implements coin.Coin.
func (coin *Coin) SetFormatUnit(unit string) {
coin.formatUnit = unit
}

// GetFormatUnit implements coin.Coin.
func (coin *Coin) GetFormatUnit() string {
if coin.code == coinpkg.CodeTBTC {
if coin.formatUnit == coinpkg.UnitSats {
return "t" + coin.formatUnit
}
return "T" + coin.formatUnit
}

return coin.formatUnit
}

// Decimals implements coinpkg.Coin.
func (coin *Coin) Decimals(isFee bool) uint {
return 8
}

// FormatAmount implements coinpkg.Coin.
func (coin *Coin) FormatAmount(amount coinpkg.Amount, isFee bool) string {
if coin.formatUnit == coinpkg.UnitSats {
return amount.BigInt().String()
}
return new(big.Rat).SetFrac(amount.BigInt(), big.NewInt(unitSatoshi)).FloatString(8)
}

Expand All @@ -168,11 +192,24 @@ func (coin *Coin) ToUnit(amount coinpkg.Amount, isFee bool) float64 {

// SetAmount implements coinpkg.Coin.
func (coin *Coin) SetAmount(amount *big.Rat, isFee bool) coinpkg.Amount {
satsAmount := new(big.Rat).Mul(amount, new(big.Rat).SetFloat64(unitSatoshi))
satsAmount := coinpkg.Btc2Sat(amount)
intSatsAmount, _ := new(big.Int).SetString(satsAmount.FloatString(0), 0)
return coinpkg.NewAmount(intSatsAmount)
}

// ParseAmount implements coinpkg.Coin.
func (coin *Coin) ParseAmount(amount string) (coinpkg.Amount, error) {
amountRat, valid := new(big.Rat).SetString(amount)
if !valid {
return coinpkg.Amount{}, errp.New("Invalid amount")
}

if coin.formatUnit == coinpkg.UnitSats {
amountRat = coinpkg.Sat2Btc(amountRat)
}
return coin.SetAmount(amountRat, false), nil
}

// Blockchain connects to a blockchain backend.
func (coin *Coin) Blockchain() blockchain.Interface {
return coin.blockchain
Expand Down
20 changes: 20 additions & 0 deletions backend/coins/btc/coin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,26 @@ func (s *testSuite) TestSetAmount() {
}
}

func (s *testSuite) TestParseAmount() {
btcAmount := "123.12345678"
satAmount := "12312345678"
intSatAmount := int64(12312345678)

s.coin.SetFormatUnit("BTC")
coinAmount, err := s.coin.ParseAmount(btcAmount)
require.Equal(s.T(), err, nil)
intAmount, err := coinAmount.Int64()
require.Equal(s.T(), err, nil)
require.Equal(s.T(), intSatAmount, intAmount)

s.coin.SetFormatUnit("sat")
coinAmount, err = s.coin.ParseAmount(satAmount)
require.Equal(s.T(), err, nil)
intAmount, err = coinAmount.Int64()
require.Equal(s.T(), err, nil)
require.Equal(s.T(), intSatAmount, intAmount)
}

func (s *testSuite) TestDecodeAddress() {
tbtcValidAddresses := []string{
"myY3Bbvj5mjwqqvubtu5Hfy2nuCeBfvNXL", // p2pkh legacy
Expand Down
14 changes: 9 additions & 5 deletions backend/coins/btc/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,27 +91,31 @@ type FormattedAmount struct {
}

func (handlers *Handlers) formatAmountAsJSON(amount coin.Amount, isFee bool) FormattedAmount {
accountCoin := handlers.account.Coin()
return FormattedAmount{
Amount: handlers.account.Coin().FormatAmount(amount, isFee),
Unit: handlers.account.Coin().Unit(isFee),
Amount: accountCoin.FormatAmount(amount, isFee),
Unit: accountCoin.GetFormatUnit(),
Conversions: coin.Conversions(
amount,
handlers.account.Coin(),
accountCoin,
isFee,
handlers.account.Config().RateUpdater,
util.FormatBtcAsSat(handlers.account.Config().BtcCurrencyUnit),
),
}
}

func (handlers *Handlers) formatAmountAtTimeAsJSON(amount coin.Amount, timeStamp *time.Time) *FormattedAmount {
accountCoin := handlers.account.Coin()
return &FormattedAmount{
Amount: handlers.account.Coin().FormatAmount(amount, false),
Unit: handlers.account.Coin().Unit(false),
Amount: accountCoin.FormatAmount(amount, false),
Unit: accountCoin.GetFormatUnit(),
Conversions: coin.ConversionsAtTime(
amount,
handlers.account.Coin(),
false,
handlers.account.Config().RateUpdater,
util.FormatBtcAsSat(handlers.account.Config().BtcCurrencyUnit),
timeStamp,
),
}
Expand Down
7 changes: 6 additions & 1 deletion backend/coins/btc/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,12 @@ func (account *Account) newTx(args *accounts.TxProposalArgs) (
}
} else {
allowZero := false
parsedAmount, err := args.Amount.Amount(big.NewInt(unitSatoshi), allowZero)

unit := int64(unitSatoshi)
if account.coin.formatUnit == coin.UnitSats {
unit = 1
}
parsedAmount, err := args.Amount.Amount(big.NewInt(unit), allowZero)
if err != nil {
return nil, nil, err
}
Expand Down
6 changes: 6 additions & 0 deletions backend/coins/btc/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/digitalbitbox/bitbox-wallet-app/backend/coins/coin"
"github.com/digitalbitbox/bitbox-wallet-app/backend/coins/ltc"
"github.com/digitalbitbox/bitbox-wallet-app/util/errp"
)
Expand Down Expand Up @@ -68,3 +69,8 @@ func AddressFromPkScript(pkScript []byte, net *chaincfg.Params) (btcutil.Address
}
return addresses[0], nil
}

// FormatBtcAsSat returns true if the btcUnit param is `[t]sat`.
func FormatBtcAsSat(btcUnit string) bool {
return btcUnit == coin.UnitSats || btcUnit == "t"+coin.UnitSats
}
7 changes: 7 additions & 0 deletions backend/coins/coin/codes.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ const (
// There are some more coin codes for the supported erc20 tokens in erc20.go.
)

const (
// UnitBtc represents Bitcoin unit.
UnitBtc string = "BTC"
// UnitSats represents Satoshi unit.
UnitSats string = "sat"
)

// TestnetCoins is the subset of all coins which are available in testnet mode.
var TestnetCoins = map[Code]struct{}{
CodeTBTC: {},
Expand Down
10 changes: 10 additions & 0 deletions backend/coins/coin/coin.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ type Coin interface {
// The fee unit is usually the same as the main unit, but can differ.
Unit(isFee bool) string

// SetFormatUnit sets the unit used to format the amount, e.g. "BTC" or "sat".
SetFormatUnit(unit string)

// GetFormatUnit sets the unit used to format the amount, e.g. "BTC" or "sat".
GetFormatUnit() string

// Number of decimal places in the standard unit, e.g. 8 for Bitcoin. Must be in the range
// [0..31].
Decimals(isFee bool) uint
Expand All @@ -55,6 +61,10 @@ type Coin interface {
// e.g. BTC 1/2 => 50000000
SetAmount(amount *big.Rat, isFee bool) Amount

// ParseAmount parse a String representing a given amount, considering the formatting unit.
// e.g. if the formatUnit is set as "sat", the amount will be considered as being sats
ParseAmount(amount string) (Amount, error)

// // Server returns the host and port of the full node used for blockchain synchronization.
// Server() string

Expand Down
Loading

0 comments on commit f5a2a82

Please sign in to comment.