Feature: Rescan button for empty wallet (#1779)

* Prompting rescan hint within tx-table when no transactions appear
* Animation of rescan part after being forwarded
* Basics option added in test-cypress for faster development on cypress tests on slower machines
* addWallet command added in Cypress
* Page limit in addresses view fixed

Co-authored-by: moneymanolis <moneymanolis@protonmail.com>
This commit is contained in:
yogendra sankhla 2022-07-19 13:19:32 +05:30 committed by GitHub
parent 51255fb9f7
commit a3ce7d3186
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 279 additions and 125 deletions

View file

@ -6,6 +6,7 @@
"spec_configures_nodes.js", "spec_configures_nodes.js",
"spec_devices.js", "spec_devices.js",
"spec_ghost_machine.js", "spec_ghost_machine.js",
"spec_rescan.js",
"spec_qr_signing.js", "spec_qr_signing.js",
"spec_wallet_send.js", "spec_wallet_send.js",
"spec_wallet_utxo.js", "spec_wallet_utxo.js",

View file

@ -1,15 +1,26 @@
describe('Test adding different devices', () => { describe('Test adding different devices', () => {
before(() => { before(() => {
cy.viewport(1200,660)
cy.visit('/')
Cypress.config('includeShadowDom', true) Cypress.config('includeShadowDom', true)
}) })
// Keeps the session cookie alive, Cypress by default clears all cookies before each test // Keeps the session cookie alive, Cypress by default clears all cookies before each test
beforeEach(() => { beforeEach(() => {
cy.viewport(1200,660)
cy.visit('/')
Cypress.Cookies.preserveOnce('session') Cypress.Cookies.preserveOnce('session')
}) })
it('Filter devices', () => {
cy.get('#toggle_devices_list').click()
cy.get('#btn_new_device').click()
cy.contains('Select Your Device Type')
cy.get('#device-type-searchbar').type("Specter")
cy.get('#trezor_device_card').should('not.be.visible')
cy.get('#specter_device_card').should('be.visible')
cy.get('#specter_device_card').click()
cy.contains('Connect your Specter-DIY')
})
it('Electrum device', () => { it('Electrum device', () => {
cy.get('#toggle_devices_list').click() cy.get('#toggle_devices_list').click()
cy.get('#btn_new_device').click() cy.get('#btn_new_device').click()

View file

@ -1,11 +1,12 @@
describe('Completely empty specter-home', () => { describe('Completely empty specter-home', () => {
beforeEach(() => { before(() => {
cy.task("clear:specter-home") cy.task("clear:specter-home")
}) })
it('Visits specter and clicks around', () => {
it('Click around on the welcome page', () => {
cy.viewport(1200,660) cy.viewport(1200,660)
cy.visit('/') cy.visit('/welcome/about')
cy.contains('Welcome to Specter Desktop') cy.contains('Welcome to Specter Desktop')
cy.get('#node-switch-icon').click() cy.get('#node-switch-icon').click()
cy.get('[href="/nodes/node/default/"]').first().click() cy.get('[href="/nodes/node/default/"]').first().click()
@ -16,28 +17,16 @@ describe('Completely empty specter-home', () => {
cy.contains('Authentication:') cy.contains('Authentication:')
cy.get('[href="/settings/hwi"]').click() cy.get('[href="/settings/hwi"]').click()
cy.contains('Hardware Devices Bridge') cy.contains('Hardware Devices Bridge')
// Hidden in Cypress behind the price cy.get('main').scrollTo('top')
// cy.get('[href="/settings/tor"]').click() cy.contains('Tor').click({ scrollBehavior: false })
// cy.contains('Tor configurations') cy.contains('Tor configurations')
cy.contains("Choose plugins") cy.contains("Choose plugins")
cy.get('#btn_plugins').click() cy.get('#btn_plugins').click()
cy.contains("Plugins in Production") cy.contains("Plugins in Production")
})
it('Creates a device in Specter', () => {
cy.viewport(1200,660)
cy.visit('/')
cy.addDevice("Some Device")
})
it('Dummytest to enforce remove of device', () => {
cy.viewport(1200,660)
cy.visit('/')
}) })
}) })

View file

@ -2,26 +2,7 @@ describe('Ghost machine', () => {
it('Create a DIY device with ghost machine keys and a wallet', () => { it('Create a DIY device with ghost machine keys and a wallet', () => {
cy.viewport(1200,660) cy.viewport(1200,660)
cy.visit('/') cy.visit('/')
// addDevice is creating a DIY device with ghost machine keys cy.addDevice('DIY ghost', 'Specter-DIY', 'ghost_machine')
cy.addDevice("DIY ghost") cy.addWallet('Ghost wallet', null, 'segwit', 'funded', 'btc')
cy.get('body').then(($body) => {
if ($body.text().includes('Wallet ghost')) {
cy.contains('Wallet ghost').click()
cy.get('#btn_settings').click({"force": true})
cy.get('#advanced_settings_tab_btn').click()
cy.get('#delete_wallet').click()
}
})
cy.get('#btn_new_wallet').click()
cy.get('[href="./simple/"]').click()
cy.get('#diy_ghost').click()
cy.get('#wallet_name').type("Wallet ghost")
cy.get('#keysform > .centered').click()
cy.get('body').contains("New wallet was created successfully!")
// Download PDF
// unfortunately this results in weird effects in cypress run
//cy.get('#pdf-wallet-download > img').click()
cy.get('#btn_continue').click()
cy.mine2wallet("btc")
}) })
}) })

View file

@ -11,7 +11,7 @@ describe('Test plugins', () => {
it('Associate an address with a service', () => { it('Associate an address with a service', () => {
// choose address // choose address
cy.selectWallet("Wallet ghost") cy.selectWallet("Ghost wallet")
cy.get('main').contains('Addresses').click() cy.get('main').contains('Addresses').click()
// Click on the first address // Click on the first address
cy.contains('td', '#0').siblings().contains('bcrt').click() cy.contains('td', '#0').siblings().contains('bcrt').click()
@ -24,7 +24,7 @@ describe('Test plugins', () => {
cy.contains("Admin password successfully updated") cy.contains("Admin password successfully updated")
// Choose address again // Choose address again
cy.selectWallet("Wallet ghost") cy.selectWallet("Ghost wallet")
cy.get('main').contains('Addresses').click() cy.get('main').contains('Addresses').click()
cy.contains('td', '#0').siblings().contains('bcrt').click() cy.contains('td', '#0').siblings().contains('bcrt').click()
cy.get('#associate-btn').click() cy.get('#associate-btn').click()

View file

@ -12,7 +12,7 @@ describe('Test QR code signing flow', () => {
}) })
it('Message signing with Specter DIY', () => { it('Message signing with Specter DIY', () => {
cy.get('.side').contains('Wallet ghost').click() cy.get('.side').contains('Ghost wallet').click()
cy.get('main').contains('Addresses').click() cy.get('main').contains('Addresses').click()
// Click on the first address // Click on the first address
cy.contains('td', '#0').siblings().contains('bcrt').click() cy.contains('td', '#0').siblings().contains('bcrt').click()
@ -33,7 +33,7 @@ describe('Test QR code signing flow', () => {
cy.get('#diy_ghost_qr_sign_msg_btn').should('not.exist') cy.get('#diy_ghost_qr_sign_msg_btn').should('not.exist')
cy.get('#diy_ghost_usb_sign_msg_btn').should('exist') cy.get('#diy_ghost_usb_sign_msg_btn').should('exist')
cy.get('#page_overlay_popup_cancel_button').click() cy.get('#page_overlay_popup_cancel_button').click()
cy.contains("Wallet ghost").click() cy.contains("Ghost wallet").click()
cy.get('main').contains('Addresses').click() cy.get('main').contains('Addresses').click()
cy.contains('td', '#0').siblings().contains('bcrt').click() cy.contains('td', '#0').siblings().contains('bcrt').click()
cy.get('#msg-signing-btn').should('exist') cy.get('#msg-signing-btn').should('exist')

View file

@ -0,0 +1,29 @@
describe('Test the UI related to a blockchain rescan', () => {
before(() => {
Cypress.config('includeShadowDom', true)
})
// Keeps the session cookie alive, Cypress by default clears all cookies before each test
beforeEach(() => {
cy.visit('/')
cy.viewport(1200,660)
Cypress.Cookies.preserveOnce('session')
})
it('Go to the rescan section from a fresh wallet', () => {
// Create a completely fresh wallet which is not receiving funds from the continous mining
cy.addDevice('Trezor hold', 'Trezor', 'hold_accident')
cy.addWallet('Fresh wallet', 'Trezor hold', 'segwit', false)
cy.get('#btn_transactions').click()
cy.get('#go-to-rescan-btn').click()
cy.get('#blockchain-rescan').should('be.visible')
// TODO: Do we keep this wallet and this device or teardown?
})
it('Check that there is no button for a used wallet', () => {
// Only works if the ghost wallet was created with funded option
cy.selectWallet('Ghost wallet')
cy.get('#rescan-hint > p').should('not.be.visible')
cy.get('#rescan-hint > .btn').should('not.be.visible')
})
})

View file

@ -98,7 +98,7 @@ describe('Test the actions in UTXO list', () => {
it('Managing unsigned transactions', () => { it('Managing unsigned transactions', () => {
// Make an unsigned tx // Make an unsigned tx
cy.get('#address_0').type("bcrt1qvtdx75y4554ngrq6aff3xdqnvjhmct5wck95qs") // address from "Wallet ghost" cy.get('#address_0').type("bcrt1qvtdx75y4554ngrq6aff3xdqnvjhmct5wck95qs") // address from "Ghost wallet"
cy.get('#send_max_0').click() cy.get('#send_max_0').click()
cy.get('#create_psbt_btn').click() cy.get('#create_psbt_btn').click()
// Check the labeling of the unsigned UTXO // Check the labeling of the unsigned UTXO

View file

@ -26,7 +26,7 @@
import 'cypress-wait-until'; import 'cypress-wait-until';
Cypress.Commands.add("addDevice", (name) => { Cypress.Commands.add("addDevice", (name, device_type, mnemonic) => {
cy.get('body').then(($body) => { cy.get('body').then(($body) => {
if ($body.text().includes(name)) { if ($body.text().includes(name)) {
cy.get('#toggle_devices_list').click() cy.get('#toggle_devices_list').click()
@ -38,25 +38,23 @@ Cypress.Commands.add("addDevice", (name) => {
cy.get('#toggle_devices_list').click() cy.get('#toggle_devices_list').click()
} }
cy.get('#btn_new_device').click() cy.get('#btn_new_device').click()
// Creating a Device cy.contains('Manual configuration').click()
cy.contains('Select Your Device Type')
cy.get('#trezor_device_card')
cy.get('#device-type-searchbar').type("specter")
cy.contains('Select Your Device Type')
cy.get('#trezor_device_card').should('not.have.class', 'disabled')
cy.get('#specter_device_card').click()
cy.get('h2 > input').type(name)
cy.go('back')
cy.get('#device-type-container > .note').click()
cy.get('#device_name').type(name) cy.get('#device_name').type(name)
cy.get('#device_type').select("Specter-DIY") // Device types are the names to select from, such as Trezor, Specter-DIY or Electrum
cy.get('#txt').type("[8c24a510/84h/1h/0h]vpub5Y24kG7ZrCFRkRnHia2sdnt5N7MmsrNry1jMrP8XptMEcZZqkjQA6bc1f52RGiEoJmdy1Vk9Qck9tAL1ohKvuq3oFXe3ADVse6UiTHzuyKx") cy.get('#device_type').select(device_type)
cy.get('#txt').type("\n[8c24a510/49h/1h/0h]upub5DCn7wm4SgVmzmtdoi8DVVfxhBJkqL1L6mmKHNgVky1Fj5VyBxV6NzKD957sr5fWXkY5y8THtqSVWWpjLnomBYw4iXpxaPbkXg5Gn6s5tQf") if (mnemonic === "ghost_machine" || mnemonic === null) {
cy.get('#txt').type("\n[8c24a510/48h/1h/0h/1h]Upub5S2BXfT5rv2bc2i4Hr8NaBzcu243ztEMJ7LUDK4A9UKRtVmr9EFNdNdPz8rAXQnZDeAoHA8KcR7grVjREWKpBr69bev4rkvxytLZ6fN3sUv") cy.get('#txt').type("[8c24a510/84h/1h/0h]vpub5Y24kG7ZrCFRkRnHia2sdnt5N7MmsrNry1jMrP8XptMEcZZqkjQA6bc1f52RGiEoJmdy1Vk9Qck9tAL1ohKvuq3oFXe3ADVse6UiTHzuyKx")
cy.get('#txt').type("\n[8c24a510/48h/1h/0h/2h]Vpub5krSqL811ba5VJdUoP42TFmfRkAaR6h4uxdDThCvDd24PR5gXWPHCvASLbEKevdQQjGx3i1WG7ueEARb8Hpo2u4HikY3wnvwvF1VSakkjew") cy.get('#txt').type("\n[8c24a510/49h/1h/0h]upub5DCn7wm4SgVmzmtdoi8DVVfxhBJkqL1L6mmKHNgVky1Fj5VyBxV6NzKD957sr5fWXkY5y8THtqSVWWpjLnomBYw4iXpxaPbkXg5Gn6s5tQf")
cy.get('#cold_device > [type="submit"]').click() cy.get('#txt').type("\n[8c24a510/48h/1h/0h/1h]Upub5S2BXfT5rv2bc2i4Hr8NaBzcu243ztEMJ7LUDK4A9UKRtVmr9EFNdNdPz8rAXQnZDeAoHA8KcR7grVjREWKpBr69bev4rkvxytLZ6fN3sUv")
cy.get('#toggle_devices_list').click() cy.get('#txt').type("\n[8c24a510/48h/1h/0h/2h]Vpub5krSqL811ba5VJdUoP42TFmfRkAaR6h4uxdDThCvDd24PR5gXWPHCvASLbEKevdQQjGx3i1WG7ueEARb8Hpo2u4HikY3wnvwvF1VSakkjew")
cy.get('#devices_list > .item > div').contains(name) }
if (mnemonic === "hold_accident") {
cy.get('#txt').type("[ccf2e5c3/84h/1h/0h]vpub5YkPJgRQsev79YZM1NRDKJWDjLFcD2xSFAt6LehC5iiMMqQgMHyCFQzwsu16Rx9rBpXZVXPjWAxybuCpsayaw8qCDZtjwH9vifJ7WiQkHwu")
cy.get('#txt').type("\n[ccf2e5c3/49h/1h/0h]upub5DH3pJxqyFKA9Xu8mbKie67UvJ5VWsDDtEg2YR98Yy99UGFNnBa6VSk36zW1ZWTrbYa1Nk6zrxSvzL2hdzjbRUatmwaVUPPYzyEniauECJy")
cy.get('#txt').type("\n[ccf2e5c3/48h/1h/0h/1h]Upub5RyWnpxermQY5L7knm9gDPMQGcFKqat9pDKxhNGoVQHFDubgitAESzyS6QH65ebd7KCs6njXEL1kh1iCweiodWT1xtq69VNx2Cwog97WEDt")
cy.get('#txt').type("\n[ccf2e5c3/48h/1h/0h/2h]Vpub5kon6Vda1Sx1xfPziprUxjBU9PjxGisfosRz3yvWz9odArUVF4embnoB4rEN76CVc5r1UB5JYXxzHTSQS6M1mAob8Wsw6FXk57RubBaizov")
}
cy.contains("Continue").click()
}) })
}) })
@ -151,6 +149,40 @@ Cypress.Commands.add("addHotWallet", (wallet_name, device_name, node_type, walle
}) })
}) })
Cypress.Commands.add("addWallet", (wallet_name, device_name, wallet_type, funded, node_type) => {
if (wallet_type == null) {
wallet_type = "segwit"
}
if (device_name == null) {
device_name = "DIY ghost"
}
cy.get('body').then(($body) => {
if ($body.text().includes(wallet_name)) {
cy.contains(wallet_name).click()
cy.get('#btn_settings' ).click( {force: true})
cy.get('#advanced_settings_tab_btn').click()
cy.get('#delete_wallet').click()
}
cy.get('#side-content').click()
cy.get('#btn_new_wallet').click()
cy.get('[href="./simple/"]').click()
var device_button = "#"+device_name.toLowerCase().replace(/ /g,"_")
cy.get(device_button).click()
cy.get('#wallet_name').type(wallet_name)
if (wallet_type == "nested_segwit") {
cy.get('#type_nested_segwit_btn').click()
}
if (wallet_type == "taproot") {
cy.get('#type_taproot_btn').click()
}
cy.get('#keysform > .centered').click()
cy.get('body').contains("New wallet was created successfully!")
cy.get('#btn_continue').click()
if (funded) {
cy.mine2wallet(node_type)
}
})
})
Cypress.Commands.add("deleteWallet", (name) => { Cypress.Commands.add("deleteWallet", (name) => {
cy.get('body').then(($body) => { cy.get('body').then(($body) => {
if ($body.text().includes(name)) { if ($body.text().includes(name)) {

11
cypress_basics.json Normal file
View file

@ -0,0 +1,11 @@
{
"testFiles": [
"spec_configures_nodes.js",
"spec_ghost_machine.js"
],
"baseUrl": "http://localhost:25444",
"env": {
"broadcast_timeout":"8000"
},
"includeShadowDom": false
}

View file

@ -444,18 +444,18 @@ def miner_loop(node_impl, my_node, data_folder, mining_every_x_seconds, echo):
def mine_2_specter_wallets(node_impl, my_node, data_folder, echo): def mine_2_specter_wallets(node_impl, my_node, data_folder, echo):
"""Get each specter-wallet some coins""" """Sending coins to all Specter wallets, except for "Fresh wallet" which is not supposed to have any tx"""
from ..process_controller.node_controller import fetch_wallet_addresses_for_mining from ..process_controller.node_controller import fetch_wallet_addresses_for_mining
# Using the dict key, not the wallet name
exception = "fresh_wallet"
try: try:
for address in fetch_wallet_addresses_for_mining(
for address in fetch_wallet_addresses_for_mining(node_impl, data_folder): node_impl, data_folder, exception
echo("") ):
echo(f"Mining to address {address}") echo(f"Mining to address {address}")
my_node.testcoin_faucet(address) my_node.testcoin_faucet(address)
# my_node.mine(address=address)
# my_node.mine(block_count=100)
except FileNotFoundError: except FileNotFoundError:
# might happen if there no ~/.specter folder yet # might happen if there is no ~/.specter folder yet
pass pass

View file

@ -555,9 +555,10 @@ def find_node_executable(node_impl):
return which(f"{node_impl}d") return which(f"{node_impl}d")
def fetch_wallet_addresses_for_mining(node_impl, data_folder): def fetch_wallet_addresses_for_mining(node_impl, data_folder, exception=None):
"""parses all the wallet-jsons in the folder (default ~/.specter/wallets/regtest) """
and returns an array with the addresses Parses all the wallet jsons in the folder (default ~/.specter/wallets/regtest) and returns an array with the addresses.
Pass a wallet name via the exception argument so that this wallet's addresses are not included.
""" """
print(f"{data_folder}/wallets") print(f"{data_folder}/wallets")
print(os.listdir(f"{data_folder}")) print(os.listdir(f"{data_folder}"))
@ -569,6 +570,11 @@ def fetch_wallet_addresses_for_mining(node_impl, data_folder):
f"{data_folder}/{folder}/{'regtest' if node_impl == 'bitcoin' else 'elreg'}" f"{data_folder}/{folder}/{'regtest' if node_impl == 'bitcoin' else 'elreg'}"
) )
wallets = load_jsons(wallet_folder) wallets = load_jsons(wallet_folder)
if exception and exception in wallets:
print(
f"Deleting wallet {exception} from wallets that receive faucet coins in testing."
)
del wallets[exception]
address_array = [value["address"] for key, value in wallets.items()] address_array = [value["address"] for key, value in wallets.items()]
# remove duplicates # remove duplicates
address_array = list(dict.fromkeys(address_array)) address_array = list(dict.fromkeys(address_array))

View file

@ -828,6 +828,7 @@ def settings(wallet_alias):
else: else:
app.specter.wallet_manager.rename_wallet(wallet, wallet_name) app.specter.wallet_manager.rename_wallet(wallet, wallet_name)
scroll_to_rescan_blockchain = request.args.get("rescan_blockchain")
return render_template( return render_template(
"wallet/settings/wallet_settings.jinja", "wallet/settings/wallet_settings.jinja",
purposes=purposes, purposes=purposes,
@ -836,4 +837,5 @@ def settings(wallet_alias):
specter=app.specter, specter=app.specter,
rand=rand, rand=rand,
error=error, error=error,
scroll_to_rescan_blockchain=scroll_to_rescan_blockchain,
) )

View file

@ -345,7 +345,7 @@ nav .separator{
background: var(--cmap-border); background: var(--cmap-border);
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 4px; border-radius: 4px;
padding: 8px 15px 8px 10px; padding: 8px 10px 8px 10px;
color: #fff; color: #fff;
font-size: 0.85em; font-size: 0.85em;
text-align: center; text-align: center;
@ -742,7 +742,7 @@ th{
font-weight: inherit; font-weight: inherit;
border-bottom: 3px solid var(--cmap-bg-lightest); border-bottom: 3px solid var(--cmap-bg-lightest);
} }
tbody tr:hover{ tbody tr:not(.nohover):hover{
background: rgba(255,255,255,0.03); background: rgba(255,255,255,0.03);
color: #fff; color: #fff;
} }
@ -832,12 +832,6 @@ table a:hover, .address-link:hover{
color: #fff; color: #fff;
text-decoration: underline; text-decoration: underline;
} }
table .btn{
margin: -10px 0;
padding: 5px 10px;
width: auto;
min-width: 0;
}
table .btn.hovering{ table .btn.hovering{
opacity: 0.2; opacity: 0.2;
} }

View file

@ -109,7 +109,7 @@
<a class="pagination-next pagination-arrow pagination-disabled">&#8250;</a> <a class="pagination-next pagination-arrow pagination-disabled">&#8250;</a>
<br> <br>
</div> </div>
<div> <div style="padding-left: 5px">
{% include "includes/page-limit-select.html" %} {% include "includes/page-limit-select.html" %}
</div> </div>
<div class="export-container hidden"> <div class="export-container hidden">

View file

@ -1,8 +1,5 @@
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
<style> <style>
.page-limit-label {
flex: 1 0 auto;
}
.page-limit-fieldset { .page-limit-fieldset {
border: none; border: none;
margin-top: 4px; /*to better vertically align on tx history / utxo list overview*/ margin-top: 4px; /*to better vertically align on tx history / utxo list overview*/

View file

@ -216,6 +216,35 @@
input:checked + .slider { input:checked + .slider {
background-color: hsl(217, 30%, 50%); background-color: hsl(217, 30%, 50%);
} }
.feedback-row {
display: flex;
justify-content: center;
}
.rescan-hint {
display: flex;
flex-direction: column;
align-items: center;
font-size: 1.1em;
}
.go-to-rescan-btn {
max-width: 175px;
margin: 0 0 5px 0;
border: 1px solid var(--cmap-border);
border-radius: 0.5em;
background-color: var(--cmap-border);
padding: 0.8em;
text-align: center;
font-weight: 250;
}
.go-to-rescan-btn:hover {
background-color: var(--cmap-border);
border: 1px solid var(--main-color);
text-decoration: none;
}
.empty-row {
display: table-row;
text-align: center;
}
:host { :host {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -309,7 +338,7 @@
</div> </div>
<div id="table-container" class="table-container"> <div id="table-container" class="table-container">
<table class="tx-table"> <table class="tx-table">
<thead> <thead id="table-header">
<tr> <tr>
<th value="callbackaction" class="column-callbackaction customaction-header"></th> <th value="callbackaction" class="column-callbackaction customaction-header"></th>
<th value="category" class="column-category category-header"><div class="category-arrow"></div></th> <th value="category" class="column-category category-header"><div class="category-arrow"></div></th>
@ -329,16 +358,16 @@
</th> </th>
</tr> </tr>
</thead> </thead>
<tr class="empty"> <tr id="feedback-row" class="hidden nohover">
<td class="column-callbackaction"></td> <td id="feedback-row-content">
<td class="column-category"></td> <div id="rescan-hint" class="hidden">
<td class="column-txid">{{ _("Fetching transactions...") }}</td> <p>{{_('Is this not a fresh wallet? Did you expect transactions here? Then click') }} &#128071; {{_('to find them') }}</p>
<td class="column-label optional"></td> <a id="go-to-rescan-btn" href="{{ url_for('wallets_endpoint.settings', wallet_alias=wallet_alias, rescan_blockchain=True) }}" class="go-to-rescan-btn btn">Go to blockchain rescan</a>
<td class="column-amount optional"></td> </div>
<td class="column-time" ></td> </td>
<td class="column-confirmations"></td> </tr>
<td class="column-blockhash optional hidden blockhash-empty"></td> <tr id="empty-row" class="hidden nohover">
<td class="column-action"></td> <td id="empty-row-cell" colspan="9"></td>
</tr> </tr>
<tr class="summary-row hidden"> <tr class="summary-row hidden">
<td class="column-callbackaction"></td> <td class="column-callbackaction"></td>
@ -366,11 +395,18 @@
var style = document.getElementById('tx-table').content; var style = document.getElementById('tx-table').content;
var clone = style.cloneNode(true); var clone = style.cloneNode(true);
this.el = clone.querySelector(".tx-table"); this.el = clone.querySelector(".tx-table");
this.tableHeader = clone.getElementById('table-header')
this.tbody = clone.querySelector(".tx-tbody"); this.tbody = clone.querySelector(".tx-tbody");
this.tableContainer = clone.getElementById("table-container") this.tableContainer = clone.getElementById("table-container")
// Empty row element // Feedback row
this.emptyRow = clone.querySelector(".empty"); this.feedbackRow = clone.getElementById('feedback-row');
this.feedbackRowContent = clone.getElementById('feedback-row-content')
this.rescanHint = clone.getElementById('rescan-hint')
// Empty row
this.emptyRow = clone.getElementById('empty-row')
this.emptyRowCell = clone.getElementById('empty-row-cell')
// Blockhash header and empty column // Blockhash header and empty column
this.blockhashHeader = clone.querySelector(".blockhash-header"); this.blockhashHeader = clone.querySelector(".blockhash-header");
@ -699,11 +735,9 @@
if (attrName != "selected-coins") { if (attrName != "selected-coins") {
if (this.getAttribute('blockhash') == 'true' && this.getAttribute('type') == "txlist" && this.getAttribute('wallet')) { if (this.getAttribute('blockhash') == 'true' && this.getAttribute('type') == "txlist" && this.getAttribute('wallet')) {
this.blockhashHeader.classList.remove('hidden'); this.blockhashHeader.classList.remove('hidden');
this.blockhashEmpty.classList.remove('hidden');
this.blockhashSummary.classList.remove('hidden'); this.blockhashSummary.classList.remove('hidden');
} else { } else {
this.blockhashHeader.classList.add('hidden'); this.blockhashHeader.classList.add('hidden');
this.blockhashEmpty.classList.add('hidden');
this.blockhashSummary.classList.add('hidden'); this.blockhashSummary.classList.add('hidden');
} }
if ( if (
@ -808,8 +842,11 @@
} }
break break
default: default:
this.export.href = `` this.export.href = ``;
this.emptyRow.children[1].innerText = '{{ _("Failed to fetch transactions...") }} '; this.tableHeader.classList.add('hidden');
this.feedbackRow.classList.remove('hidden');
this.feedbackRow.classList.add('feedback-row');
this.feedbackRowContent.innerText = '{{ _("Failed to fetch transactions ...") }} ';
return return
} }
@ -828,7 +865,9 @@
formData.append('service_id', this.serviceId); formData.append('service_id', this.serviceId);
} }
formData.append('csrf_token', '{{ csrf_token() }}'); formData.append('csrf_token', '{{ csrf_token() }}');
console.log(url) this.emptyRow.classList.remove('hidden');
this.emptyRow.classList.add('empty-row');
this.emptyRowCell.innerText = '{{ _("Fetching transactions ...") }}';
try { try {
let callId = this.callId; let callId = this.callId;
const response = await fetch( const response = await fetch(
@ -865,13 +904,16 @@
// "txlist": "[{\"txid\": \"1f2b4f9b6762ab37d3b0495cda51d8733c68a5e3a01356f0a67f215a29b2f05e\", \"blockhash\": \"05d7c1f7c4fa2a530e0916dcd15551657c22f4c30df175292b0c0b0f320adc10\", \"blockheight\": 722, \"time\": 1644162928, \"conflicts\": [], \"bip125-replaceable\": \"no\", \"vsize\": null, \"category\": \"receive\", \"address\": \"bcrt1qc3z4j7kksr3wm2heaahzx62yjrj8fn83s7x3ps\", \"amount\": 20.0, \"ismine\": true, \"confirmations\": 132, \"label\": \"Address #7\", \"validated_blockhash\": \"\", \"wallet_alias\": \"myhot_2\"}, {\"txid\": \"aafaecb085944a6fce4556885c1f10c1cd97bac146a2a0e676a2b12bcf428d3b\", \"blockhash\": \"201eea173347cdc8e8effeaf96053b5867860cec163f3eacec7c8918c94bdaaa\", \"blockheight\": 721, \"time\": 1644162928, \"conflicts\": [], \"bip125-replaceable\": \"no\", \"vsize\": null, \"category\": \"receive\", \"address\": \"bcrt1qyncynwdxmeynk245wa3f4urul3nm2v0ulfueek\", \"amount\": 20.0, \"ismine\": true, \"confirmations\": 133, \"label\": \"Address #6\", \"validated_blockhash\": \"\", \"wallet_alias\": \"myhot_2\"}, {\"txid\": \"bf34b090996dfcd2971c88f645edd4a0a0ad180e15096389d8841cf2bac0ecde\", \"blockhash\": \"07952e41edea8a9507722531e922b9d217518d6116ba4fba47c9e62ce12dee9e\", \"blockheight\": 305, \"time\": 1644159720, \"conflicts\": [], \"bip125-replaceable\": \"no\", \"vsize\": null, \"category\": \"receive\", \"address\": \"bcrt1qyncynwdxmeynk245wa3f4urul3nm2v0ulfueek\", \"amount\": 20.0, \"ismine\": true, \"confirmations\": 549, \"label\": \"Address #6\", \"validated_blockhash\": \"\", \"wallet_alias\": \"myhot_2\"}]" // "txlist": "[{\"txid\": \"1f2b4f9b6762ab37d3b0495cda51d8733c68a5e3a01356f0a67f215a29b2f05e\", \"blockhash\": \"05d7c1f7c4fa2a530e0916dcd15551657c22f4c30df175292b0c0b0f320adc10\", \"blockheight\": 722, \"time\": 1644162928, \"conflicts\": [], \"bip125-replaceable\": \"no\", \"vsize\": null, \"category\": \"receive\", \"address\": \"bcrt1qc3z4j7kksr3wm2heaahzx62yjrj8fn83s7x3ps\", \"amount\": 20.0, \"ismine\": true, \"confirmations\": 132, \"label\": \"Address #7\", \"validated_blockhash\": \"\", \"wallet_alias\": \"myhot_2\"}, {\"txid\": \"aafaecb085944a6fce4556885c1f10c1cd97bac146a2a0e676a2b12bcf428d3b\", \"blockhash\": \"201eea173347cdc8e8effeaf96053b5867860cec163f3eacec7c8918c94bdaaa\", \"blockheight\": 721, \"time\": 1644162928, \"conflicts\": [], \"bip125-replaceable\": \"no\", \"vsize\": null, \"category\": \"receive\", \"address\": \"bcrt1qyncynwdxmeynk245wa3f4urul3nm2v0ulfueek\", \"amount\": 20.0, \"ismine\": true, \"confirmations\": 133, \"label\": \"Address #6\", \"validated_blockhash\": \"\", \"wallet_alias\": \"myhot_2\"}, {\"txid\": \"bf34b090996dfcd2971c88f645edd4a0a0ad180e15096389d8841cf2bac0ecde\", \"blockhash\": \"07952e41edea8a9507722531e922b9d217518d6116ba4fba47c9e62ce12dee9e\", \"blockheight\": 305, \"time\": 1644159720, \"conflicts\": [], \"bip125-replaceable\": \"no\", \"vsize\": null, \"category\": \"receive\", \"address\": \"bcrt1qyncynwdxmeynk245wa3f4urul3nm2v0ulfueek\", \"amount\": 20.0, \"ismine\": true, \"confirmations\": 549, \"label\": \"Address #6\", \"validated_blockhash\": \"\", \"wallet_alias\": \"myhot_2\"}]"
// } // }
this.emptyRow.classList.remove('empty-row');
this.emptyRow.classList.add('hidden');
// Clean up from existing transaction list // Clean up from existing transaction list
this.summaryRow.classList.add('hidden'); this.summaryRow.classList.add('hidden');
this.emptyRow.children[1].innerText = '{{ _("Fetching transactions...") }}';
this.emptyRow.classList.remove('hidden');
this.tbody.querySelectorAll('tx-row').forEach((row) => {row.remove()}); this.tbody.querySelectorAll('tx-row').forEach((row) => {row.remove()});
if (!this.listType) { if (!this.listType) {
this.emptyRow.children[1].innerText = '{{ _("Failed to fetch transactions...") }}'; this.tableHeader.classList.add('hidden');
this.feedbackRow.classList.remove('hidden');
this.feedbackRow.classList.add('feedback-row');
this.feedbackRow.innerText = '{{ _("Failed to fetch transactions ...") }}';
return return
} }
@ -880,7 +922,6 @@
} else { } else {
this.utxoViewBtn.classList.add("checked"); this.utxoViewBtn.classList.add("checked");
} }
// Populate the table with TXRowElement objects using the fetched txlist // Populate the table with TXRowElement objects using the fetched txlist
if (jsonResponse.txlist) { if (jsonResponse.txlist) {
let txlist = JSON.parse(jsonResponse.txlist); let txlist = JSON.parse(jsonResponse.txlist);
@ -917,13 +958,19 @@
} }
// Pagination // Pagination
this.pageCount = jsonResponse.pageCount; this.pageCount = jsonResponse.pageCount;
// Show empty row if no transaction found
// Show rescan hint if no transactions are found
if (this.pageCount == 0 || txlist.length == 0) { if (this.pageCount == 0 || txlist.length == 0) {
this.emptyRow.classList.remove('hidden'); this.tableHeader.classList.add('hidden');
this.emptyRow.children[1].innerText = '{{ _("No transactions found...") }}'; this.feedbackRow.classList.add('feedback-row');
this.rescanHint.classList.remove('hidden')
this.rescanHint.classList.add('rescan-hint')
this.pageCount = 1; this.pageCount = 1;
} else { } else {
this.emptyRow.classList.add('hidden'); this.tableHeader.classList.remove('hidden');
this.feedbackRow.classList.add('hidden');
this.rescanHint.classList.remove('rescan-hint')
this.rescanHint.classList.add('hidden');
} }
// If index is greater than the pages count fetch again for the last page // If index is greater than the pages count fetch again for the last page

View file

@ -9,7 +9,7 @@
{% include "includes/tx-data.html" %} {% include "includes/tx-data.html" %}
{% include "includes/explorer-link.html" %} {% include "includes/explorer-link.html" %}
{% include "includes/tx-table.html" %} {% include "includes/tx-table.html" %}
<tx-table <tx-table
{% if specter.price_check and (specter.alt_rate and specter.alt_symbol) %} {% if specter.price_check and (specter.alt_rate and specter.alt_symbol) %}
price="{{ specter.alt_rate }}" price="{{ specter.alt_rate }}"
symbol="{{ specter.alt_symbol }}" symbol="{{ specter.alt_symbol }}"
@ -22,3 +22,5 @@
</tx-table> </tx-table>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -12,6 +12,14 @@
border-top: 1px solid #aaa; border-top: 1px solid #aaa;
margin: 40px 20px; margin: 40px 20px;
} }
#blockchain-rescan {
transition: border-color 3s;
}
.animation-rescan {
padding-top: 5px;
border: 0.5px solid orange;
border-radius: 10px;
}
</style> </style>
<br> <br>
<nav class="row collapse-on-mobile"> <nav class="row collapse-on-mobile">
@ -91,7 +99,7 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if supports_export_to_device != [] %} {% if supports_export_to_device != [] %}
<h2 class="subtitle">{{ _("Export wallet to device") }}</h1> <h2 class="subtitle">{{ _("Export wallet to device") }}</h2>
<p class="center"> <p class="center">
{{ _("Export this wallet to a device by scanning its QR code or importing its data file.") }} {{ _("Export this wallet to a device by scanning its QR code or importing its data file.") }}
</p> </p>
@ -220,7 +228,7 @@
</div> </div>
</form> </form>
<div class="section-separator"></div> <div class="section-separator"></div>
<h2 class="subtitle">{{ _("Blockchain rescan") }}</h2> <h2 class="subtitle" id="blockchain-rescan">{{ _("Blockchain rescan") }}</h2>
<form action="." method="POST" class="padded" style="max-width: 500px"> <form action="." method="POST" class="padded" style="max-width: 500px">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/> <input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
{% if wallet.rescan_progress %} {% if wallet.rescan_progress %}
@ -230,7 +238,7 @@
</div> </div>
{% else %} {% else %}
{{ _("Rescan blockchain from block:") }}<br> {{ _("Rescan blockchain from block:") }}<br>
<div class="row aligned"> <div class="row aligned" style="justify-content: space-between;">
{% if specter.info.chain == "main" %} {% if specter.info.chain == "main" %}
{% if wallet.is_taproot %} {% if wallet.is_taproot %}
{% set startblock=(709632 if not specter.info['pruned'] or specter.info['pruneheight'] < 709632 else specter.info['pruneheight']) %} {% set startblock=(709632 if not specter.info['pruned'] or specter.info['pruneheight'] < 709632 else specter.info['pruneheight']) %}
@ -240,16 +248,16 @@
{% else %} {% else %}
{% set startblock=(0 if not specter.info['pruned'] else specter.info['pruneheight']) %} {% set startblock=(0 if not specter.info['pruned'] else specter.info['pruneheight']) %}
{% endif %} {% endif %}
<input type="number" name="startblock" value="{{startblock}}" style="margin-right: 10px" min="{{startblock}}"> <input type="number" name="startblock" value="{{startblock}}" style="flex-basis: 220px;" min="{{startblock}}">
<button type="submit" name="action" value="rescanblockchain" class="btn">{{ _("Rescan") }}</button> <button id="rescan-btn" type="submit" name="action" value="rescanblockchain" class="btn" style="flex-basis: 170px; flex-grow: 0;">{{ _("Rescan") }}</button>
</div> </div>
{% if specter.info['pruned'] %} {% if specter.info['pruned'] %}
<div class="note center full-width">{{ _("Note: You are using a pruned node. Rescan is limited by the pruned height to block:") }} {{specter.info['pruneheight']}}</div> <div class="note full-width">{{ _("Note: You are using a pruned node. Rescan is limited by the pruned height to block:") }} {{specter.info['pruneheight']}}</div>
{% endif %} {% endif %}
{% if wallet.is_taproot %} {% if wallet.is_taproot %}
<div class="note center"><b>709632</b> {{ _("was the first Taproot block.") }}</div> <div class="note"><b>709632</b> {{ _("was the first Taproot block.") }}</div>
{% else %} {% else %}
<div class="note center"><b>481824</b> {{ _("was the first Segwit block.") }}</div> <div class="note"><b>481824</b> {{ _("was the first Segwit block.") }}</div>
{% endif %} {% endif %}
{% endif %} {% endif %}
</form> </form>
@ -261,10 +269,10 @@
<button type="submit" name="action" value="abortrescanutxo" class="btn" style="max-width: 130px; margin-left: 20px">{{ _("Abort rescan") }}</button> <button type="submit" name="action" value="abortrescanutxo" class="btn" style="max-width: 130px; margin-left: 20px">{{ _("Abort rescan") }}</button>
</div> </div>
{% else %} {% else %}
<div class="row aligned"> <div class="row aligned" style="justify-content: space-between;">
<div style="flex-grow: 1">{{ _("Or rescan only unspent transactions:") }} <div style="flex-grow: 1">{{ _("Or rescan only unspent transactions:") }}
</div> </div>
<button type="submit" name="action" value="rescanutxo" class="btn" style="max-width: 180px; margin-left: 20px"> <button id="rescan-utxo-btn" type="submit" name="action" value="rescanutxo" class="btn" style="flex-grow: 0; max-width: 170px; max-height: 35.5px">
{{ _("Rescan UTXO") }} {{ _("Rescan UTXO") }}
<div class="tool-tip"> <div class="tool-tip">
<i class="tool-tip__icon">i</i> <i class="tool-tip__icon">i</i>
@ -433,6 +441,16 @@
let textarea = document.getElementById("address_labels_data"); let textarea = document.getElementById("address_labels_data");
textarea.value = ""; textarea.value = "";
}) })
function scrolltoRescanBlockchain() {
setSettingsView("advanced");
document.getElementById('blockchain-rescan').scrollIntoView();
}
// Only called when the user comes from an empty tx list to rescan
{% if scroll_to_rescan_blockchain %}
scrolltoRescanBlockchain();
document.getElementById('blockchain-rescan').classList.add("animation-rescan")
{% endif %}
</script> </script>
{% endblock %} {% endblock %}

26
utils/cypress_basics.py Executable file
View file

@ -0,0 +1,26 @@
#!/usr/bin/env python3
"""
Returns a string of a list of basic test files (such as configuring the nodes) on which other more specialised tests depend on.
This string is picked up by a subprocess in test-cypress.sh
"""
import json
import click
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@click.command()
def execute():
with open("cypress_basics.json") as json_file:
data = json.load(json_file)
spec_run_list = []
for file in data["testFiles"]:
spec_run_list.append("./cypress/integration/" + file)
click.echo(",".join(spec_run_list))
if __name__ == "__main__":
execute()

View file

@ -337,6 +337,14 @@ function sub_run {
fi fi
} }
function sub_basics {
start_bitcoind --reset
start_elementsd --reset
start_specter --reset
$(npm bin)/cypress run --spec $(./utils/cypress_basics.py) --config video=false
$(npm bin)/cypress open
}
function sub_snapshot { function sub_snapshot {
spec_file=$1 spec_file=$1
# We'll create a snapshot BEFORE this spec-file has been tested: # We'll create a snapshot BEFORE this spec-file has been tested: