Skip to content

Commit c21f60a

Browse files
committed
backend/coins: refactor formatted amounts and add unit tests
This refactors the code to format amounts with fiat conversions, removing it from handlers into the coin package, and adds some tests.
1 parent 8b44ef7 commit c21f60a

File tree

19 files changed

+215
-163
lines changed

19 files changed

+215
-163
lines changed

backend/accounts.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -875,7 +875,6 @@ func (backend *Backend) createAndAddAccount(coin coinpkg.Coin, persistedConfig *
875875
},
876876
GetSaveFilename: backend.environment.GetSaveFilename,
877877
UnsafeSystemOpen: backend.environment.SystemOpen,
878-
BtcCurrencyUnit: backend.config.AppConfig().Backend.BtcUnit,
879878
}
880879

881880
// This function is passed as a callback to the BTC account constructor. It is called when the

backend/accounts/baseaccount.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,6 @@ type AccountConfig struct {
5656
GetSaveFilename func(suggestedFilename string) string
5757
// Opens a file in a default application. The filename is not checked.
5858
UnsafeSystemOpen func(filename string) error
59-
// BtcCurrencyUnit is the unit which should be used to format fiat amounts values expressed in BTC..
60-
BtcCurrencyUnit coin.BtcUnit
6159
}
6260

6361
// BaseAccount is an account struct with common functionality to all coin accounts.

backend/coins/btc/handlers/handlers.go

Lines changed: 37 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -94,90 +94,27 @@ func (handlers *Handlers) Uninit() {
9494
handlers.account = nil
9595
}
9696

97-
// FormattedAmount with unit and conversions.
98-
type FormattedAmount struct {
99-
Amount string `json:"amount"`
100-
Unit string `json:"unit"`
101-
Conversions map[string]string `json:"conversions"`
102-
// Estimated flag is enabled if the Conversions map was expected to
103-
// be calculated using historical rates, but latest rates have been used instead.
104-
Estimated bool `json:"estimated"`
105-
}
106-
107-
func (handlers *Handlers) formatAmountAsJSON(amount coin.Amount, isFee bool) FormattedAmount {
108-
accountCoin := handlers.account.Coin()
109-
return FormattedAmount{
110-
Amount: accountCoin.FormatAmount(amount, isFee),
111-
Unit: accountCoin.GetFormatUnit(isFee),
112-
Conversions: coin.Conversions(
113-
amount,
114-
accountCoin,
115-
isFee,
116-
handlers.account.Config().RateUpdater,
117-
util.FormatBtcAsSat(handlers.account.Config().BtcCurrencyUnit),
118-
),
119-
}
120-
}
121-
122-
func (handlers *Handlers) formatAmountAtTimeAsJSON(amount coin.Amount, timeStamp *time.Time) FormattedAmount {
123-
accountCoin := handlers.account.Coin()
124-
rateUpdater := handlers.account.Config().RateUpdater
125-
formatBtcAsSat := util.FormatBtcAsSat(handlers.account.Config().BtcCurrencyUnit)
126-
var conversions map[string]string
127-
var estimated bool
128-
129-
if timeStamp == nil {
130-
conversions = coin.Conversions(
131-
amount,
132-
accountCoin,
133-
false,
134-
rateUpdater,
135-
formatBtcAsSat,
136-
)
137-
estimated = true
138-
} else {
139-
conversions, estimated = coin.ConversionsAtTime(
140-
amount,
141-
accountCoin,
142-
false,
143-
rateUpdater,
144-
formatBtcAsSat,
145-
timeStamp,
146-
)
147-
}
148-
return FormattedAmount{
149-
Amount: accountCoin.FormatAmount(amount, false),
150-
Unit: accountCoin.GetFormatUnit(false),
151-
Conversions: conversions,
152-
Estimated: estimated,
153-
}
154-
}
155-
156-
func (handlers *Handlers) formatBTCAmountAsJSON(amount btcutil.Amount, isFee bool) FormattedAmount {
157-
return handlers.formatAmountAsJSON(coin.NewAmountFromInt64(int64(amount)), isFee)
158-
}
159-
16097
// Transaction is the info returned per transaction by the /transactions and /transaction endpoint.
16198
type Transaction struct {
162-
TxID string `json:"txID"`
163-
InternalID string `json:"internalID"`
164-
NumConfirmations int `json:"numConfirmations"`
165-
NumConfirmationsComplete int `json:"numConfirmationsComplete"`
166-
Type string `json:"type"`
167-
Status accounts.TxStatus `json:"status"`
168-
Amount FormattedAmount `json:"amount"`
169-
AmountAtTime FormattedAmount `json:"amountAtTime"`
170-
DeductedAmountAtTime FormattedAmount `json:"deductedAmountAtTime"`
171-
Fee FormattedAmount `json:"fee"`
172-
Time *string `json:"time"`
173-
Addresses []string `json:"addresses"`
174-
Note string `json:"note"`
99+
TxID string `json:"txID"`
100+
InternalID string `json:"internalID"`
101+
NumConfirmations int `json:"numConfirmations"`
102+
NumConfirmationsComplete int `json:"numConfirmationsComplete"`
103+
Type string `json:"type"`
104+
Status accounts.TxStatus `json:"status"`
105+
Amount coin.FormattedAmountWithConversions `json:"amount"`
106+
AmountAtTime coin.FormattedAmountWithConversions `json:"amountAtTime"`
107+
DeductedAmountAtTime coin.FormattedAmountWithConversions `json:"deductedAmountAtTime"`
108+
Fee coin.FormattedAmountWithConversions `json:"fee"`
109+
Time *string `json:"time"`
110+
Addresses []string `json:"addresses"`
111+
Note string `json:"note"`
175112

176113
// BTC specific fields.
177-
VSize int64 `json:"vsize"`
178-
Size int64 `json:"size"`
179-
Weight int64 `json:"weight"`
180-
FeeRatePerKb FormattedAmount `json:"feeRatePerKb"`
114+
VSize int64 `json:"vsize"`
115+
Size int64 `json:"size"`
116+
Weight int64 `json:"weight"`
117+
FeeRatePerKb coin.FormattedAmountWithConversions `json:"feeRatePerKb"`
181118

182119
// ETH specific fields
183120
Gas uint64 `json:"gas"`
@@ -196,19 +133,20 @@ func (handlers *Handlers) ensureAccountInitialized(h func(*http.Request) (interf
196133
// getTxInfoJSON encodes a given transaction in JSON.
197134
// If `detail` is false, Coin related details, fees and historical fiat amount won't be included.
198135
func (handlers *Handlers) getTxInfoJSON(txInfo *accounts.TransactionData, detail bool) Transaction {
199-
var feeString FormattedAmount
136+
accountConfig := handlers.account.Config()
137+
var feeString coin.FormattedAmountWithConversions
200138
if txInfo.Fee != nil {
201-
feeString = handlers.formatAmountAsJSON(*txInfo.Fee, true)
139+
feeString = txInfo.Fee.FormatWithConversions(handlers.account.Coin(), true, accountConfig.RateUpdater)
202140
}
203-
amount := handlers.formatAmountAsJSON(txInfo.Amount, false)
141+
amount := txInfo.Amount.FormatWithConversions(handlers.account.Coin(), false, accountConfig.RateUpdater)
204142
var formattedTime *string
205143
timestamp := txInfo.Timestamp
206144
if timestamp == nil {
207145
timestamp = txInfo.CreatedTimestamp
208146
}
209147

210-
deductedAmountAtTime := handlers.formatAmountAtTimeAsJSON(txInfo.DeductedAmount, timestamp)
211-
amountAtTime := handlers.formatAmountAtTimeAsJSON(txInfo.Amount, timestamp)
148+
deductedAmountAtTime := txInfo.DeductedAmount.FormatWithConversionsAtTime(handlers.account.Coin(), timestamp, accountConfig.RateUpdater)
149+
amountAtTime := txInfo.Amount.FormatWithConversionsAtTime(handlers.account.Coin(), timestamp, accountConfig.RateUpdater)
212150

213151
if timestamp != nil {
214152
t := timestamp.Format(time.RFC3339)
@@ -247,7 +185,7 @@ func (handlers *Handlers) getTxInfoJSON(txInfo *accounts.TransactionData, detail
247185
txInfoJSON.Weight = txInfo.Weight
248186
feeRatePerKb := txInfo.FeeRatePerKb
249187
if feeRatePerKb != nil {
250-
txInfoJSON.FeeRatePerKb = handlers.formatBTCAmountAsJSON(*feeRatePerKb, true)
188+
txInfoJSON.FeeRatePerKb = coin.ConvertBTCAmount(handlers.account.Coin(), *feeRatePerKb, true, accountConfig.RateUpdater)
251189
}
252190
case *eth.Coin:
253191
txInfoJSON.Gas = txInfo.Gas
@@ -344,6 +282,7 @@ func (handlers *Handlers) getAccountInfo(*http.Request) (interface{}, error) {
344282
}
345283

346284
func (handlers *Handlers) getUTXOs(*http.Request) (interface{}, error) {
285+
accountConfig := handlers.account.Config()
347286
result := []map[string]interface{}{}
348287

349288
t, ok := handlers.account.(*btc.Account)
@@ -378,7 +317,7 @@ func (handlers *Handlers) getUTXOs(*http.Request) (interface{}, error) {
378317
"outPoint": output.OutPoint.String(),
379318
"txId": output.OutPoint.Hash.String(),
380319
"txOutput": output.OutPoint.Index,
381-
"amount": handlers.formatBTCAmountAsJSON(btcutil.Amount(output.TxOut.Value), false),
320+
"amount": coin.ConvertBTCAmount(handlers.account.Coin(), btcutil.Amount(output.TxOut.Value), false, accountConfig.RateUpdater),
382321
"address": address,
383322
"scriptType": output.Address.AccountConfiguration.ScriptType(),
384323
"note": handlers.account.TxNote(output.OutPoint.Hash.String()),
@@ -391,11 +330,12 @@ func (handlers *Handlers) getUTXOs(*http.Request) (interface{}, error) {
391330
}
392331

393332
func (handlers *Handlers) getAccountBalance(*http.Request) (interface{}, error) {
333+
accountConfig := handlers.account.Config()
394334
type balance struct {
395-
HasAvailable bool `json:"hasAvailable"`
396-
Available FormattedAmount `json:"available"`
397-
HasIncoming bool `json:"hasIncoming"`
398-
Incoming FormattedAmount `json:"incoming"`
335+
HasAvailable bool `json:"hasAvailable"`
336+
Available coin.FormattedAmountWithConversions `json:"available"`
337+
HasIncoming bool `json:"hasIncoming"`
338+
Incoming coin.FormattedAmountWithConversions `json:"incoming"`
399339
}
400340

401341
type result struct {
@@ -410,9 +350,9 @@ func (handlers *Handlers) getAccountBalance(*http.Request) (interface{}, error)
410350
Success: true,
411351
Balance: balance{
412352
HasAvailable: accountBalance.Available().BigInt().Sign() > 0,
413-
Available: handlers.formatAmountAsJSON(accountBalance.Available(), false),
353+
Available: accountBalance.Available().FormatWithConversions(handlers.account.Coin(), false, accountConfig.RateUpdater),
414354
HasIncoming: accountBalance.Incoming().BigInt().Sign() > 0,
415-
Incoming: handlers.formatAmountAsJSON(accountBalance.Incoming(), false),
355+
Incoming: accountBalance.Incoming().FormatWithConversions(handlers.account.Coin(), false, accountConfig.RateUpdater),
416356
},
417357
}, nil
418358
}
@@ -551,6 +491,7 @@ func txProposalError(err error) (interface{}, error) {
551491
}
552492

553493
func (handlers *Handlers) postAccountTxProposal(r *http.Request) (interface{}, error) {
494+
accountConfig := handlers.account.Config()
554495
var input sendTxInput
555496
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
556497
return txProposalError(errp.WithStack(err))
@@ -561,9 +502,9 @@ func (handlers *Handlers) postAccountTxProposal(r *http.Request) (interface{}, e
561502
}
562503
return map[string]interface{}{
563504
"success": true,
564-
"amount": handlers.formatAmountAsJSON(outputAmount, false),
565-
"fee": handlers.formatAmountAsJSON(fee, true),
566-
"total": handlers.formatAmountAsJSON(total, false),
505+
"amount": outputAmount.FormatWithConversions(handlers.account.Coin(), false, accountConfig.RateUpdater),
506+
"fee": fee.FormatWithConversions(handlers.account.Coin(), true, accountConfig.RateUpdater),
507+
"total": total.FormatWithConversions(handlers.account.Coin(), false, accountConfig.RateUpdater),
567508
}, nil
568509
}
569510

backend/coins/btc/util/util.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
"strconv"
1919
"strings"
2020

21-
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/coin"
2221
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/ltc"
2322
"github.com/BitBoxSwiss/bitbox-wallet-app/util/errp"
2423
"github.com/btcsuite/btcd/btcutil"
@@ -69,8 +68,3 @@ func AddressFromPkScript(pkScript []byte, net *chaincfg.Params) (btcutil.Address
6968
}
7069
return addresses[0], nil
7170
}
72-
73-
// FormatBtcAsSat returns true if the btcUnit param is `[t]sat`.
74-
func FormatBtcAsSat(btcUnit coin.BtcUnit) bool {
75-
return btcUnit == coin.BtcUnitSats
76-
}

backend/coins/coin/amount.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ package coin
1717
import (
1818
"math/big"
1919
"strings"
20+
"time"
2021

2122
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/accounts/errors"
23+
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/rates"
2224
"github.com/BitBoxSwiss/bitbox-wallet-app/util/errp"
25+
"github.com/btcsuite/btcd/btcutil"
2326
)
2427

2528
// Amount represents an amount in the smallest coin unit (e.g. satoshi).
@@ -76,6 +79,64 @@ func (amount Amount) BigInt() *big.Int {
7679
return new(big.Int).Set(amount.n)
7780
}
7881

82+
// FormattedAmountWithConversions and json tags.
83+
type FormattedAmountWithConversions struct {
84+
Amount string `json:"amount"`
85+
Unit string `json:"unit"`
86+
Conversions map[string]string `json:"conversions"`
87+
// Estimated flag is enabled if the Conversions map was expected to
88+
// be calculated using historical rates, but latest rates have been used instead.
89+
Estimated bool `json:"estimated"`
90+
}
91+
92+
// FormatWithConversions formats the Amount and adds fiat conversions.
93+
func (amount Amount) FormatWithConversions(accountCoin Coin, isFee bool, rateUpdater *rates.RateUpdater) FormattedAmountWithConversions {
94+
return FormattedAmountWithConversions{
95+
Amount: accountCoin.FormatAmount(amount, isFee),
96+
Unit: accountCoin.GetFormatUnit(isFee),
97+
Conversions: Conversions(
98+
amount,
99+
accountCoin,
100+
isFee,
101+
rateUpdater),
102+
}
103+
}
104+
105+
// FormatWithConversionsAtTime formats the Amount and adds fiat conversions at a given timestamp.
106+
func (amount Amount) FormatWithConversionsAtTime(accountCoin Coin, timeStamp *time.Time, rateUpdater *rates.RateUpdater) FormattedAmountWithConversions {
107+
var conversions map[string]string
108+
var estimated bool
109+
110+
if timeStamp == nil {
111+
conversions = Conversions(
112+
amount,
113+
accountCoin,
114+
false,
115+
rateUpdater,
116+
)
117+
estimated = true
118+
} else {
119+
conversions, estimated = ConversionsAtTime(
120+
amount,
121+
accountCoin,
122+
false,
123+
rateUpdater,
124+
timeStamp,
125+
)
126+
}
127+
return FormattedAmountWithConversions{
128+
Amount: accountCoin.FormatAmount(amount, false),
129+
Unit: accountCoin.GetFormatUnit(false),
130+
Conversions: conversions,
131+
Estimated: estimated,
132+
}
133+
}
134+
135+
// ConvertBTCAmount formats a btcUtil.Amount and its fiat conversions into a FiatConvertedAmount.
136+
func ConvertBTCAmount(accountCoin Coin, amount btcutil.Amount, isFee bool, rateUpdater *rates.RateUpdater) FormattedAmountWithConversions {
137+
return NewAmountFromInt64(int64(amount)).FormatWithConversions(accountCoin, isFee, rateUpdater)
138+
}
139+
79140
// SendAmount is either a concrete amount, or "all"/"max". The concrete amount is user input and is
80141
// parsed/validated in Amount().
81142
type SendAmount struct {

backend/coins/coin/amount_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"testing/quick"
2323

2424
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/coin"
25+
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/rates"
2526
"github.com/stretchr/testify/require"
2627
)
2728

@@ -101,3 +102,13 @@ func TestSendAmount(t *testing.T) {
101102
require.Equal(t, int64(0), amount.BigInt().Int64())
102103

103104
}
105+
106+
func TestFormatWithConversionsAtTime(t *testing.T) {
107+
testCoin := mockCoin(t)
108+
rateUpdater := rates.MockRateUpdater()
109+
110+
amount := coin.NewAmountFromInt64(12345678)
111+
formattedAmount := amount.FormatWithConversionsAtTime(&testCoin, nil, rateUpdater)
112+
// conversions are tested separately, we just check for the estimated flag when timestamp is nil.
113+
require.True(t, formattedAmount.Estimated)
114+
}

backend/coins/coin/conversions.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func FormatAsCurrency(amount *big.Rat, currency string) string {
4848
}
4949

5050
// Conversions handles fiat conversions.
51-
func Conversions(amount Amount, coin Coin, isFee bool, ratesUpdater *ratesPkg.RateUpdater, formatBtcAsSats bool) map[string]string {
51+
func Conversions(amount Amount, coin Coin, isFee bool, ratesUpdater *ratesPkg.RateUpdater) map[string]string {
5252
conversions := map[string]string{}
5353
rates := ratesUpdater.LatestPrice()
5454
if rates != nil {
@@ -66,11 +66,11 @@ func Conversions(amount Amount, coin Coin, isFee bool, ratesUpdater *ratesPkg.Ra
6666
// ConversionsAtTime handles fiat conversions at a specific time.
6767
// It returns the map of conversions and a bool indicating if the rates have been estimated
6868
// using the latest instead of the historical rates for recent transactions.
69-
func ConversionsAtTime(amount Amount, coin Coin, isFee bool, ratesUpdater *ratesPkg.RateUpdater, formatBtcAsSats bool, timeStamp *time.Time) (map[string]string, bool) {
69+
func ConversionsAtTime(amount Amount, coin Coin, isFee bool, ratesUpdater *ratesPkg.RateUpdater, timeStamp *time.Time) (map[string]string, bool) {
7070
latestRatesTime := ratesUpdater.HistoryLatestTimestampCoin(string(coin.Code()))
7171
historicalRatesNotAvailable := latestRatesTime.IsZero() || latestRatesTime.Before(*timeStamp)
7272
if historicalRatesNotAvailable && time.Since(*timeStamp) < 2*time.Hour {
73-
return Conversions(amount, coin, isFee, ratesUpdater, formatBtcAsSats), true
73+
return Conversions(amount, coin, isFee, ratesUpdater), true
7474
}
7575

7676
conversions := map[string]string{}

0 commit comments

Comments
 (0)