From fe94945f2ae95a96f011cddf4d7a8604c1e76d2e Mon Sep 17 00:00:00 2001
From: Daniel <10026790+dnlggr@users.noreply.github.com>
Date: Fri, 11 Mar 2022 14:29:06 +0100
Subject: [PATCH] feat: satscomma formatting for bitcoin balances (#171)
* Use satscomma for btc formatting
* Harden balance parsing (a bit)
* Add tests for large values
---
src/components/Balance.jsx | 149 ++++++++++++++++----------
src/components/Balance.test.jsx | 114 ++++++++++++++++++++
src/components/CurrentWalletMagic.jsx | 4 +-
src/components/DisplayAccounts.jsx | 6 +-
src/components/DisplayUTXOs.jsx | 2 +-
src/components/Navbar.jsx | 2 +-
src/index.css | 16 ---
7 files changed, 216 insertions(+), 77 deletions(-)
create mode 100644 src/components/Balance.test.jsx
diff --git a/src/components/Balance.jsx b/src/components/Balance.jsx
index efdb8388..4e0c0d9c 100644
--- a/src/components/Balance.jsx
+++ b/src/components/Balance.jsx
@@ -1,74 +1,115 @@
import React, { useState, useEffect } from 'react'
-import Sprite from './Sprite'
import { BTC, SATS, btcToSats, satsToBtc } from '../utils'
+import Sprite from './Sprite'
-const UNIT_MODE_BTC = 0
-const UNIT_MODE_SATS = 1
-const UNIT_MODE_HIDDEN = 2
+const DISPLAY_MODE_BTC = 0
+const DISPLAY_MODE_SATS = 1
+const DISPLAY_MODE_HIDDEN = 2
-const getUnitMode = (unit, showBalance) => {
- if (showBalance && unit === SATS) return UNIT_MODE_SATS
- if (showBalance && unit === BTC) return UNIT_MODE_BTC
+const decimalPoint = '\u002E'
+const nbHalfSpace = '\u202F'
- return UNIT_MODE_HIDDEN
+const getDisplayMode = (unit, showBalance) => {
+ if (showBalance && unit === SATS) return DISPLAY_MODE_SATS
+ if (showBalance && unit === BTC) return DISPLAY_MODE_BTC
+
+ return DISPLAY_MODE_HIDDEN
}
-export default function Balance({ value, unit, showBalance = false }) {
- const [unitMode, setUnitMode] = useState(getUnitMode(unit, showBalance))
-
- useEffect(() => {
- setUnitMode(getUnitMode(unit, showBalance))
- }, [unit, showBalance])
-
- if (unitMode === UNIT_MODE_HIDDEN) {
- return (
-
-
- *****
-
-
-
-
-
- )
- }
-
- const btcFormatter = new Intl.NumberFormat('en-US', {
+const formatBtc = (value) => {
+ const formatter = new Intl.NumberFormat('en-US', {
minimumIntegerDigits: 1,
minimumFractionDigits: 8,
})
- const satFormatter = new Intl.NumberFormat('en-US', {
+ const numberString = formatter.format(value)
+
+ const [integerPart, fractionalPart] = numberString.split(decimalPoint)
+
+ const formattedFractionalPart = fractionalPart
+ .split('')
+ .map((char, idx) => (idx === 2 || idx === 5 ? `${nbHalfSpace}${char}` : char))
+ .join('')
+
+ return integerPart + decimalPoint + formattedFractionalPart
+}
+
+const formatSats = (value) => {
+ const formatter = new Intl.NumberFormat('en-US', {
minimumIntegerDigits: 1,
minimumFractionDigits: 0,
})
- const isSats = value === parseInt(value)
- const isBTC = !isSats && typeof value === 'string' && value.indexOf('.') > -1
+ return formatter.format(value)
+}
- const btcSymbol = {'\u20BF'}
- const satSymbol =
-
- const balanceJSX = (symbolJSX, formattedValue, isPrefix = true) => {
- return (
-
- {isPrefix ? symbolJSX : ''}
- {formattedValue}
- {!isPrefix ? symbolJSX : ''}
-
- )
- }
-
- if (isBTC && unitMode === UNIT_MODE_BTC) return balanceJSX(btcSymbol, btcFormatter.format(value))
- if (isSats && unitMode === UNIT_MODE_SATS) return balanceJSX(satSymbol, satFormatter.format(value), false)
-
- if (isBTC && unitMode === UNIT_MODE_SATS) return balanceJSX(satSymbol, satFormatter.format(btcToSats(value)), false)
- if (isSats && unitMode === UNIT_MODE_BTC) return balanceJSX(btcSymbol, btcFormatter.format(satsToBtc(value)))
-
- // Something unexpected happened. Simply render what was passed in the props.
+const BalanceComponent = ({ symbol, value, symbolIsPrefix }) => {
return (
-
- {value} {unit}
+
+ {symbolIsPrefix && symbol}
+ {value}
+ {!symbolIsPrefix && symbol}
)
}
+
+/**
+ * Render balances nicely formatted.
+ *
+ * @param {valueString}: The balance value to render.
+ * Integer values are treated as SATS while decimal numbers with a decimal point (.) are treated as BTC.
+ * For example:
+ * - 0, 10, 2100000000000000 are treated as a value in SATS; while
+ * - 0.00000000, 150.00000001, 21000000.00000000 are treated as a value in BTC.
+ * @param {convertToUnit}: The unit to convert the `valueString` to.
+ * Possible options are `BTC` and `SATS` from `src/utils.js`
+ * @param {showBalance}: A flag indicating whether to render or hide the balance.
+ * Hidden balances are masked with `*****`.
+ */
+export default function Balance({ valueString, convertToUnit, showBalance = false }) {
+ const [displayMode, setDisplayMode] = useState(DISPLAY_MODE_HIDDEN)
+
+ useEffect(() => {
+ setDisplayMode(getDisplayMode(convertToUnit, showBalance))
+ }, [convertToUnit, showBalance])
+
+ if (displayMode === DISPLAY_MODE_HIDDEN) {
+ return (
+
+
+
+ }
+ value={'*****'}
+ symbolIsPrefix={false}
+ />
+ )
+ }
+
+ if (typeof valueString !== 'string') {
+ console.warn(' component expects string input')
+ return
+ }
+
+ // Treat integers as sats.
+ const valueIsSats = valueString === Number.parseInt(valueString).toString()
+ // Treat decimal numbers as btc.
+ const valueIsBtc = !valueIsSats && !Number.isNaN(Number.parseFloat(valueString)) && valueString.indexOf('.') > -1
+
+ const btcSymbol = {'\u20BF'}
+ const satSymbol =
+
+ if (valueIsBtc && displayMode === DISPLAY_MODE_BTC)
+ return
+ if (valueIsSats && displayMode === DISPLAY_MODE_SATS)
+ return
+
+ if (valueIsBtc && displayMode === DISPLAY_MODE_SATS)
+ return
+ if (valueIsSats && displayMode === DISPLAY_MODE_BTC)
+ return
+
+ console.warn(' component cannot determine balance format')
+ return
+}
diff --git a/src/components/Balance.test.jsx b/src/components/Balance.test.jsx
new file mode 100644
index 00000000..287ede3c
--- /dev/null
+++ b/src/components/Balance.test.jsx
@@ -0,0 +1,114 @@
+import React from 'react'
+import { render, screen } from '../testUtils'
+import { BTC, SATS } from '../utils'
+
+import Balance from './Balance'
+
+describe('', () => {
+ it('should render BTC using satscomma formatting', () => {
+ render()
+ expect(screen.getByText(`123.45 600 000`)).toBeInTheDocument()
+ })
+
+ it('should hide balance for BTC by default', () => {
+ render()
+ expect(screen.getByText(`*****`)).toBeInTheDocument()
+ expect(screen.queryByText(`123.45 600 000`)).not.toBeInTheDocument()
+ })
+
+ it('should hide balance for SATS by default', () => {
+ render()
+ expect(screen.getByText(`*****`)).toBeInTheDocument()
+ expect(screen.queryByText(`123`)).not.toBeInTheDocument()
+ })
+
+ it('should render a string BTC value correctly as BTC', () => {
+ render()
+ expect(screen.getByText(`123.03 224 961`)).toBeInTheDocument()
+ })
+
+ it('should render a string BTC value correctly as SATS', () => {
+ render()
+ expect(screen.getByText(`12,303,224,961`)).toBeInTheDocument()
+ })
+
+ it('should render a zero string BTC value correctly as BTC', () => {
+ render()
+ expect(screen.getByText(`0.00 000 000`)).toBeInTheDocument()
+ })
+
+ it('should render a zero string BTC value correctly as SATS', () => {
+ render()
+ expect(screen.getByText(`0`)).toBeInTheDocument()
+ })
+
+ it('should render a large string BTC value correctly as BTC', () => {
+ render()
+ expect(screen.getByText(`20,999,999.97 690 000`)).toBeInTheDocument()
+ })
+
+ it('should render a large string BTC value correctly as SATS', () => {
+ render()
+ expect(screen.getByText(`2,099,999,997,690,000`)).toBeInTheDocument()
+ })
+
+ it('should render a max string BTC value correctly as BTC', () => {
+ render()
+ expect(screen.getByText(`21,000,000.00 000 000`)).toBeInTheDocument()
+ })
+
+ it('should render a max string BTC value correctly as SATS', () => {
+ render()
+ expect(screen.getByText(`2,100,000,000,000,000`)).toBeInTheDocument()
+ })
+
+ it('should render a number BTC value as fallback', () => {
+ render()
+ expect(screen.getByText(`123.456`)).toBeInTheDocument()
+ })
+
+ it('should render a string SATS value correctly as SATS', () => {
+ render()
+ expect(screen.getByText(`43,000`)).toBeInTheDocument()
+ })
+
+ it('should render a string SATS value correctly as BTC', () => {
+ render()
+ expect(screen.getByText(`0.00 043 000`)).toBeInTheDocument()
+ })
+
+ it('should render a zero string SATS value correctly as BTC', () => {
+ render()
+ expect(screen.getByText(`0.00 000 000`)).toBeInTheDocument()
+ })
+
+ it('should render a zero string SATS value correctly as SATS', () => {
+ render()
+ expect(screen.getByText(`0`)).toBeInTheDocument()
+ })
+
+ it('should render a large string SATS value correctly as BTC', () => {
+ render()
+ expect(screen.getByText(`20,999,999.97 690 000`)).toBeInTheDocument()
+ })
+
+ it('should render a large string SATS value correctly as SATS', () => {
+ render()
+ expect(screen.getByText(`2,099,999,997,690,000`)).toBeInTheDocument()
+ })
+
+ it('should render a max string SATS value correctly as BTC', () => {
+ render()
+ expect(screen.getByText(`21,000,000.00 000 000`)).toBeInTheDocument()
+ })
+
+ it('should render a max string SATS value correctly as SATS', () => {
+ render()
+ expect(screen.getByText(`2,100,000,000,000,000`)).toBeInTheDocument()
+ })
+
+ it('should render a number SATS value as fallback', () => {
+ render()
+ expect(screen.getByText(`43000`)).toBeInTheDocument()
+ })
+})
diff --git a/src/components/CurrentWalletMagic.jsx b/src/components/CurrentWalletMagic.jsx
index c6915363..c79f775b 100644
--- a/src/components/CurrentWalletMagic.jsx
+++ b/src/components/CurrentWalletMagic.jsx
@@ -13,7 +13,7 @@ const WalletHeader = ({ name, balance, unit, showBalance }) => {
{walletDisplayName(name)}
-
+
)
@@ -54,7 +54,7 @@ const PrivacyLevel = ({ numAccounts, level, balance }) => {
{outlinedShields}
-
+
)
diff --git a/src/components/DisplayAccounts.jsx b/src/components/DisplayAccounts.jsx
index db96ae0e..5154e6c4 100644
--- a/src/components/DisplayAccounts.jsx
+++ b/src/components/DisplayAccounts.jsx
@@ -18,7 +18,7 @@ const BranchEntry = ({ entry, ...props }) => {
{hdPath}
-
+
{address} {labels && {labels}}
@@ -45,7 +45,7 @@ export default function DisplayAccounts({ accounts, ...props }) {
-
+
@@ -65,7 +65,7 @@ export default function DisplayAccounts({ accounts, ...props }) {
{titleize(type)}
-
+
diff --git a/src/components/DisplayUTXOs.jsx b/src/components/DisplayUTXOs.jsx
index 52fa47f4..9ec6bd75 100644
--- a/src/components/DisplayUTXOs.jsx
+++ b/src/components/DisplayUTXOs.jsx
@@ -61,7 +61,7 @@ const Utxo = ({ utxo, ...props }) => {
-
+
{utxo.confirmations} Confirmations
diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx
index 4d8219ce..57c3ef52 100644
--- a/src/components/Navbar.jsx
+++ b/src/components/Navbar.jsx
@@ -15,7 +15,7 @@ const WalletPreview = ({ wallet, walletInfo, unit, showBalance }) => {
{wallet &&
{walletDisplayName(wallet.name)}
}
{walletInfo && walletInfo?.total_balance && unit ? (
-
+
) : (
diff --git a/src/index.css b/src/index.css
index 6745b76e..8bdaf537 100644
--- a/src/index.css
+++ b/src/index.css
@@ -578,22 +578,6 @@ h2 {
}
}
-/* Balance Styles */
-
-.balance-wrapper {
- display: inline-flex;
- align-items: center;
-}
-
-.balance {
- display: flex;
- align-items: center;
-}
-
-.bitcoin-symbol {
- padding-right: 0.1em;
-}
-
/* Onboarding */
.onboarding button {