Skip to content

Commit 7f0ac69

Browse files
relativisticelectronKim Neunertmoneymanolis
authored
UIUX: Better balance display (#1841)
* Better alignment and introduction of spaces and colours for BTC amounts. Co-authored-by: Kim Neunert <[email protected]> Co-authored-by: moneymanolis <[email protected]>
1 parent 66d56a2 commit 7f0ac69

File tree

19 files changed

+723
-251
lines changed

19 files changed

+723
-251
lines changed

cypress.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"spec_fees.js",
1010
"spec_rescan.js",
1111
"spec_qr_signing.js",
12+
"spec_balances_amounts.js",
1213
"spec_wallet_send.js",
1314
"spec_wallet_utxo.js",
1415
"spec_plugins.js",
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//
2+
describe('Test the rendering of balances and amounts', () => {
3+
before(() => {
4+
Cypress.config('includeShadowDom', true)
5+
cy.visit('/')
6+
})
7+
8+
// Keeps the session cookie alive, Cypress by default clears all cookies before each test
9+
beforeEach(() => {
10+
cy.viewport(1200,660)
11+
Cypress.Cookies.preserveOnce('session')
12+
})
13+
14+
it('Total balance of 20 BTC', () => {
15+
/* This is how the DOM looks like
16+
<th id="fullbalance_amount" class="right-align">
17+
20.0
18+
<span class="unselectable transparent-text">0</span>
19+
<span class="thousand-digits-in-btc-amount">
20+
<span class="unselectable transparent-text">0</span>
21+
<span class="unselectable transparent-text">0</span>
22+
<span class="unselectable transparent-text">0</span>
23+
</span>
24+
<span class="last-digits-in-btc-amount">
25+
<span class="unselectable transparent-text">0</span>
26+
<span class="unselectable transparent-text">0</span>
27+
<span class="unselectable transparent-text">0</span>
28+
</span>
29+
</th>
30+
*/
31+
cy.selectWallet('Ghost wallet')
32+
cy.get('#fullbalance_amount').should('have.text', '20.00000000') // should('have.text') returns ALL textContents (descendants and unvisible text)
33+
cy.get('#fullbalance_amount').find('span').first().should('have.text', '0').and('not.be.visible')
34+
cy.get('#fullbalance_amount').find('.thousand-digits-in-btc-amount').children().each((element) => {
35+
cy.wrap(element).should('have.text', '0')
36+
cy.wrap(element).should('not.be.visible')
37+
});
38+
cy.get('#fullbalance_amount').find('.last-digits-in-btc-amount').children().each((element) => {
39+
cy.wrap(element).should('have.text', '0')
40+
cy.wrap(element).should('not.be.visible')
41+
});
42+
})
43+
44+
it('Unconfirmed balance of 0.05 BTC', () => {
45+
/* This is how the DOM looks like
46+
<th id="unconfirmed_amount" class="right-align">
47+
0.05
48+
<span class="thousand-digits-in-btc-amount">
49+
<span class="unselectable transparent-text">0</span>
50+
<span class="unselectable transparent-text">0</span>
51+
<span class="unselectable transparent-text">0</span>
52+
</span>
53+
<span class="last-digits-in-btc-amount">
54+
<span class="unselectable transparent-text">0</span>
55+
<span class="unselectable transparent-text">0</span>
56+
<span class="unselectable transparent-text">0</span>
57+
</span>
58+
</th>
59+
*/
60+
// We get 5 mio. sats from a funding wallet
61+
// TODO: If this funding wallet is used more, move it to a seperate spec file
62+
cy.addHotDevice('Satoshis hot keys','bitcoin')
63+
cy.addWallet('Funding wallet', 'segwit', 'funded', 'btc', 'singlesig', 'Satoshis hot keys')
64+
cy.selectWallet('Funding wallet')
65+
cy.get('#btn_send').click()
66+
cy.get('#recipient_0').find('#address').invoke('val', 'bcrt1qvtdx75y4554ngrq6aff3xdqnvjhmct5wck95qs')
67+
cy.get('#recipient_0').find('#amount').type(0.05, { force: true })
68+
cy.get('#toggle_advanced').click()
69+
cy.get('.fee_container').find('#fee_option_manual').click()
70+
cy.get('#fee_manual').find('#fee_rate').clear( { force: true })
71+
cy.get('#fee_manual').find('#fee_rate').type(5, { force: true }) // Should be a fee of 709 sats.
72+
cy.get('#create_psbt_btn').click()
73+
cy.get('body').contains("Paste signed transaction")
74+
cy.get('#satoshis_hot_keys_tx_sign_btn').click()
75+
cy.get('#satoshis_hot_keys_hot_sign_btn').click()
76+
cy.get('#hot_enter_passphrase__submit').click()
77+
cy.get('#broadcast_local_btn').click()
78+
cy.reload()
79+
cy.selectWallet('Ghost wallet')
80+
cy.get('#unconfirmed_amount').should('have.text', '0.05000000')
81+
cy.get('#unconfirmed_amount').find('.thousand-digits-in-btc-amount').children().each((element) => {
82+
cy.wrap(element).should('have.text', '0')
83+
cy.wrap(element).should('not.be.visible')
84+
});
85+
cy.get('#unconfirmed_amount').find('.last-digits-in-btc-amount').children().each((element) => {
86+
cy.wrap(element).should('have.text', '0')
87+
cy.wrap(element).should('not.be.visible')
88+
});
89+
})
90+
91+
it('Total balance with all digits', () => {
92+
/* This is how the DOM looks like
93+
<th id="fullbalance_amount" class="right-align">
94+
19.94
95+
<span class="thousand-digits-in-btc-amount">999</span>
96+
<span class="last-digits-in-btc-amount">291</span>
97+
</th>
98+
*/
99+
// Let's use the funding wallet
100+
// Works as long as the fee was 709 and the original balance of the funding wallet was 20 BTC
101+
cy.selectWallet('Funding wallet')
102+
cy.get('#fullbalance_amount').should('have.text', '19.94999291')
103+
cy.get('#fullbalance_amount').find('.thousand-digits-in-btc-amount').should('have.text', '999')
104+
cy.get('#fullbalance_amount').find('.thousand-digits-in-btc-amount').should('have.css', 'color','rgb(145, 145, 145)')
105+
cy.get('#fullbalance_amount').find('.last-digits-in-btc-amount').should('have.text', '291')
106+
cy.get('#fullbalance_amount').find('.last-digits-in-btc-amount').should('have.css', 'color','rgb(121, 121, 121)')
107+
cy.get('#fullbalance_amount').children().each((element) => {
108+
cy.wrap(element).should('be.visible')
109+
cy.log(element)
110+
});
111+
})
112+
113+
// TODO: Test (new) amount display once implemented, e.g. in sending dialogue, could probably be done in one of the tests above.
114+
115+
})

cypress/support/commands.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ Cypress.Commands.add("deleteWallet", (name) => {
216216
Cypress.Commands.add("selectWallet", (name) => {
217217
cy.get('body').then(($body) => {
218218
if ($body.text().includes(name)) {
219-
cy.contains(name).click()
219+
cy.contains(name).click( {force: true} )
220220
}
221221
})
222222
})
@@ -225,8 +225,8 @@ Cypress.Commands.add("mine2wallet", (chain) => {
225225
// Fund it and check the balance
226226
// Only works if a wallet is selected, use addHotWallet / selectWallet commands before if needed
227227
cy.get('#btn_transactions').click()
228-
cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") }).then(($span) => {
229-
const oldBalance = parseFloat($span.text())
228+
cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") }).then(($header) => {
229+
const oldBalance = parseFloat($header.text())
230230
if (chain=="elm" || chain=="elements") {
231231
cy.task("elm:mine")
232232
} else if (chain=="btc" || chain=="bitcoin") {
@@ -235,8 +235,8 @@ Cypress.Commands.add("mine2wallet", (chain) => {
235235
throw new Error("Unknown chain: " + chain)
236236
}
237237
cy.waitUntil( () => cy.reload().get('#fullbalance_amount', { timeout: 3000 })
238-
.then(($span) => {
239-
const n = parseFloat($span.text())
238+
.then(($header) => {
239+
const n = parseFloat($header.text())
240240
return n > oldBalance
241241
})
242242
, {

src/cryptoadvance/specter/cli/cli_noded.py

Lines changed: 63 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
import time
99
from pathlib import Path
1010
from threading import Event
11+
from xmlrpc.client import Boolean
1112

1213
import click
1314
import psutil
1415
from flask import Config
16+
from requests.exceptions import ConnectionError
1517

1618
from ..config import DEFAULT_CONFIG
1719
from ..process_controller.elementsd_controller import ElementsPlainController
@@ -352,15 +354,15 @@ def cleanup():
352354

353355
if node_impl == "elements":
354356
prepare_elements_default_wallet(my_node)
355-
356-
if mining:
357-
miner_loop(
358-
node_impl,
359-
my_node,
360-
config_obj["SPECTER_DATA_FOLDER"],
361-
mining_every_x_seconds,
362-
echo,
363-
)
357+
# Mining/NOP loop (necessary to keep the Python process running)
358+
endless_loop(
359+
node_impl,
360+
my_node,
361+
mining,
362+
config_obj["SPECTER_DATA_FOLDER"],
363+
mining_every_x_seconds,
364+
echo,
365+
)
364366

365367

366368
def prepare_elements_default_wallet(my_node):
@@ -386,61 +388,75 @@ def prepare_elements_default_wallet(my_node):
386388
rpc.generatetoaddress(101, unconfidential)
387389

388390

389-
def miner_loop(node_impl, my_node, data_folder, mining_every_x_seconds, echo):
390-
"An endless loop mining bitcoin"
391-
392-
echo(
393-
"Now, mining a block every %f seconds, avoid it via --no-mining"
394-
% mining_every_x_seconds
395-
)
396-
mine_2_specter_wallets(node_impl, my_node, data_folder, echo)
391+
def endless_loop(
392+
node_impl, my_node, mining: Boolean, data_folder, mining_every_x_seconds, echo
393+
):
394+
"""This loop can enable continuous mining"""
397395

398-
# make them spendable
399-
my_node.mine(block_count=100)
400-
echo(
401-
f"height: {my_node.rpcconn.get_rpc().getblockchaininfo()['blocks']} | ",
402-
nl=False,
403-
)
396+
# To stop the Python process
404397
exit = Event()
405398

406-
def exit_now(signo, _frame):
399+
def exit_now(signum, frame):
400+
echo(f"Signal {signum} received. Terminating the Python process. Bye, bye!")
407401
exit.set()
408402

409-
for sig in ("HUP", "INT"):
410-
signal.signal(getattr(signal, "SIG" + sig), exit_now)
403+
signal.signal(signal.SIGINT, exit_now)
404+
signal.signal(signal.SIGHUP, exit_now)
405+
signal.signal(signal.SIGTERM, exit_now)
406+
# SIGKILL cannot be caught
407+
408+
if mining:
409+
echo(
410+
"Now, mining a block every %f seconds, avoid it via --no-mining"
411+
% mining_every_x_seconds
412+
)
413+
mine_2_specter_wallets(node_impl, my_node, data_folder, echo)
414+
# make them spendable
415+
my_node.mine(block_count=100)
416+
echo(
417+
f"height: {my_node.rpcconn.get_rpc().getblockchaininfo()['blocks']} | ",
418+
nl=False,
419+
)
420+
else:
421+
echo("Press Ctrl-C to abort and stop the node")
422+
411423
prevent_mining_file = Path("prevent_mining")
412424
i = 0
413-
while True:
425+
while not exit.is_set():
414426
try:
415427
current_height = my_node.rpcconn.get_rpc().getblockchaininfo()["blocks"]
416428
exit.wait(mining_every_x_seconds)
417-
if not prevent_mining_file.is_file():
429+
# Having a prevent_mining_file overrides the mining cli option
430+
if mining and not prevent_mining_file.is_file():
418431
my_node.mine()
419-
else:
432+
echo("%i" % (i % 10), prefix=False, nl=False)
433+
if i % 10 == 9:
434+
echo(" ", prefix=False, nl=False)
435+
i += 1
436+
if i >= 50:
437+
i = 0
438+
echo("", prefix=False)
439+
echo(
440+
f"height: {current_height} | ",
441+
nl=False,
442+
)
443+
elif mining:
420444
echo("X", prefix=False, nl=False)
421445
continue
422446

423-
echo("%i" % (i % 10), prefix=False, nl=False)
424-
if i % 10 == 9:
425-
echo(" ", prefix=False, nl=False)
426-
i += 1
427-
if i >= 50:
428-
i = 0
429-
echo("", prefix=False)
430-
echo(
431-
f"height: {current_height} | ",
432-
nl=False,
433-
)
434-
447+
except ConnectionError as nce:
448+
# This terminates the Python processes if the bitcoind / elementsd (child) processes are somehow terminated
449+
echo("Exiting endless loop due to lost RPC connection.")
450+
break
435451
except Exception as e:
436452
logger.debug(
437-
f"Caught {e}, Couldn't mine, assume SIGTERM occured => exiting!"
453+
f"Caught {e.__module__}, Couldn't mine, assume SIGTERM occured => exiting!"
438454
)
439-
echo(f"THE_END(@height:{current_height})")
440-
if prevent_mining_file.is_file():
441-
echo("Deleting file prevent_mining")
442-
prevent_mining_file.unlink()
443455
break
456+
if prevent_mining_file.is_file():
457+
echo("Deleting file prevent_mining")
458+
prevent_mining_file.unlink()
459+
echo(f"THE_END(@height:{current_height})")
444460

445461

446462
def mine_2_specter_wallets(node_impl, my_node, data_folder, echo):
@@ -451,6 +467,7 @@ def mine_2_specter_wallets(node_impl, my_node, data_folder, echo):
451467
# Using the dict key, not the wallet name
452468
exception = "fresh_wallet"
453469
try:
470+
logger.debug(f"Funding wallets in {data_folder}/wallets")
454471
for address in fetch_wallet_addresses_for_mining(
455472
node_impl, data_folder, exception
456473
):

src/cryptoadvance/specter/process_controller/node_controller.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -560,8 +560,6 @@ def fetch_wallet_addresses_for_mining(node_impl, data_folder, exception=None):
560560
Parses all the wallet jsons in the folder (default ~/.specter/wallets/regtest) and returns an array with the addresses.
561561
Pass a wallet name via the exception argument so that this wallet's addresses are not included.
562562
"""
563-
print(f"{data_folder}/wallets")
564-
print(os.listdir(f"{data_folder}"))
565563
addresses_all = []
566564
for folder in [
567565
folder for folder in os.listdir(data_folder) if folder.startswith("wallets")

src/cryptoadvance/specter/server_endpoints/filters.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from flask import Blueprint
44
from jinja2 import pass_context
55
from ..helpers import to_ascii20
6+
from ..util.common import format_btc_amount_as_sats, format_btc_amount
67

78
filters_bp = Blueprint("filters", __name__)
89

@@ -27,6 +28,43 @@ def timedatetime(context, s):
2728
return format(datetime.fromtimestamp(s), "%d.%m.%Y %H:%M")
2829

2930

31+
@pass_context
32+
@filters_bp.app_template_filter("average_of_attribute")
33+
def average_of_attribute(context, values, attribute):
34+
dicts = [
35+
getattr(value, attribute)
36+
for value in values
37+
if getattr(value, attribute) is not None
38+
]
39+
return sum(dicts) / len(dicts) if dicts else None
40+
41+
42+
@pass_context
43+
@filters_bp.app_template_filter("btcunitamount_fixed_decimals")
44+
def btcunitamount_fixed_decimals(
45+
context,
46+
value,
47+
maximum_digits_to_strip=7,
48+
minimum_digits_to_strip=6,
49+
enable_digit_formatting=True,
50+
):
51+
if app.specter.hide_sensitive_info:
52+
return "#########"
53+
if value is None:
54+
return "Unknown"
55+
if value < 0 and app.specter.is_liquid:
56+
return "Confidential"
57+
if app.specter.unit == "sat":
58+
return format_btc_amount_as_sats(value)
59+
60+
return format_btc_amount(
61+
value,
62+
maximum_digits_to_strip=maximum_digits_to_strip,
63+
minimum_digits_to_strip=minimum_digits_to_strip,
64+
enable_digit_formatting=enable_digit_formatting,
65+
)
66+
67+
3068
@pass_context
3169
@filters_bp.app_template_filter("btcamount")
3270
def btcamount(context, value):

0 commit comments

Comments
 (0)