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_devices.js",
"spec_ghost_machine.js",
"spec_rescan.js",
"spec_qr_signing.js",
"spec_wallet_send.js",
"spec_wallet_utxo.js",

View file

@ -1,15 +1,26 @@
describe('Test adding different devices', () => {
before(() => {
cy.viewport(1200,660)
cy.visit('/')
Cypress.config('includeShadowDom', true)
})
// Keeps the session cookie alive, Cypress by default clears all cookies before each test
beforeEach(() => {
cy.viewport(1200,660)
cy.visit('/')
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', () => {
cy.get('#toggle_devices_list').click()
cy.get('#btn_new_device').click()

View file

@ -1,11 +1,12 @@
describe('Completely empty specter-home', () => {
beforeEach(() => {
before(() => {
cy.task("clear:specter-home")
})
it('Visits specter and clicks around', () => {
it('Click around on the welcome page', () => {
cy.viewport(1200,660)
cy.visit('/')
cy.visit('/welcome/about')
cy.contains('Welcome to Specter Desktop')
cy.get('#node-switch-icon').click()
cy.get('[href="/nodes/node/default/"]').first().click()
@ -16,28 +17,16 @@ describe('Completely empty specter-home', () => {
cy.contains('Authentication:')
cy.get('[href="/settings/hwi"]').click()
cy.contains('Hardware Devices Bridge')
// Hidden in Cypress behind the price
// cy.get('[href="/settings/tor"]').click()
// cy.contains('Tor configurations')
cy.get('main').scrollTo('top')
cy.contains('Tor').click({ scrollBehavior: false })
cy.contains('Tor configurations')
cy.contains("Choose plugins")
cy.get('#btn_plugins').click()
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', () => {
cy.viewport(1200,660)
cy.visit('/')
// addDevice is creating a DIY device with ghost machine keys
cy.addDevice("DIY ghost")
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")
cy.addDevice('DIY ghost', 'Specter-DIY', 'ghost_machine')
cy.addWallet('Ghost wallet', null, 'segwit', 'funded', 'btc')
})
})

View file

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

View file

@ -12,7 +12,7 @@ describe('Test QR code signing flow', () => {
})
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()
// Click on the first address
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_usb_sign_msg_btn').should('exist')
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.contains('td', '#0').siblings().contains('bcrt').click()
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', () => {
// 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('#create_psbt_btn').click()
// Check the labeling of the unsigned UTXO

View file

@ -26,7 +26,7 @@
import 'cypress-wait-until';
Cypress.Commands.add("addDevice", (name) => {
Cypress.Commands.add("addDevice", (name, device_type, mnemonic) => {
cy.get('body').then(($body) => {
if ($body.text().includes(name)) {
cy.get('#toggle_devices_list').click()
@ -38,25 +38,23 @@ Cypress.Commands.add("addDevice", (name) => {
cy.get('#toggle_devices_list').click()
}
cy.get('#btn_new_device').click()
// Creating a Device
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.contains('Manual configuration').click()
cy.get('#device_name').type(name)
cy.get('#device_type').select("Specter-DIY")
cy.get('#txt').type("[8c24a510/84h/1h/0h]vpub5Y24kG7ZrCFRkRnHia2sdnt5N7MmsrNry1jMrP8XptMEcZZqkjQA6bc1f52RGiEoJmdy1Vk9Qck9tAL1ohKvuq3oFXe3ADVse6UiTHzuyKx")
cy.get('#txt').type("\n[8c24a510/49h/1h/0h]upub5DCn7wm4SgVmzmtdoi8DVVfxhBJkqL1L6mmKHNgVky1Fj5VyBxV6NzKD957sr5fWXkY5y8THtqSVWWpjLnomBYw4iXpxaPbkXg5Gn6s5tQf")
cy.get('#txt').type("\n[8c24a510/48h/1h/0h/1h]Upub5S2BXfT5rv2bc2i4Hr8NaBzcu243ztEMJ7LUDK4A9UKRtVmr9EFNdNdPz8rAXQnZDeAoHA8KcR7grVjREWKpBr69bev4rkvxytLZ6fN3sUv")
cy.get('#txt').type("\n[8c24a510/48h/1h/0h/2h]Vpub5krSqL811ba5VJdUoP42TFmfRkAaR6h4uxdDThCvDd24PR5gXWPHCvASLbEKevdQQjGx3i1WG7ueEARb8Hpo2u4HikY3wnvwvF1VSakkjew")
cy.get('#cold_device > [type="submit"]').click()
cy.get('#toggle_devices_list').click()
cy.get('#devices_list > .item > div').contains(name)
// Device types are the names to select from, such as Trezor, Specter-DIY or Electrum
cy.get('#device_type').select(device_type)
if (mnemonic === "ghost_machine" || mnemonic === null) {
cy.get('#txt').type("[8c24a510/84h/1h/0h]vpub5Y24kG7ZrCFRkRnHia2sdnt5N7MmsrNry1jMrP8XptMEcZZqkjQA6bc1f52RGiEoJmdy1Vk9Qck9tAL1ohKvuq3oFXe3ADVse6UiTHzuyKx")
cy.get('#txt').type("\n[8c24a510/49h/1h/0h]upub5DCn7wm4SgVmzmtdoi8DVVfxhBJkqL1L6mmKHNgVky1Fj5VyBxV6NzKD957sr5fWXkY5y8THtqSVWWpjLnomBYw4iXpxaPbkXg5Gn6s5tQf")
cy.get('#txt').type("\n[8c24a510/48h/1h/0h/1h]Upub5S2BXfT5rv2bc2i4Hr8NaBzcu243ztEMJ7LUDK4A9UKRtVmr9EFNdNdPz8rAXQnZDeAoHA8KcR7grVjREWKpBr69bev4rkvxytLZ6fN3sUv")
cy.get('#txt').type("\n[8c24a510/48h/1h/0h/2h]Vpub5krSqL811ba5VJdUoP42TFmfRkAaR6h4uxdDThCvDd24PR5gXWPHCvASLbEKevdQQjGx3i1WG7ueEARb8Hpo2u4HikY3wnvwvF1VSakkjew")
}
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) => {
cy.get('body').then(($body) => {
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):
"""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
# Using the dict key, not the wallet name
exception = "fresh_wallet"
try:
for address in fetch_wallet_addresses_for_mining(node_impl, data_folder):
echo("")
for address in fetch_wallet_addresses_for_mining(
node_impl, data_folder, exception
):
echo(f"Mining to address {address}")
my_node.testcoin_faucet(address)
# my_node.mine(address=address)
# my_node.mine(block_count=100)
except FileNotFoundError:
# might happen if there no ~/.specter folder yet
# might happen if there is no ~/.specter folder yet
pass

View file

@ -555,9 +555,10 @@ def find_node_executable(node_impl):
return which(f"{node_impl}d")
def fetch_wallet_addresses_for_mining(node_impl, data_folder):
"""parses all the wallet-jsons in the folder (default ~/.specter/wallets/regtest)
and returns an array with the addresses
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.
Pass a wallet name via the exception argument so that this wallet's addresses are not included.
"""
print(f"{data_folder}/wallets")
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'}"
)
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()]
# remove duplicates
address_array = list(dict.fromkeys(address_array))

View file

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

View file

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

View file

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

View file

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

View file

@ -216,6 +216,35 @@
input:checked + .slider {
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 {
display: flex;
flex-direction: column;
@ -309,7 +338,7 @@
</div>
<div id="table-container" class="table-container">
<table class="tx-table">
<thead>
<thead id="table-header">
<tr>
<th value="callbackaction" class="column-callbackaction customaction-header"></th>
<th value="category" class="column-category category-header"><div class="category-arrow"></div></th>
@ -329,16 +358,16 @@
</th>
</tr>
</thead>
<tr class="empty">
<td class="column-callbackaction"></td>
<td class="column-category"></td>
<td class="column-txid">{{ _("Fetching transactions...") }}</td>
<td class="column-label optional"></td>
<td class="column-amount optional"></td>
<td class="column-time" ></td>
<td class="column-confirmations"></td>
<td class="column-blockhash optional hidden blockhash-empty"></td>
<td class="column-action"></td>
<tr id="feedback-row" class="hidden nohover">
<td id="feedback-row-content">
<div id="rescan-hint" class="hidden">
<p>{{_('Is this not a fresh wallet? Did you expect transactions here? Then click') }} &#128071; {{_('to find them') }}</p>
<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>
</div>
</td>
</tr>
<tr id="empty-row" class="hidden nohover">
<td id="empty-row-cell" colspan="9"></td>
</tr>
<tr class="summary-row hidden">
<td class="column-callbackaction"></td>
@ -366,11 +395,18 @@
var style = document.getElementById('tx-table').content;
var clone = style.cloneNode(true);
this.el = clone.querySelector(".tx-table");
this.tableHeader = clone.getElementById('table-header')
this.tbody = clone.querySelector(".tx-tbody");
this.tableContainer = clone.getElementById("table-container")
// Empty row element
this.emptyRow = clone.querySelector(".empty");
// Feedback row
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
this.blockhashHeader = clone.querySelector(".blockhash-header");
@ -699,11 +735,9 @@
if (attrName != "selected-coins") {
if (this.getAttribute('blockhash') == 'true' && this.getAttribute('type') == "txlist" && this.getAttribute('wallet')) {
this.blockhashHeader.classList.remove('hidden');
this.blockhashEmpty.classList.remove('hidden');
this.blockhashSummary.classList.remove('hidden');
} else {
this.blockhashHeader.classList.add('hidden');
this.blockhashEmpty.classList.add('hidden');
this.blockhashSummary.classList.add('hidden');
}
if (
@ -808,8 +842,11 @@
}
break
default:
this.export.href = ``
this.emptyRow.children[1].innerText = '{{ _("Failed to fetch transactions...") }} ';
this.export.href = ``;
this.tableHeader.classList.add('hidden');
this.feedbackRow.classList.remove('hidden');
this.feedbackRow.classList.add('feedback-row');
this.feedbackRowContent.innerText = '{{ _("Failed to fetch transactions ...") }} ';
return
}
@ -828,7 +865,9 @@
formData.append('service_id', this.serviceId);
}
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 {
let callId = this.callId;
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\"}]"
// }
this.emptyRow.classList.remove('empty-row');
this.emptyRow.classList.add('hidden');
// Clean up from existing transaction list
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()});
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
}
@ -880,7 +922,6 @@
} else {
this.utxoViewBtn.classList.add("checked");
}
// Populate the table with TXRowElement objects using the fetched txlist
if (jsonResponse.txlist) {
let txlist = JSON.parse(jsonResponse.txlist);
@ -917,13 +958,19 @@
}
// Pagination
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) {
this.emptyRow.classList.remove('hidden');
this.emptyRow.children[1].innerText = '{{ _("No transactions found...") }}';
this.tableHeader.classList.add('hidden');
this.feedbackRow.classList.add('feedback-row');
this.rescanHint.classList.remove('hidden')
this.rescanHint.classList.add('rescan-hint')
this.pageCount = 1;
} 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

View file

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

View file

@ -12,6 +12,14 @@
border-top: 1px solid #aaa;
margin: 40px 20px;
}
#blockchain-rescan {
transition: border-color 3s;
}
.animation-rescan {
padding-top: 5px;
border: 0.5px solid orange;
border-radius: 10px;
}
</style>
<br>
<nav class="row collapse-on-mobile">
@ -91,7 +99,7 @@
{% endif %}
{% endfor %}
{% if supports_export_to_device != [] %}
<h2 class="subtitle">{{ _("Export wallet to device") }}</h1>
<h2 class="subtitle">{{ _("Export wallet to device") }}</h2>
<p class="center">
{{ _("Export this wallet to a device by scanning its QR code or importing its data file.") }}
</p>
@ -220,7 +228,7 @@
</div>
</form>
<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">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
{% if wallet.rescan_progress %}
@ -230,7 +238,7 @@
</div>
{% else %}
{{ _("Rescan blockchain from block:") }}<br>
<div class="row aligned">
<div class="row aligned" style="justify-content: space-between;">
{% if specter.info.chain == "main" %}
{% if wallet.is_taproot %}
{% set startblock=(709632 if not specter.info['pruned'] or specter.info['pruneheight'] < 709632 else specter.info['pruneheight']) %}
@ -240,16 +248,16 @@
{% else %}
{% set startblock=(0 if not specter.info['pruned'] else specter.info['pruneheight']) %}
{% endif %}
<input type="number" name="startblock" value="{{startblock}}" style="margin-right: 10px" min="{{startblock}}">
<button type="submit" name="action" value="rescanblockchain" class="btn">{{ _("Rescan") }}</button>
<input type="number" name="startblock" value="{{startblock}}" style="flex-basis: 220px;" min="{{startblock}}">
<button id="rescan-btn" type="submit" name="action" value="rescanblockchain" class="btn" style="flex-basis: 170px; flex-grow: 0;">{{ _("Rescan") }}</button>
</div>
{% 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 %}
{% 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 %}
<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 %}
</form>
@ -261,10 +269,10 @@
<button type="submit" name="action" value="abortrescanutxo" class="btn" style="max-width: 130px; margin-left: 20px">{{ _("Abort rescan") }}</button>
</div>
{% 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>
<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") }}
<div class="tool-tip">
<i class="tool-tip__icon">i</i>
@ -433,6 +441,16 @@
let textarea = document.getElementById("address_labels_data");
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>
{% 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
}
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 {
spec_file=$1
# We'll create a snapshot BEFORE this spec-file has been tested: