mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
UIUX: Overhaul of UTXO list, handling of locked UTXOs and scrollbar added to tx-table web component (#1580)
* Added tests/bitcoin to gitignore * Ensure locked UTXO can't be used for transactions, fix rounding error in coin selection, fix styling/text in general settings. * overhaul of checkbox logic in utxolist including mouseover event for checkboxes, manage psbt button action added, icon info popups added,design changes in utxo- and txlist to make the display quieter * change of balance display, scrollbars for main and tx table, bugfixes (pagination, coin selection, unsigned labeling) & some styling finetuning * deleted locked utxo balance calculations, added doc strings regarding balances * jinja comment added in tx table, making scrollbar less vivid, tx table settings changed to no compact view toggle and scrollbar on by default * more granular amount properties, little test fix. * applying new amount properties to the frontend and other parts of the code & improving balance display design * refactored a_simple_wallet to be only used in test_rest once * tests for amounts and test performance optimisations. * fix spec_node_configured * fix spec_wallet_send.js * fix spec_wallet_utxo.js * keep action teaser div hidden on wallets overview. * minor bug fix in tx-table, added includeShadowDom to cypress.json, adapted spec_wallet_utxo.js to new utxo list logic and added tests of new functionality to it. * fix spec_plugins.js and switch back to includeShadowDom false The problem with true is, that you are no longer able to spot when you need to apply the workaround described in https://docs.cypress.io/api/commands/shadow#Known-Issue as you only need to apply in a shadow-DOM. Feel free to switch it back in any test you see fit. Cypress.config('includeShadowDom',true) * better organisation of wallet utxo spec file * some wait-tweaks to cypress tests to make them run smoother on cirrus, note: cypress tests green in local dev. * longer wait in spec_elm_multi_segwit_wallet.js * fixing spec_elm_multi_segwit_wallet.js * more checks whether checkboxes get selected and additional waits for cirrus in spec_wallet_utxo.js. Co-authored-by: Kim Neunert <k9ert@gmx.de>
This commit is contained in:
parent
06f81c5ebc
commit
b6bd82a90c
28 changed files with 1391 additions and 998 deletions
|
|
@ -14,5 +14,6 @@
|
|||
"baseUrl": "http://localhost:25444",
|
||||
"env": {
|
||||
"broadcast_timeout":"8000"
|
||||
}
|
||||
},
|
||||
"includeShadowDom": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,26 @@
|
|||
describe('Operating with an elements multisig wallet', () => {
|
||||
|
||||
describe('Operating with an Elements multisig wallet', () => {
|
||||
|
||||
it('Creates an two elements multisig hot wallets (segwit/nested) both 2/3', () => {
|
||||
it('Creates two (segwit/nested) Elements multisig (2/3) hot wallets', () => {
|
||||
cy.viewport(1200,660)
|
||||
cy.visit('/')
|
||||
cy.get('#node-switch-icon').click()
|
||||
cy.get('#elements_node-select-node-form > .item > div').click()
|
||||
|
||||
// Delete Wallet if existing
|
||||
// Delete wallets if existing
|
||||
cy.deleteWallet("Elm Multi Segwit Wallet")
|
||||
cy.deleteWallet("Elm Multi Nested Wallet")
|
||||
|
||||
// Create the Hot Element Wallets
|
||||
cy.addHotDevice("Hot Elements Device 2","elements")
|
||||
cy.addHotDevice("Hot Elements Device 3","elements")
|
||||
cy.addHotDevice("Hot Elements Device 4","elements")
|
||||
// Add devices for multisig
|
||||
cy.addHotDevice("Elm Multisig Device 1","elements")
|
||||
cy.addHotDevice("Elm Multisig Device 2","elements")
|
||||
cy.addHotDevice("Elm Multisig Device 3","elements")
|
||||
|
||||
// Create Segwit multisig wallet
|
||||
cy.get('#btn_new_wallet').click()
|
||||
cy.get('[href="./multisig/"]').click()
|
||||
cy.get('#hot_elements_device_2').click()
|
||||
cy.get('#hot_elements_device_3').click()
|
||||
cy.get('#hot_elements_device_4').click()
|
||||
cy.get('#elm_multisig_device_1').click()
|
||||
cy.get('#elm_multisig_device_2').click()
|
||||
cy.get('#elm_multisig_device_3').click()
|
||||
cy.get('#submit-device').click()
|
||||
cy.get('#wallet_name').type("Elm Multi Segwit Wallet")
|
||||
cy.get(':nth-child(9) > .inline').clear()
|
||||
|
|
@ -34,193 +33,156 @@ describe('Operating with an elements multisig wallet', () => {
|
|||
//Get some funds
|
||||
cy.mine2wallet("elm")
|
||||
|
||||
|
||||
// Create Nested multisig wallet
|
||||
cy.get('#btn_new_wallet').click()
|
||||
cy.get('[href="./multisig/"]').click()
|
||||
cy.get('#hot_elements_device_2').click()
|
||||
cy.get('#hot_elements_device_3').click()
|
||||
cy.get('#hot_elements_device_4').click()
|
||||
cy.get('#elm_multisig_device_1').click()
|
||||
cy.get('#elm_multisig_device_2').click()
|
||||
cy.get('#elm_multisig_device_3').click()
|
||||
cy.get('#submit-device').click()
|
||||
cy.get(':nth-child(1) > #type_nested_segwit_btn') // Nested!!
|
||||
// Switch to Nested Segwit
|
||||
cy.get('#type_nested_segwit_btn').click()
|
||||
cy.get('#wallet_name').type("Elm Multi Nested Wallet")
|
||||
// 2 of 3
|
||||
cy.get(':nth-child(9) > .inline').clear()
|
||||
// 2 of 2
|
||||
cy.get(':nth-child(9) > .inline').type("2")
|
||||
// submit
|
||||
cy.get('#keysform > .centered').click()
|
||||
// Cancel-button (no pdf download)
|
||||
// Click cancel (no pdf download)
|
||||
cy.get('#page_overlay_popup_cancel_button').click()
|
||||
//Get some funds
|
||||
// Get some funds
|
||||
cy.mine2wallet("elm")
|
||||
})
|
||||
|
||||
it('Spending to a Confidential address from segwit', () => {
|
||||
it('Spending to a confidential address from segwit', () => {
|
||||
cy.viewport(1200,660)
|
||||
cy.visit('/')
|
||||
cy.contains("Elm Multi Segwit Wallet").click()
|
||||
|
||||
cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") }).then(($div) => {
|
||||
// Create PSBT
|
||||
const oldBalance = parseFloat($div.text())
|
||||
expect(oldBalance).to.be.gte(1.5)
|
||||
cy.createPsbt("el1qqdsywea5scrn7t9q83fd540pw447h0uae30pdp82rzgkl7yzvjz6gra9ls8qu6sslw4s0ck48we06zhqd6kwjy2quh69zwxwn", "Burn address","1.5")
|
||||
|
||||
// First signature
|
||||
cy.get('#elm_multisig_device_1_tx_sign_btn').click()
|
||||
cy.get('#elm_multisig_device_1_hot_sign_btn').click()
|
||||
cy.contains('Sign transaction').click()
|
||||
|
||||
// Second signature
|
||||
cy.get('#elm_multisig_device_2_tx_sign_btn').click()
|
||||
cy.get('#elm_multisig_device_2_hot_sign_btn').click()
|
||||
cy.contains('Sign transaction').click()
|
||||
|
||||
// Send the tx
|
||||
cy.get('#broadcast_local_btn').click()
|
||||
|
||||
// Redirect to "transactions", check balance there
|
||||
cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") })
|
||||
.should(($div) => {
|
||||
const newBalance = parseFloat($div.text())
|
||||
expect(newBalance).to.be.lte(oldBalance - 1.5)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Spending to an unnconfidential address from segwit', () => {
|
||||
cy.viewport(1200,660)
|
||||
cy.visit('/')
|
||||
// spend the money again
|
||||
cy.contains("Elm Multi Segwit Wallet").click()
|
||||
|
||||
cy.get('#fullbalance_amount').then(($div) => {
|
||||
cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") }).then(($div) => {
|
||||
// Create PSBT
|
||||
const oldBalance = parseFloat($div.text())
|
||||
expect(oldBalance).to.be.gte(1.5)
|
||||
cy.createPsbt("ert1q38la37ulxgc0uwt334he46eua7h8qagqnlm5phcqk7ntgv3x73cqjtr2fa", "unconf Burn address","1.5")
|
||||
|
||||
// First signature
|
||||
cy.get('#elm_multisig_device_1_tx_sign_btn').click()
|
||||
cy.get('#elm_multisig_device_1_hot_sign_btn').click()
|
||||
cy.contains('Sign transaction').click()
|
||||
|
||||
// Second signature
|
||||
cy.get('#elm_multisig_device_2_tx_sign_btn').click()
|
||||
cy.get('#elm_multisig_device_2_hot_sign_btn').click()
|
||||
cy.contains('Sign transaction').click()
|
||||
|
||||
// Send tx
|
||||
cy.get('#broadcast_local_btn').click()
|
||||
|
||||
// Redirect to "transactions", check balance there
|
||||
cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") })
|
||||
.should(($div) => {
|
||||
const newBalance = parseFloat($div.text())
|
||||
expect(newBalance).to.be.lte(oldBalance - 1.5)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Spending to a confidential address from nested', () => {
|
||||
cy.viewport(1200,660)
|
||||
cy.visit('/')
|
||||
cy.contains("Elm Multi Nested Wallet").click()
|
||||
|
||||
cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") }).then(($div) => {
|
||||
// Create PSBT
|
||||
const oldBalance = parseFloat($div.text())
|
||||
expect(oldBalance).to.be.gte(1.5)
|
||||
cy.createPsbt("el1qqdsywea5scrn7t9q83fd540pw447h0uae30pdp82rzgkl7yzvjz6gra9ls8qu6sslw4s0ck48we06zhqd6kwjy2quh69zwxwn", "Burn address","1.5")
|
||||
|
||||
// First signature
|
||||
cy.get('#elm_multisig_device_1_tx_sign_btn').click()
|
||||
cy.get('#elm_multisig_device_1_hot_sign_btn').click()
|
||||
cy.contains('Sign transaction').click()
|
||||
|
||||
cy.get('#hot_elements_device_2_tx_sign_btn').click()
|
||||
cy.get('#hot_elements_device_2_hot_sign_btn').click()
|
||||
cy.get('#hot_enter_passphrase__submit').click()
|
||||
cy.get('#hot_elements_device_3_tx_sign_btn').click()
|
||||
cy.get('#hot_elements_device_3_hot_sign_btn').click()
|
||||
cy.get('#hot_enter_passphrase__submit').click()
|
||||
// cy.get('#broadcast_local_btn').click()
|
||||
// There is a bug as the first device doesn't seem to have signed and so we can't send the tx just right from here
|
||||
// 2 more clicks until to get to the send button
|
||||
cy.get('#btn_send').click()
|
||||
cy.get('.row > :nth-child(1) > .btn').click()
|
||||
cy.get('#send_tx_btn').click()
|
||||
// Second signature
|
||||
cy.get('#elm_multisig_device_2_tx_sign_btn').click()
|
||||
cy.get('#elm_multisig_device_2_hot_sign_btn').click()
|
||||
cy.contains('Sign transaction').click()
|
||||
|
||||
// Send the tx
|
||||
cy.get('#broadcast_local_btn').click()
|
||||
// gets redirected to "transactions"
|
||||
|
||||
// Redirect to "transactions", check balance there
|
||||
cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") })
|
||||
.should(($div) => {
|
||||
const newBalance = parseFloat($div.text())
|
||||
expect(newBalance).to.be.lte(oldBalance - 1.5)
|
||||
})
|
||||
})
|
||||
|
||||
// Workaround: Transaction does not disappear
|
||||
cy.get('#btn_send').click()
|
||||
// The "delete" button in the first psbt
|
||||
cy.get('.row > :nth-child(2) > .btn').click()
|
||||
|
||||
|
||||
})
|
||||
|
||||
it('Spending to a Unconfidential address from segwit', () => {
|
||||
|
||||
it('Spending to a unconfidential address from nested', () => {
|
||||
cy.viewport(1200,660)
|
||||
cy.visit('/')
|
||||
// spend the money again
|
||||
cy.contains("Elm Multi Segwit Wallet").click()
|
||||
cy.contains("Elm Multi Nested Wallet").click()
|
||||
|
||||
cy.get('#fullbalance_amount').then(($div) => {
|
||||
cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") }).then(($div) => {
|
||||
// Create PSBT
|
||||
const oldBalance = parseFloat($div.text())
|
||||
expect(oldBalance).to.be.gte(1.5)
|
||||
cy.createPsbt("ert1q38la37ulxgc0uwt334he46eua7h8qagqnlm5phcqk7ntgv3x73cqjtr2fa", "unconf Burn address","1.5")
|
||||
|
||||
// First signature
|
||||
cy.get('#elm_multisig_device_1_tx_sign_btn').click()
|
||||
cy.get('#elm_multisig_device_1_hot_sign_btn').click()
|
||||
cy.contains('Sign transaction').click()
|
||||
|
||||
cy.get('#hot_elements_device_2_tx_sign_btn').click()
|
||||
cy.get('#hot_elements_device_2_hot_sign_btn').click()
|
||||
cy.get('#hot_enter_passphrase__submit').click()
|
||||
cy.get('#hot_elements_device_3_tx_sign_btn').click()
|
||||
cy.get('#hot_elements_device_3_hot_sign_btn').click()
|
||||
cy.get('#hot_enter_passphrase__submit').click()
|
||||
// cy.get('#broadcast_local_btn').click()
|
||||
// There is a bug as the first device doesn't seem to have signed and so we can't send the tx just right from here
|
||||
// 2 more clicks until to get to the send button
|
||||
cy.get('#btn_send').click()
|
||||
cy.get('.row > :nth-child(1) > .btn').click()
|
||||
cy.get('#send_tx_btn').click()
|
||||
// Second signature
|
||||
cy.get('#elm_multisig_device_2_tx_sign_btn').click()
|
||||
cy.get('#elm_multisig_device_2_hot_sign_btn').click()
|
||||
cy.contains('Sign transaction').click()
|
||||
|
||||
// Send the tx
|
||||
cy.get('#broadcast_local_btn').click()
|
||||
// gets redirected to "transactions"
|
||||
|
||||
// Redirect to "transactions", check balance there
|
||||
cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") })
|
||||
.should(($div) => {
|
||||
const newBalance = parseFloat($div.text())
|
||||
expect(newBalance).to.be.lte(oldBalance - 1.5)
|
||||
})
|
||||
})
|
||||
|
||||
// Workaround: Transaction does not disappear
|
||||
// Strange bug, not needed here
|
||||
// cy.get('#btn_send').click()
|
||||
// The "delete" button in the first psbt
|
||||
// cy.get('.row > :nth-child(2) > .btn').click()
|
||||
|
||||
})
|
||||
|
||||
it('Spending to a Confidential address from nested', () => {
|
||||
cy.viewport(1200,660)
|
||||
cy.visit('/')
|
||||
// spend the money again
|
||||
cy.contains("Elm Multi Nested Wallet").click()
|
||||
|
||||
cy.get('#fullbalance_amount').then(($div) => {
|
||||
const oldBalance = parseFloat($div.text())
|
||||
expect(oldBalance).to.be.gte(1.5)
|
||||
cy.createPsbt("el1qqdsywea5scrn7t9q83fd540pw447h0uae30pdp82rzgkl7yzvjz6gra9ls8qu6sslw4s0ck48we06zhqd6kwjy2quh69zwxwn", "Burn address","1.5")
|
||||
|
||||
|
||||
cy.get('#hot_elements_device_2_tx_sign_btn').click()
|
||||
cy.get('#hot_elements_device_2_hot_sign_btn').click()
|
||||
cy.get('#hot_enter_passphrase__submit').click()
|
||||
cy.get('#hot_elements_device_3_tx_sign_btn').click()
|
||||
cy.get('#hot_elements_device_3_hot_sign_btn').click()
|
||||
cy.get('#hot_enter_passphrase__submit').click()
|
||||
// cy.get('#broadcast_local_btn').click()
|
||||
// There is a bug as the first device doesn't seem to have signed and so we can't send the tx just right from here
|
||||
// 2 more clicks until to get to the send button
|
||||
cy.get('#btn_send').click()
|
||||
cy.get('.row > :nth-child(1) > .btn').click()
|
||||
cy.get('#send_tx_btn').click()
|
||||
cy.get('#broadcast_local_btn').click()
|
||||
// gets redirected to "transactions"
|
||||
|
||||
cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") })
|
||||
.should(($div) => {
|
||||
const newBalance = parseFloat($div.text())
|
||||
expect(newBalance).to.be.lte(oldBalance - 1.5)
|
||||
})
|
||||
})
|
||||
|
||||
// Workaround: Transaction does not disappear
|
||||
cy.get('#btn_send').click()
|
||||
// The "delete" button in the first psbt
|
||||
cy.get('.row > :nth-child(2) > .btn').click()
|
||||
|
||||
|
||||
})
|
||||
|
||||
it('Spending to a Unconfidential address from nested', () => {
|
||||
|
||||
cy.viewport(1200,660)
|
||||
cy.visit('/')
|
||||
// spend the money again
|
||||
cy.contains("Elm Multi Nested Wallet").click()
|
||||
|
||||
cy.get('#fullbalance_amount').then(($div) => {
|
||||
const oldBalance = parseFloat($div.text())
|
||||
expect(oldBalance).to.be.gte(1.5)
|
||||
cy.createPsbt("ert1q38la37ulxgc0uwt334he46eua7h8qagqnlm5phcqk7ntgv3x73cqjtr2fa", "unconf Burn address","1.5")
|
||||
|
||||
|
||||
cy.get('#hot_elements_device_2_tx_sign_btn').click()
|
||||
cy.get('#hot_elements_device_2_hot_sign_btn').click()
|
||||
cy.get('#hot_enter_passphrase__submit').click()
|
||||
cy.get('#hot_elements_device_3_tx_sign_btn').click()
|
||||
cy.get('#hot_elements_device_3_hot_sign_btn').click()
|
||||
cy.get('#hot_enter_passphrase__submit').click()
|
||||
// cy.get('#broadcast_local_btn').click()
|
||||
// There is a bug as the first device doesn't seem to have signed and so we can't send the tx just right from here
|
||||
// 2 more clicks until to get to the send button
|
||||
cy.get('#btn_send').click()
|
||||
cy.get('.row > :nth-child(1) > .btn').click()
|
||||
cy.get('#send_tx_btn').click()
|
||||
cy.get('#broadcast_local_btn').click()
|
||||
// gets redirected to "transactions"
|
||||
cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") })
|
||||
.should(($div) => {
|
||||
const newBalance = parseFloat($div.text())
|
||||
expect(newBalance).to.be.lte(oldBalance - 1.5)
|
||||
})
|
||||
})
|
||||
|
||||
// Workaround: Transaction does not disappear
|
||||
// Strange bug, not needed here
|
||||
// cy.get('#btn_send').click()
|
||||
// The "delete" button in the first psbt
|
||||
// cy.get('.row > :nth-child(2) > .btn').click()
|
||||
|
||||
})
|
||||
})
|
||||
|
|
@ -5,10 +5,10 @@ describe('Node Configured', () => {
|
|||
cy.addDevice("Testdevice Ghost")
|
||||
cy.get('body').then(($body) => {
|
||||
if ($body.text().includes('Testwallet Ghost')) {
|
||||
cy.get('#wallets_list > .item > svg').click()
|
||||
cy.get(':nth-child(6) > .right').click()
|
||||
cy.contains('Testwallet Ghost').click()
|
||||
cy.get('#btn_settings').click({"force": true})
|
||||
cy.get('#advanced_settings_tab_btn').click()
|
||||
cy.get('.card > :nth-child(9) > .btn').click()
|
||||
cy.get('#delete_wallet').click()
|
||||
}
|
||||
})
|
||||
cy.get('#btn_new_wallet').click()
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ describe('plugins are working', () => {
|
|||
// choose address
|
||||
cy.get('#test_hot_wallet_1-sidebar-list-item').click()
|
||||
cy.get('[href="/wallets/wallet/test_hot_wallet_1/addresses/"]').click()
|
||||
cy.get('addresses-table').shadow().find('address-row').eq(0).shadow().find('.explorer-link').click()
|
||||
cy.get('addresses-table').shadow().find('address-row').eq(0).shadow().find('.explorer-link').eq(0).click({position: 'top'})
|
||||
cy.get('address-data').shadow().find('#associate-btn').click()
|
||||
|
||||
cy.contains("Service integration requires an authentication method that includes a password")
|
||||
|
|
@ -20,7 +20,7 @@ describe('plugins are working', () => {
|
|||
// choose address again
|
||||
cy.get('#test_hot_wallet_1-sidebar-list-item').click()
|
||||
cy.get('[href="/wallets/wallet/test_hot_wallet_1/addresses/"]').click()
|
||||
cy.get('addresses-table').shadow().find('address-row').eq(0).shadow().find('.explorer-link').click()
|
||||
cy.get('addresses-table').shadow().find('address-row').eq(0).shadow().find('.explorer-link').eq(0).click()
|
||||
cy.get('address-data').shadow().find('#associate-btn').click()
|
||||
cy.contains("Associating an address with a service will")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,82 +1,150 @@
|
|||
describe('Send transactions from wallets', () => {
|
||||
it('Freeze and unfreeze UTXO', () => {
|
||||
const name = "UTXO Hot Bitcoin3"
|
||||
const wallet_name = name+" wallet"
|
||||
describe('Test the actions in UTXO list', () => {
|
||||
|
||||
before(() => {
|
||||
Cypress.config('includeShadowDom', true)
|
||||
const device_name = "UTXO device"
|
||||
const wallet_name = "UTXO wallet"
|
||||
var wallet_name_ref = wallet_name.toLowerCase().replace(/ /g,"_")
|
||||
cy.viewport(1200,660)
|
||||
cy.visit('/')
|
||||
cy.addHotDevice(name+" device","bitcoin")
|
||||
cy.addHotWallet(wallet_name,name+" device", "bitcoin", "segwit")
|
||||
cy.get('#fullbalance_amount').then(($div) => {
|
||||
const balance = parseFloat($div.text())
|
||||
if ( balance <= 20) {
|
||||
cy.get('#toggle_devices_list').click()
|
||||
cy.addHotDevice(device_name, "bitcoin")
|
||||
cy.addHotWallet(wallet_name, device_name, "bitcoin", "segwit")
|
||||
cy.get('#fullbalance_amount').then(($span) => {
|
||||
const balance = parseFloat($span.text())
|
||||
if (balance <= 20) {
|
||||
cy.log("balance " + balance + " too low. Mining!")
|
||||
cy.mine2wallet("btc")
|
||||
cy.mine2wallet("btc")
|
||||
cy.mine2wallet("btc")
|
||||
cy.mine2wallet("btc")
|
||||
// We are having 5 UTXO in the end
|
||||
}
|
||||
})
|
||||
|
||||
cy.contains(wallet_name).click()
|
||||
})
|
||||
|
||||
// The table as component is only available through the shadow tree
|
||||
// That's why we have this stupid .shadow() ...
|
||||
cy.get('tx-table').shadow().find('.utxo-view-btn').click({ force: true })
|
||||
// Keeps the session cookie alive, Cypress by default clears all cookies before each test
|
||||
beforeEach(() => {
|
||||
Cypress.Cookies.preserveOnce('session')
|
||||
})
|
||||
|
||||
cy.log("Check that nothis is frozen")
|
||||
cy.get('tx-table').shadow().find('tx-row').each(($el, index, $list) => {
|
||||
cy.wrap($el).shadow().find('.tx-row').should('not.have.class', 'frozen')
|
||||
cy.wrap($el).shadow().find('.frozen-img').should('have.class', 'hidden')
|
||||
})
|
||||
|
||||
|
||||
cy.log("First select it, then freeze it")
|
||||
cy.get('tx-table').shadow().find('tx-row').eq(0).shadow().find('.select-tx-img').click()
|
||||
it('Freezing', () => {
|
||||
cy.contains("UTXO wallet").click()
|
||||
cy.get('tx-table').find('.utxo-view-btn').click({ force: true })
|
||||
cy.wait(500)
|
||||
// Freeze 3 UTXOs
|
||||
cy.log("Freeze 3 UTXO")
|
||||
cy.get('tx-table').find('tx-row').eq(0).find('.select-tx-img').click( {position: 'top'} )
|
||||
cy.get('tx-table').find('tx-row').eq(0).find('.select-tx-value').invoke('attr', 'value').should('eq', 'true') // Check whether click worked
|
||||
cy.wait(100)
|
||||
// then click the freeze-button
|
||||
cy.get('tx-table').shadow().find('.freeze-tx-btn').click()
|
||||
cy.get('tx-table').find('tx-row').eq(1).find('.select-tx-img').click( {position: 'top'} )
|
||||
cy.get('tx-table').find('tx-row').eq(1).find('.select-tx-value').invoke('attr', 'value').should('eq', 'true')
|
||||
cy.wait(100)
|
||||
|
||||
cy.get('tx-table').shadow().find('tx-row').each(($el, index, $list) => {
|
||||
if (index == 0) {
|
||||
cy.wrap($el).shadow().find('.tx-row').should('have.class', 'frozen')
|
||||
cy.wrap($el).shadow().find('.frozen-img').should('not.have.class', 'hidden')
|
||||
cy.get('tx-table').find('tx-row').eq(3).find('.select-tx-img').click( {position: 'top'} )
|
||||
cy.get('tx-table').find('tx-row').eq(3).find('.select-tx-value').invoke('attr', 'value').should('eq', 'true')
|
||||
cy.wait(100)
|
||||
cy.get('tx-table').find('.freeze-tx-btn').click()
|
||||
cy.wait(500)
|
||||
// We should have 3 frozen outputs now, let's check that
|
||||
cy.log("Check whether we have 3 frozen outputs")
|
||||
cy.get('tx-table').find('tx-row').each(($el, index, $list) => {
|
||||
if (index == 0 || index == 1 || index == 3) {
|
||||
cy.wrap($el).find('.tx-row').should('have.class', 'frozen')
|
||||
cy.wrap($el).find('.frozen-img').should('not.have.class', 'hidden')
|
||||
} else {
|
||||
cy.wrap($el).shadow().find('.tx-row').should('not.have.class', 'frozen')
|
||||
cy.wrap($el).shadow().find('.frozen-img').should('have.class', 'hidden')
|
||||
cy.wrap($el).find('.tx-row').should('not.have.class', 'frozen')
|
||||
cy.wrap($el).find('.frozen-img').should('have.class', 'hidden')
|
||||
}
|
||||
})
|
||||
// Check that we can't create a transaction when a frozen UTXO is selected ...
|
||||
cy.log("Check that we can't create a transaction when frozen UTXO is selected")
|
||||
cy.get('tx-table').find('tx-row').eq(2).find('.select-tx-img').click( {position: 'top'} )
|
||||
cy.get('tx-table').find('tx-row').eq(0).find('.select-tx-img').click( {position: 'top'} )
|
||||
cy.get('tx-table').find('.compose-tx-btn').should('be.hidden')
|
||||
// ... and that the checkboxes disappear for non-frozen UTXO and get unticked
|
||||
cy.log("... and that the checkboxes disappear for non-frozen UTXO and get unticked")
|
||||
cy.get('tx-table').find('tx-row').eq(2).find('.select-tx-img').should('be.hidden')
|
||||
cy.get('tx-table').find('tx-row').eq(2).find('.select-tx-img').should('have.attr', 'src').and('contain', 'untick')
|
||||
cy.get('tx-table').find('tx-row').eq(4).find('.select-tx-img').should('be.hidden')
|
||||
cy.get('tx-table').find('tx-row').eq(4).find('.select-tx-img').should('have.attr', 'src').and('contain', 'untick')
|
||||
// Unselect the first UTXO again and check that all checkboxes are visible and none are selected
|
||||
cy.log("Unselect the first UTXO again and check that all checkboxes are visible and none are selected")
|
||||
cy.get('tx-table').find('tx-row').eq(0).find('.select-tx-img').click( {position: 'top'} )
|
||||
cy.wait(100)
|
||||
cy.get('tx-table').find('tx-row').each(($el, index, $list) => {
|
||||
cy.wrap($el).find('.tx-row').find('.select-tx-img').should('not.be.hidden').should('have.attr', 'src').and('contain', 'untick')
|
||||
})
|
||||
})
|
||||
|
||||
// Test freeze UTXO can't be spend, and unfreeze works for coin selection option
|
||||
cy.log("Select 3 UTXOs and freeze them")
|
||||
cy.get('tx-table').shadow().find('tx-row').eq(0).shadow().find('.select-tx-img').click()
|
||||
cy.get('tx-table').shadow().find('tx-row').eq(1).shadow().find('.select-tx-img').click()
|
||||
cy.get('tx-table').shadow().find('tx-row').eq(3).shadow().find('.select-tx-img').click()
|
||||
cy.get('tx-table').shadow().find('.compose-tx-btn').click()
|
||||
it('Creating a transaction', () => {
|
||||
// Make a transaction with the third and forth UTXO
|
||||
cy.log("Make a transaction with the third and forth UTXO")
|
||||
// Unfreeze necessary for the forth UTXO
|
||||
cy.get('tx-table').find('tx-row').eq(3).find('.select-tx-img').click( {position: 'top'} )
|
||||
cy.wait(100)
|
||||
cy.get('tx-table').find('.freeze-tx-btn').click()
|
||||
cy.wait(200)
|
||||
cy.get('tx-table').find('tx-row').eq(2).find('.select-tx-img').click( {position: 'top'} );
|
||||
cy.get('tx-table').find('tx-row').eq(2).find('.select-tx-value').invoke('attr', 'value').should('eq', 'true') // Check that the click flow is (still) in order
|
||||
cy.get('tx-table').find('tx-row').eq(3).find('.select-tx-img').click( {position: 'top'} )
|
||||
cy.get('tx-table').find('tx-row').eq(3).find('.select-tx-value').invoke('attr', 'value').should('eq', 'true')
|
||||
cy.wait(100)
|
||||
cy.get('tx-table').find('.compose-tx-btn').should('not.be.hidden')
|
||||
cy.get('tx-table').find('.compose-tx-btn').click()
|
||||
// Switch to coin selection, check that the right amount of coins are preselected in the coin selection
|
||||
cy.log("Switch to coin selection, check that the right amount of coins are preselected in the coin selection")
|
||||
cy.get('.coinselect-hidden').should('have.length', 2);
|
||||
})
|
||||
|
||||
// If you select a coin from the utxo-set and cklick on "create transaction", the coins need to be preselected
|
||||
cy.get('.coinselect-hidden').should('have.length', 3);
|
||||
|
||||
// Unfreeze the UTXO
|
||||
it('Managing unsigned transactions', () => {
|
||||
// Make an unsigned tx
|
||||
cy.get('#address_0').type("bcrt1q74tj43hrg550vdtkhsmkrfjqspkku8zes0vq34") // some address from testwallet ghost
|
||||
cy.get('#send_max_0').click()
|
||||
cy.get('#create_psbt_btn').click()
|
||||
// Check the labeling of the unsigned UTXO
|
||||
cy.log("Check the labeling of the unsigned UTXO")
|
||||
cy.get('#btn_transactions').click()
|
||||
cy.wait(1000)
|
||||
cy.get('tx-table').shadow().find('.utxo-view-btn').click()
|
||||
|
||||
cy.get('tx-table').shadow().find('tx-row').eq(0).shadow().find('.select-tx-img').click()
|
||||
cy.get('tx-table').shadow().find('.freeze-tx-btn').click()
|
||||
|
||||
cy.get('tx-table').shadow().find('tx-row').each(($el, index, $list) => {
|
||||
cy.wrap($el).shadow().find('.tx-row').should('not.have.class', 'frozen')
|
||||
cy.wrap($el).shadow().find('.frozen-img').should('have.class', 'hidden')
|
||||
cy.get('tx-table').find('.utxo-view-btn').click()
|
||||
cy.get('tx-table').find('tx-row').eq(2).find('#column-category').should('contain', 'Unsigned')
|
||||
cy.get('tx-table').find('tx-row').eq(3).find('#column-category').should('contain', 'Unsigned')
|
||||
// Check that only the two checkboxes of the unsigned UTXO are visible
|
||||
cy.log("Check that only the two checkboxes of the unsigned UTXO are visible")
|
||||
cy.get('tx-table').find('tx-row').eq(2).find('.select-tx-img').click( {position: 'top'} )
|
||||
cy.wait(200)
|
||||
cy.get('tx-table').find('tx-row').each(($el, index, $list) => {
|
||||
if (index == 0 || index == 1) {
|
||||
cy.wrap($el).find('.tx-row').find('.select-tx-img').should('be.hidden').should('have.attr', 'src').and('contain', 'untick')
|
||||
}
|
||||
})
|
||||
|
||||
// Ensure selection of unfreeze now works
|
||||
cy.get('tx-table').shadow().find('tx-row').eq(0).shadow().find('.select-tx-img').click()
|
||||
cy.get('tx-table').shadow().find('tx-row').eq(1).shadow().find('.select-tx-img').click()
|
||||
cy.get('tx-table').shadow().find('tx-row').eq(3).shadow().find('.select-tx-img').click()
|
||||
cy.get('tx-table').shadow().find('.compose-tx-btn').click()
|
||||
|
||||
cy.get('.coinselect-hidden').should('have.length', 3);
|
||||
|
||||
// Unselect the third UTXO again and check that all checkboxes are visible and none are selected
|
||||
cy.log("Unselect the third UTXO again and check that all checkboxes are visible and none are selected")
|
||||
cy.get('tx-table').find('tx-row').eq(2).find('.select-tx-img').click( {position: 'top'} )
|
||||
cy.get('tx-table').find('tx-row').eq(2).find('.select-tx-value').invoke('attr', 'value').should('eq', '')
|
||||
cy.wait(100)
|
||||
cy.get('tx-table').find('tx-row').each(($el, index, $list) => {
|
||||
cy.wrap($el).find('.tx-row').find('.select-tx-img').should('not.be.hidden')
|
||||
})
|
||||
// Select a normal UTXO, select an unsigned UTXO and then unselect the unsigned again
|
||||
cy.log("Select a normal UTXO, select an unsigned UTXO and then unselect the unsigned again")
|
||||
cy.get('tx-table').find('tx-row').eq(4).find('.select-tx-img').click( {position: 'top'} )
|
||||
cy.get('tx-table').find('tx-row').eq(4).find('.select-tx-value').invoke('attr', 'value').should('eq', 'true')
|
||||
cy.wait(100)
|
||||
cy.get('tx-table').find('tx-row').eq(3).find('.select-tx-img').click( {position: 'top'} )
|
||||
cy.get('tx-table').find('tx-row').eq(3).find('.select-tx-value').invoke('attr', 'value').should('eq', 'true')
|
||||
cy.wait(100)
|
||||
cy.get('tx-table').find('tx-row').eq(3).find('.select-tx-img').click( {position: 'top'} )
|
||||
cy.get('tx-table').find('tx-row').eq(3).find('.select-tx-value').invoke('attr', 'value').should('eq', '')
|
||||
cy.wait(100)
|
||||
// Check that all checkboxes are visible again and none are selected
|
||||
cy.get('tx-table').find('tx-row').each(($el, index, $list) => {
|
||||
cy.wrap($el).find('.tx-row').find('.select-tx-img').should('not.be.hidden').should('have.attr', 'src').and('contain', 'untick')
|
||||
})
|
||||
// Check that the manage PSBT button is working
|
||||
cy.get('tx-table').find('tx-row').eq(2).find('.select-tx-img').click( {position: 'top'} )
|
||||
cy.wait(100)
|
||||
cy.get('tx-table').find('#manage-psbt-btn').should('not.be.hidden').click()
|
||||
cy.wait(200)
|
||||
cy.contains("Here you can manage PSBTs")
|
||||
})
|
||||
})
|
||||
|
|
@ -48,6 +48,18 @@ module.exports = (on, config) => {
|
|||
}
|
||||
})
|
||||
|
||||
// For future use, not used yet
|
||||
on('task', {
|
||||
'delete:bitcoin-hotwallet': (name) => {
|
||||
console.log('connection details: %s', btc_conn)
|
||||
const bitcoin_data_dir=btc_conn["bitcoin_data_dir"];
|
||||
var rimraf = require("rimraf");
|
||||
console.log('Removing all wallets in %s', bitcoin_data_dir+"/regtest/wallets/specter123456_hotstorage")
|
||||
rimraf.sync(bitcoin_data_dir+"/regtest/wallets/specter123456_hotstorage");
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
on('task', {
|
||||
'btc:mine': () => {
|
||||
// sending the bitcoind-process a signal SIGUSR1 (10) will cause mining towards all specter-wallets
|
||||
|
|
|
|||
|
|
@ -63,10 +63,11 @@ Cypress.Commands.add("addDevice", (name) => {
|
|||
Cypress.Commands.add("addHotDevice", (name, node_type) => {
|
||||
// node_type is either elements or bitcoin
|
||||
cy.get('body').then(($body) => {
|
||||
//cy.task("delete:elements-hotwallet")
|
||||
cy.task("delete:elements-hotwallet")
|
||||
if ($body.text().includes(name)) {
|
||||
cy.get('#toggle_devices_list').click()
|
||||
var refName = "#device_list_item_"+name.toLowerCase().replace(/ /g,"_")
|
||||
cy.get(refName).click()
|
||||
cy.get(refName).click( {force: true} )
|
||||
cy.get('#forget_device').click()
|
||||
// We might get an error here, if the device is used in a wallet
|
||||
// We assume therefore that this is ok (see below)
|
||||
|
|
@ -75,7 +76,7 @@ Cypress.Commands.add("addHotDevice", (name, node_type) => {
|
|||
if (!cy.get('#btn_new_device').isVisible) {
|
||||
cy.get('#toggle_devices_list').click()
|
||||
}
|
||||
cy.get('#btn_new_device').click()
|
||||
cy.get('#btn_new_device').click( {force: true} )
|
||||
cy.contains('Select Your Device Type')
|
||||
cy.get(`#${node_type}core_device_card`).click()
|
||||
cy.get('#submit-mnemonic').click()
|
||||
|
|
@ -102,9 +103,7 @@ Cypress.Commands.add("addHotWallet", (wallet_name, device_name, node_type, walle
|
|||
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,"_")
|
||||
|
|
@ -131,7 +130,7 @@ Cypress.Commands.add("deleteWallet", (name) => {
|
|||
cy.get('body').then(($body) => {
|
||||
if ($body.text().includes(name)) {
|
||||
cy.contains(name).click()
|
||||
cy.get('#btn_settings').click()
|
||||
cy.get('#btn_settings').click( {force: true} )
|
||||
cy.get('#advanced_settings_tab_btn').click()
|
||||
cy.get('#delete_wallet').click()
|
||||
// That does not seem to delete the wallet-file in elements, though
|
||||
|
|
@ -144,8 +143,8 @@ Cypress.Commands.add("deleteWallet", (name) => {
|
|||
Cypress.Commands.add("mine2wallet", (chain) => {
|
||||
// Fund it and check the balance
|
||||
cy.get('#btn_transactions').click()
|
||||
cy.get('#fullbalance_amount').then(($div) => {
|
||||
const oldBalance = parseFloat($div.text())
|
||||
cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") }).then(($span) => {
|
||||
const oldBalance = parseFloat($span.text())
|
||||
if (chain=="elm" || chain=="elements") {
|
||||
cy.task("elm:mine")
|
||||
} else if (chain=="btc" || chain=="bitcoin") {
|
||||
|
|
@ -154,8 +153,8 @@ Cypress.Commands.add("mine2wallet", (chain) => {
|
|||
throw new Error("Unknown chain: " + chain)
|
||||
}
|
||||
cy.waitUntil( () => cy.reload().get('#fullbalance_amount', { timeout: 3000 })
|
||||
.then(($div) => {
|
||||
const n = parseFloat($div.text())
|
||||
.then(($span) => {
|
||||
const n = parseFloat($span.text())
|
||||
return n > oldBalance
|
||||
})
|
||||
, {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ def timedatetime(context, s):
|
|||
def btcamount(context, value):
|
||||
if value is None:
|
||||
return "Unknown"
|
||||
if value < 0:
|
||||
if value < 0 and app.specter.is_liquid:
|
||||
return "Confidential"
|
||||
value = round(float(value), 8)
|
||||
return "{:,.8f}".format(value).rstrip("0").rstrip(".")
|
||||
|
|
@ -63,7 +63,7 @@ def btcunitamount(context, value):
|
|||
return "#########"
|
||||
if value is None:
|
||||
return "Unknown"
|
||||
if value < 0:
|
||||
if value < 0 and app.specter.is_liquid:
|
||||
return "Confidential"
|
||||
if app.specter.unit != "sat":
|
||||
return btcamount(context, value)
|
||||
|
|
|
|||
|
|
@ -361,7 +361,7 @@ def new_wallet(wallet_type):
|
|||
@login_required
|
||||
def wallet(wallet_alias):
|
||||
wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
|
||||
if wallet.fullbalance > 0:
|
||||
if wallet.amount_total > 0:
|
||||
return redirect(url_for("wallets_endpoint.history", wallet_alias=wallet_alias))
|
||||
else:
|
||||
return redirect(url_for("wallets_endpoint.receive", wallet_alias=wallet_alias))
|
||||
|
|
@ -468,6 +468,15 @@ def send_new(wallet_alias):
|
|||
rbf_utxo = []
|
||||
rbf_tx_id = ""
|
||||
selected_coins = request.form.getlist("coinselect")
|
||||
# Additional server side check not to use frozen UTXO as a precaution
|
||||
frozen_utxo = wallet.frozen_utxo
|
||||
for utxo in selected_coins:
|
||||
if utxo in frozen_utxo:
|
||||
selected_coins.remove(utxo)
|
||||
flash(f"You've selected a frozen UTXO for a transaction.", "error")
|
||||
return redirect(
|
||||
url_for("wallets_endpoint.history", wallet_alias=wallet_alias)
|
||||
)
|
||||
|
||||
if request.method == "POST":
|
||||
action = request.form.get("action")
|
||||
|
|
@ -601,11 +610,7 @@ def send_new(wallet_alias):
|
|||
flash(_("Failed to get RBF coins. Error: {}").format(e), "error")
|
||||
|
||||
show_advanced_settings = (
|
||||
ui_option != "ui"
|
||||
or subtract
|
||||
or fee_options != "dynamic"
|
||||
or not rbf
|
||||
or selected_coins
|
||||
ui_option != "ui" or subtract or fee_options != "dynamic" or not rbf
|
||||
)
|
||||
wallet_utxo = wallet.utxo
|
||||
if app.specter.is_liquid:
|
||||
|
|
|
|||
|
|
@ -439,6 +439,14 @@ def wallets_overview_utxo_list():
|
|||
)
|
||||
|
||||
|
||||
@wallets_endpoint_api.route("/wallet/<wallet_alias>/pending_psbt_list", methods=["GET"])
|
||||
@login_required
|
||||
def pending_psbt_list(wallet_alias):
|
||||
wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
|
||||
pending_psbts = wallet.pending_psbts_dict()
|
||||
return jsonify(pending_psbts=pending_psbts)
|
||||
|
||||
|
||||
@wallets_endpoint_api.route("/wallet/<wallet_alias>/addresses_list/", methods=["POST"])
|
||||
@login_required
|
||||
@app.csrf.exempt
|
||||
|
|
@ -676,11 +684,11 @@ def asset_balances(wallet_alias):
|
|||
textUnit = app.specter.unit
|
||||
asset_balances = {
|
||||
"btc": {
|
||||
"balance": wallet.full_available_balance,
|
||||
"balance": wallet.amount_available,
|
||||
"label": label,
|
||||
},
|
||||
"sat": {
|
||||
"balance": int(wallet.full_available_balance * 1e8),
|
||||
"balance": int(wallet.amount_available * 1e8),
|
||||
"label": "sat",
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,16 +141,6 @@ ul, li{
|
|||
/*max-width: 100%;*/
|
||||
justify-content: center;
|
||||
}
|
||||
tr.unconfirmed{
|
||||
background: var(--cmap-bg);
|
||||
/*font-style: italic;*/
|
||||
}
|
||||
.unconfirmed td img{
|
||||
opacity: 0.7;
|
||||
}
|
||||
.unconfirmed:hover td img{
|
||||
opacity: 1;
|
||||
}
|
||||
.row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
@ -215,7 +205,6 @@ main{
|
|||
/*justify-content: center;*/
|
||||
padding: 0;
|
||||
/*padding: 30px 0 20px 0;*/
|
||||
overflow-y: scroll;
|
||||
}
|
||||
nav.side > .item, nav.side > div > .item{
|
||||
padding: 10px 0;
|
||||
|
|
@ -520,6 +509,24 @@ input:hover{
|
|||
.flex-item{
|
||||
margin: 0.5em;
|
||||
}
|
||||
.smaller-card{
|
||||
min-width: 220px;
|
||||
padding: 15px;
|
||||
background: var(--cmap-bg-lighter);
|
||||
border: 2px solid var(--cmap-border);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-size: 0.9em;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
margin: 5px;
|
||||
}
|
||||
.smaller-card:hover{
|
||||
border: 2px solid hsl(217, 15%, 55%);
|
||||
}
|
||||
.small-card{
|
||||
min-width: 220px;
|
||||
/*height: 250px;*/
|
||||
|
|
@ -707,8 +714,6 @@ table{
|
|||
.table-holder{
|
||||
padding: 0 30px;
|
||||
width: 100%;
|
||||
overflow-y: scroll;
|
||||
/*overflow-x: scroll;*/
|
||||
}
|
||||
.full-width{
|
||||
width: 100%;
|
||||
|
|
@ -741,29 +746,74 @@ tbody tr:hover{
|
|||
.tx{
|
||||
max-width: 200px;
|
||||
}
|
||||
.scroll{
|
||||
|
||||
/* Scroll functionality along x axis
|
||||
(mainly used for xpub keys)
|
||||
*/
|
||||
|
||||
.scroll {
|
||||
padding: 0 10px;
|
||||
}
|
||||
.scroll > *{
|
||||
.scroll > * {
|
||||
/* ugly fix for firefox - scrolling child element */
|
||||
display: block;
|
||||
overflow-x: scroll;
|
||||
width: 100%;
|
||||
padding: 20px 0;
|
||||
}
|
||||
.scroll > *::-webkit-scrollbar,
|
||||
#side-content::-webkit-scrollbar,
|
||||
main::-webkit-scrollbar,
|
||||
pre::-webkit-scrollbar{
|
||||
display: none;
|
||||
}
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
td.scroll > *,
|
||||
#side-content,
|
||||
main,pre{
|
||||
/* Hide scrollbar thumb and track for IE, Edge and Firefox */
|
||||
td.scroll > *, pre {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
scrollbar-height: none; /* Firefox */
|
||||
}
|
||||
/* Hide scrollbar thumb and track for Chrome */
|
||||
td.scroll > *::-webkit-scrollbar,
|
||||
pre::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Standard scrollbar along y axis
|
||||
scrollbar-color and scrollbar-width only supported by Firefox */
|
||||
|
||||
main {
|
||||
overflow-y: scroll;
|
||||
scrollbar-color: hsl(217, 10%, 80%) var(--cmap-border);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
/* Not supported by Firefox & Safari on iOS */
|
||||
main::-webkit-scrollbar {
|
||||
width: 0.5em;
|
||||
}
|
||||
main::-webkit-scrollbar:horizontal {
|
||||
display: none;
|
||||
}
|
||||
main::-webkit-scrollbar-track {
|
||||
background: var(--cmap-bg);
|
||||
border-radius: 100vw;
|
||||
margin-top: 3.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
main::-webkit-scrollbar-track:active {
|
||||
background: hsl(217, 15%, 55%);
|
||||
border-radius: 100vw;
|
||||
margin-top: 3.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
main::-webkit-scrollbar-thumb {
|
||||
background: hsl(217, 10%, 80%);
|
||||
border-radius: 100vw;
|
||||
}
|
||||
main::-webkit-scrollbar-thumb:hover {
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* No scrollbar for side-content */
|
||||
#side-content::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#side-content {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
table a, .address-link{
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@
|
|||
{% endblock %}
|
||||
<main><div><br><br></div>
|
||||
{% if current_user.is_authenticated and not hwi_bridge %}
|
||||
<div class="row" id="status-bar" style="{% if session['is_language_rtl'] %}border-radius: 0 0 15px 0; left{% else %}border-radius: 0 0 0 15px; right{% endif %}: 0; position: absolute; color: #ddd; background-color: #323e50;">
|
||||
<div class="row" id="status-bar" style="{% if session['is_language_rtl'] %}border-radius: 0 0 15px 0; left{% else %}border-radius: 0 0 15px 15px; right{% endif %}: 1%; position: absolute; color: #ddd; background-color: #323e50;">
|
||||
{% include "components/price_bar.jinja" %}
|
||||
<a class="settings-bar-btn" onclick="toggleHideSensitiveInfo()" style="padding: 0px 12px; cursor: pointer;" title="{{ 'Hide sensitive info' if not specter.hide_sensitive_info else 'Show sensitive info' }}">
|
||||
<img src="{{ url_for('static', filename='img/hidden.svg' if specter.hide_sensitive_info else 'img/visible.svg') }}" style="width: 30px;" class="svg-white"/>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@
|
|||
btc-unit="{{ specter.unit }}"
|
||||
hide-sensitive-info="{{ specter.hide_sensitive_info | lower }}"
|
||||
type="utxo" hide-columns="category time txid confirmations action"
|
||||
blockhash="true" hide-switcher="true">
|
||||
blockhash="true" hide-switcher="true"
|
||||
wallet="{{ wallet.alias }}"
|
||||
scrollbar-height="400px"
|
||||
scrollbar-default="on"
|
||||
compact-switch="false">
|
||||
</tx-table>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -26,11 +30,11 @@
|
|||
<coin-selection
|
||||
id="coinselection-webcomponent"
|
||||
wallet-alias="{{wallet.alias}}"
|
||||
spendable-amount='{{ wallet.full_available_balance | btcamount }}'
|
||||
spendable-amount='{{ wallet.amount_available | btcamount }}'
|
||||
is-liquid="{{specter.is_liquid}}"
|
||||
selected-coins="{{ selected_coins }}">
|
||||
</coin-selection>
|
||||
Internally, a tx-table is used which is customizable in order to be usefull here.
|
||||
Internally, a tx-table is used which is customizable in order to be useful here.
|
||||
We're using the asset_balance endpoint to get Liquid assets.
|
||||
*/
|
||||
class CoinSelection extends HTMLElement {
|
||||
|
|
@ -71,16 +75,12 @@
|
|||
if (this.getAttribute("is-liquid") == "True") {
|
||||
this.classList.add("hidden")
|
||||
}
|
||||
|
||||
this.txtable.setAttribute("wallet", this.walletAlias)
|
||||
|
||||
let selectedCoins = JSON.parse(this.getAttribute('selected-coins').replace(/'/g, '"'));
|
||||
if (selectedCoins.length > 0) {
|
||||
this.txtable.setAttribute("selected-coins", JSON.stringify(selectedCoins))
|
||||
// unfold if we have preselected coins
|
||||
this.toggleCoinselection()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -117,7 +117,7 @@
|
|||
let spendableAmount;
|
||||
if (!this.isCoinSelectionActive()) {
|
||||
if(unit == 'btc' || unit == 'sat'){
|
||||
spendableAmount = '{{ wallet.full_available_balance }}';
|
||||
spendableAmount = '{{ wallet.amount_available }}';
|
||||
} else {
|
||||
if (unit in assetBalances){
|
||||
return assetBalances[unit].balance;
|
||||
|
|
@ -155,6 +155,7 @@
|
|||
this.txtable.getSelectedTxs().forEach( (tx) => {
|
||||
sum = sum + tx.amount
|
||||
})
|
||||
sum = Number.parseFloat(sum.toFixed(8))
|
||||
return sum
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
.page-limit-fieldset {
|
||||
float: left;
|
||||
border: none;
|
||||
margin-top: 4px; /*to better vertically align on tx history / utxo list overview*/
|
||||
display: inline;
|
||||
}
|
||||
.page-limit-fieldset > select {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
<template id="tx-row">
|
||||
<style>
|
||||
.frozen {
|
||||
background: #546773;
|
||||
}
|
||||
.svg-send {
|
||||
filter: invert(63%) sepia(67%) saturate(851%) hue-rotate(346deg) brightness(107%) contrast(92%);
|
||||
}
|
||||
|
|
@ -15,10 +12,21 @@
|
|||
.svg-cancelled {
|
||||
filter: invert(20%) sepia(32%) saturate(4838%) hue-rotate(332deg) brightness(81%) contrast(97%);
|
||||
}
|
||||
|
||||
.tx-row:hover {
|
||||
background-color: var(--cmap-bg-lightest);
|
||||
vertical-align: middle;
|
||||
}
|
||||
.tx-row {
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* .frozen {
|
||||
background: #48535d
|
||||
#546773;
|
||||
}
|
||||
.frozen:hover {
|
||||
background: #48535d
|
||||
#546773;
|
||||
} */
|
||||
.address {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
|
@ -29,14 +37,64 @@
|
|||
height:24px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.coin_select_checkbox {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.icon-info {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
.icon-info .icon-info-text {
|
||||
display: none;
|
||||
background: #262626;
|
||||
border: 1px solid grey;
|
||||
border-radius: 5px;
|
||||
padding: 0.5em;
|
||||
position: absolute;
|
||||
left: 30px;
|
||||
top: -20px;
|
||||
width: 100px;
|
||||
z-index: 2;
|
||||
}
|
||||
.icon-info .icon-info-text:before, .icon-info .icon-info-text:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -10px;
|
||||
top: 7px;
|
||||
border-style: solid;
|
||||
border-width: 10px 10px 10px 0;
|
||||
border-color: transparent grey;
|
||||
}
|
||||
.icon-info .icon-info-text:after {
|
||||
left: -8px;
|
||||
border-right-color: #262626;
|
||||
}
|
||||
.icon-info .icon-info-text .info-2 {
|
||||
display: block;
|
||||
}
|
||||
.icon-info-text {
|
||||
text-transform: capitalize;
|
||||
text-align: center;
|
||||
}
|
||||
.icon-info:hover .icon-info-text, .icon-info:focus .icon-info-text {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}">
|
||||
|
||||
<tr class="tx-row">
|
||||
<td id="column-callbackaction" class="callbackaction">
|
||||
<input class="checkbox coin_select_checkbox" type="checkbox" name="coinselect" value="" >
|
||||
<input id="coin-select-checkbox" class="checkbox coin_select_checkbox" type="checkbox" name="coinselect" value="">
|
||||
</td>
|
||||
<td id="column-category">
|
||||
<img class="category" style="vertical-align: middle; margin-right: 10px;"/>
|
||||
<div class="icon-info">
|
||||
<img class="category" style="vertical-align: middle; margin-right: 10px;"/>
|
||||
<p id="icon-info-text" class="icon-info-text">
|
||||
</p>
|
||||
</div>
|
||||
<img class="frozen-img hidden svg-white" style="vertical-align: middle; height: 28px;" src="{{ url_for('static', filename='img/snowflake.svg') }}"/>
|
||||
</td>
|
||||
<td id="column-txid" class="tx scroll optional txid">
|
||||
|
|
@ -54,7 +112,10 @@
|
|||
<button class="rbf-cancel danger btn optional hidden" style="width: 130px; float: right; margin-right: 10px;" type="button">{{ _("Cancel transaction") }}</button>
|
||||
</td>
|
||||
<td class="hidden optional blockhash"></td>
|
||||
<td id="column-action"><input class="select-tx-value" type="hidden" value=""><img style="vertical-align: middle;" class="select-tx-img" src="{{ url_for('static', filename='img/checkbox-untick.svg') }}" width="25px"></tool-tip></td>
|
||||
<td id="column-action" class="highlight-box">
|
||||
<input class="select-tx-value" type="hidden" value="">
|
||||
<img style="vertical-align: middle;" class="select-tx-img" src="{{ url_for('static', filename='img/checkbox-untick.svg') }}" width="25px">
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
|
|
@ -69,7 +130,10 @@
|
|||
this.el = clone.querySelector(".tx-row");
|
||||
|
||||
this.callbackaction = clone.querySelector(".callbackaction");
|
||||
this.coinSelectCheckbox = clone.getElementById("coin-select-checkbox");
|
||||
this.categoryCell = clone.getElementById("column-category");
|
||||
this.category = clone.querySelector(".category");
|
||||
this.iconInfoText = clone.getElementById("icon-info-text");
|
||||
this.txid = clone.querySelector(".txid .explorer-link");
|
||||
this.address = clone.querySelector(".address");
|
||||
this.serviceIcon = clone.querySelector(".service-icon");
|
||||
|
|
@ -97,15 +161,15 @@
|
|||
this.symbol = this.getAttribute('data-symbol');
|
||||
this.mode = this.getAttribute('data-mode');
|
||||
this.hideSensitiveInfo = this.getAttribute('data-hide-sensitive-info') == 'true';
|
||||
|
||||
// Set data for customaction
|
||||
this.callbackaction.childNodes[1].value=`${this.tx['txid']},${this.tx['vout']}`
|
||||
this.callbackaction.childNodes[1].amount = `${this.tx['amount']}` // a bit hackish, doing that to have access via this in next line
|
||||
this.callbackaction.childNodes[1].addEventListener("change", (e) => {
|
||||
let event = new CustomEvent('txRowCustomSelected', { detail: {
|
||||
txid: this.tx.txid,
|
||||
// Listening for changes of coin selection checkboxes
|
||||
this.coinSelectCheckbox.value=`${this.tx['txid']},${this.tx['vout']}` // Necessary?
|
||||
this.coinSelectCheckbox.amount = `${this.tx['amount']}` // a bit hackish, doing that to have access via this in next line - necessary?
|
||||
this.coinSelectCheckbox.addEventListener("change", (e) => {
|
||||
let event = new CustomEvent(('coinSelectRowSelected'), { detail: {
|
||||
txid: this.tx.txid, // Detail currently not used, remove?
|
||||
vout: this.tx.vout,
|
||||
amount: this.amount
|
||||
amount: this.amount,
|
||||
locked: this.tx.locked,
|
||||
} }
|
||||
);
|
||||
this.dispatchEvent(event);
|
||||
|
|
@ -257,8 +321,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Show blockhash
|
||||
if (this.showBlockhash) {
|
||||
this.blockhash.classList.remove('hidden');
|
||||
|
|
@ -285,7 +347,8 @@
|
|||
selected: this.selectTxValue.value == "true",
|
||||
txid: this.tx.txid,
|
||||
vout: this.tx.vout,
|
||||
amount: this.amount
|
||||
amount: this.amount,
|
||||
locked: this.tx.locked,
|
||||
} });
|
||||
this.dispatchEvent(event);
|
||||
}
|
||||
|
|
@ -315,15 +378,15 @@
|
|||
}
|
||||
|
||||
isSelected() {
|
||||
return this.callbackaction.childNodes[1].checked
|
||||
return this.coinSelectCheckbox.checked
|
||||
}
|
||||
|
||||
unselect() {
|
||||
this.callbackaction.childNodes[1].checked = false
|
||||
this.coinSelectCheckbox.checked = false
|
||||
}
|
||||
|
||||
select() {
|
||||
this.callbackaction.childNodes[1].checked = true
|
||||
this.coinSelectCheckbox.checked = true
|
||||
}
|
||||
|
||||
rbfPopup(rbfType) {
|
||||
|
|
@ -376,9 +439,10 @@
|
|||
case "send":
|
||||
return `{{ url_for('static', filename='img') }}/send.svg`;
|
||||
case "receive":
|
||||
return `{{ url_for('static', filename='img') }}/receive.svg`;
|
||||
case "immature":
|
||||
case "generate":
|
||||
return `{{ url_for('static', filename='img') }}/receive.svg`;
|
||||
return `{{ url_for('static', filename='img') }}/gear.svg`;
|
||||
case "selftransfer":
|
||||
return `{{ url_for('static', filename='img') }}/transfer.svg`;
|
||||
case "mixed":
|
||||
|
|
|
|||
|
|
@ -1,9 +1,49 @@
|
|||
<!--
|
||||
Custom element for showing a table of transactions.
|
||||
Can be configured to show:
|
||||
- The transaction history of a wallet (set attribute `type` to "txlist" and `wallet` to the wallet's alias)
|
||||
- The UTXO list of a wallet (set attribute `type` to "utxo" and `wallet` to the wallet's alias)
|
||||
- The transaction history of all the user's wallets (set attribute `type` to "txlist" and `wallet` to null)
|
||||
- The UTXO list of all the user's wallets (set attribute `type` to "utxo" and `wallet` to null)
|
||||
The uses Specter's API to fetch transactions every time the user changes its parameters.
|
||||
These parameters are configured using components included in this table element:
|
||||
- Tabs for switching between History (`type="txlist"`) and UTXO (`type="utxo"`) list types
|
||||
- Sorting transactions by any column (ascending or descending)
|
||||
- Search bar to filter transactions using free text search
|
||||
- Configuring limit for transactions displayed per page
|
||||
- Pagination, jumping between pages and showing the total pages count
|
||||
- Export - export the full (unfiltered) list from which the table is drawing its data as CSV (optionally with price history)
|
||||
|
||||
The table also supports:
|
||||
- Showing amounts in either BTC or sats (set attribute `btc-unit` to either "btc" or "sat")
|
||||
- Showing prices next to the amounts (set attribute `price` to the BTC price and symbol to the symbol of the currency you're pricing at)
|
||||
- Showing validated blockhash column (set attribute `blockhash` to either "true" or "false")
|
||||
- Limiting txs/utxos to just those associated with the specified `data-service-id`.
|
||||
- Hide columns by hide-columns="category time"
|
||||
- Enable scrollbar
|
||||
- Toggle for scrollbar
|
||||
|
||||
{#- Usage -
|
||||
|
||||
<tx-table
|
||||
btc-unit="{{ specter.unit }}"
|
||||
hide-sensitive-info="{{ specter.hide_sensitive_info | lower }}"
|
||||
type="utxo"
|
||||
hide-columns="category time txid confirmations action"
|
||||
blockhash="true"
|
||||
hide-switcher="true"
|
||||
wallet="{{ wallet.alias }}"
|
||||
scrollbar-height="333px"
|
||||
scrollbar-default="off"
|
||||
compact-switch="true">
|
||||
</tx-table>
|
||||
#}
|
||||
-->
|
||||
|
||||
<template id="tx-table">
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}">
|
||||
<style>
|
||||
.tx-table {
|
||||
display: table;
|
||||
}
|
||||
tx-row {
|
||||
display: contents;
|
||||
}
|
||||
|
|
@ -19,12 +59,10 @@
|
|||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
input[type=number] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.pagination-idx {
|
||||
min-width: 50px;
|
||||
max-width: 50px;
|
||||
|
|
@ -33,17 +71,16 @@
|
|||
text-align: center;
|
||||
height: 35px;
|
||||
}
|
||||
.search {
|
||||
/* float: right; */
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
max-width: 350px;
|
||||
}
|
||||
.search-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.search {
|
||||
margin: auto;
|
||||
}
|
||||
.up-arrow {
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
|
@ -56,7 +93,6 @@
|
|||
margin-right: 50px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.down-arrow {
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
|
@ -70,7 +106,6 @@
|
|||
margin-right: 50px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.export-container {
|
||||
max-width: 50%;
|
||||
margin: 20px auto;
|
||||
|
|
@ -91,7 +126,85 @@
|
|||
margin: 5px;
|
||||
height: 38px;
|
||||
}
|
||||
.action-teaser {
|
||||
max-width: 20em;
|
||||
border: 1px solid var(--cmap-border);
|
||||
background-color: var(--cmap-bg-lighter);
|
||||
padding: 0.8em;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
line-height: 12px;
|
||||
font-weight: 50;
|
||||
color: grey;
|
||||
}
|
||||
.freeze-tx-btn {
|
||||
border: 1px solid var(--cmap-border);
|
||||
border-radius: 0.5em;
|
||||
background-color: var(--cmap-bg-lighter);
|
||||
padding: 0.8em;
|
||||
text-align: center;
|
||||
font-weight: 250;
|
||||
}
|
||||
.freeze-tx-btn:hover {
|
||||
background-color: var(--cmap-bg-lightest);
|
||||
border: 1px solid var(--main-color);
|
||||
}
|
||||
.compose-tx-btn {
|
||||
border: 1px solid var(--cmap-border);
|
||||
border-radius: 0.5em;
|
||||
background-color: var(--cmap-bg-lighter);
|
||||
padding: 0.8em;
|
||||
text-align: center;
|
||||
font-weight: 250;
|
||||
}
|
||||
.compose-tx-btn:hover {
|
||||
background-color: var(--cmap-bg-lightest);
|
||||
border: 1px solid var(--main-color);
|
||||
}
|
||||
.manage-psbt-btn {
|
||||
border: 1px solid var(--cmap-border);
|
||||
border-radius: 0.5em;
|
||||
background-color: var(--cmap-bg-lighter);
|
||||
padding: 0.8em;
|
||||
text-align: center;
|
||||
font-weight: 250;
|
||||
}
|
||||
.manage-psbt-btn:hover {
|
||||
background-color: var(--cmap-bg-lightest);
|
||||
border: 1px solid var(--main-color);
|
||||
}
|
||||
.table-container {
|
||||
width: 100%;
|
||||
}
|
||||
.scrollbar {
|
||||
min-height: 350px;
|
||||
overflow-y: scroll;
|
||||
scrollbar-color: hsl(217, 10%, 80%) var(--cmap-border);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.table-container::-webkit-scrollbar {
|
||||
width: 0.4em;
|
||||
}
|
||||
.table-container::-webkit-scrollbar-track {
|
||||
background: var(--cmap-bg);
|
||||
border-radius: 100vw;
|
||||
margin-top: 4em;
|
||||
}
|
||||
.table-container::-webkit-scrollbar-thumb {
|
||||
background: hsl(217, 10%, 80%);
|
||||
border-radius: 100vw;
|
||||
}
|
||||
.table-container::-webkit-scrollbar-thumb:hover {
|
||||
background: white;
|
||||
}
|
||||
.scrollbar-checkbox-div {
|
||||
margin-left: 15px;
|
||||
}
|
||||
input:checked + .slider {
|
||||
background-color: var(--cmap-border);
|
||||
}
|
||||
</style>
|
||||
|
||||
<nav class="switcher row">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -112,34 +225,48 @@
|
|||
</div>
|
||||
<div class="search-container">
|
||||
<!--Left part of the toolbar-->
|
||||
<div style="flex: 1;">
|
||||
<div style="flex: 0 0 200px">
|
||||
{% include "includes/page-limit-select.html" %}
|
||||
</div>
|
||||
<!--Middle part of the toolbar-->
|
||||
<input class="search" type="text" placeholder="Search..." />
|
||||
<tool-tip title="Search options">
|
||||
{{ _("anything in the address, tx, label, confirmations or amount") }}<br>
|
||||
{{ _('search like') }} <code>%d.%m.%Y %H:%M</code> {{ _('if you want to search in the time field') }}<br>
|
||||
{{ _("<code> 1</code> for anything more than 1 BTC and likewise <code>< 1</code> (don't forget the space)") }}<br>
|
||||
</tool-tip>
|
||||
<!--Placeholder for right part of the toolbar-->
|
||||
<div style="flex: 1;"></div>
|
||||
</div>
|
||||
<div class="selected-rows-action-box row hidden">
|
||||
<form class="freeze-tx-form" method="POST">
|
||||
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="button" class="btn freeze-tx-btn">
|
||||
<!--Middle part of the toolbar-->
|
||||
<div style="flex-grow: 0.5">
|
||||
<input class="search" type="text" placeholder="Search..."></input>
|
||||
</div>
|
||||
<div style="flex: 0 0 50px">
|
||||
<tool-tip title="Search options">
|
||||
{{ _("anything in the address, tx, label, confirmations or amount") }}<br>
|
||||
{{ _('search like') }} <code>%d.%m.%Y %H:%M</code> {{ _('if you want to search in the time field') }}<br>
|
||||
{{ _("<code> 1</code> for anything more than 1 BTC and likewise <code>< 1</code> (don't forget the space)") }}<br>
|
||||
</tool-tip>
|
||||
</div>
|
||||
<!--Right part of the toolbar-->
|
||||
<div id="action-teaser" class="action-teaser hidden" style="flex-grow: 1">
|
||||
<span id="action-teaser-text" class="action-teaser-text">Select checkboxes for actions</span>
|
||||
</div>
|
||||
<div class="selected-rows-action-box row hidden">
|
||||
<form class="freeze-tx-form" method="POST">
|
||||
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="button" class="btn freeze-tx-btn">
|
||||
<tool-tip title="Freezing UTXOs">
|
||||
{{ _("Freezing a UTXO will make Specter disallow spending it.") }}<br><br>
|
||||
{{ _("This is useful especially for dealing with a dust attack, where you don't want to accidentally spend the dust received.") }}"
|
||||
{{ _("This is useful especially for dealing with a dust attack, where you don't want to accidentally spend the dust received.") }}
|
||||
</tool-tip>
|
||||
Un/Freeze UTXOs
|
||||
</button>
|
||||
</form>
|
||||
<form class="compose-tx-form" method="POST">
|
||||
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="button" class="btn compose-tx-btn">{{ _("Create new transaction") }}</button>
|
||||
</form>
|
||||
</form>
|
||||
<form class="compose-tx-form" method="POST">
|
||||
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="button" class="btn compose-tx-btn">{{ _("Create new transaction") }}</button>
|
||||
</form>
|
||||
<a id="manage-psbt-btn" type="button" class="btn manage-psbt-btn hidden" href="">{{ _("Manage unsigned PSBT") }}</a>
|
||||
</div>
|
||||
<div id="scrollbar-checkbox-div" class="scrollbar-checkbox-div row hidden">
|
||||
{{ _("Compact table") }}
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="scrollbar-checkbox" class="scrollbar-checkbox" name="scrollbar-checkbox">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="export-container hidden">
|
||||
<h1>{{ _("Export transactions to CSV") }}</h1>
|
||||
|
|
@ -156,78 +283,57 @@
|
|||
</div>
|
||||
<a class="export btn" download>{{ _("Export") }}</a>
|
||||
</div>
|
||||
<table class="tx-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<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="txid" class="column-txid optional txid-header">TxID<div class="txid-arrow"></div></th>
|
||||
<th value="label" class="column-label optional label-header">{{ _("Label") }}<div class="label-arrow"></div></th>
|
||||
<th value="amount" class="column-amount amount-header">{{ _("Amount") }}<div class="amount-arrow"></div></th>
|
||||
<th value="time" class="column-time time-header">{{ _("Time") }}<div class="time-arrow up-arrow"></div></th>
|
||||
<th value="confirmations" class="column-confirmations confirmations-header">{{ _("Confirmations") }}<div class="confirmations-arrow"></div></th>
|
||||
<th value="blockhash" class="column-blockhash optional hidden blockhash-header">
|
||||
{{ _("Block Hash") }}
|
||||
{% include "includes/merkletooltip.html" %}
|
||||
</th>
|
||||
<th class="column-action" style="width: 50px; padding: 10px;">
|
||||
<button type="button" class="export-btn btn" style="width: 50px;">
|
||||
{{ _("Export") }}
|
||||
</button>
|
||||
</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>
|
||||
<tr class="summary-row hidden">
|
||||
<td class="column-callbackaction"></td>
|
||||
<td class="column-category" ></td>
|
||||
<td class="column-txid summary-tx-count"></td>
|
||||
<td class="column-label optional"></td>
|
||||
<td class="column-amount summary-amount"></td>
|
||||
<td class="column-time optional"></td>
|
||||
<td class="column-confirmations"> </td>
|
||||
<td class="column-blockhash optional hidden blockhash-summary"></td>
|
||||
<td class="column-action"></td>
|
||||
</tr>
|
||||
<tbody class="tx-tbody">
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="table-container" class="table-container">
|
||||
<table class="tx-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<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="txid" class="column-txid optional txid-header">TxID<div class="txid-arrow"></div></th>
|
||||
<th value="label" class="column-label optional label-header">{{ _("Label") }}<div class="label-arrow"></div></th>
|
||||
<th value="amount" class="column-amount amount-header">{{ _("Amount") }}<div class="amount-arrow"></div></th>
|
||||
<th value="time" class="column-time time-header">{{ _("Time") }}<div class="time-arrow up-arrow"></div></th>
|
||||
<th value="confirmations" class="column-confirmations confirmations-header">{{ _("Confirmations") }}<div class="confirmations-arrow"></div></th>
|
||||
<th value="blockhash" class="column-blockhash optional hidden blockhash-header">
|
||||
{{ _("Block Hash") }}
|
||||
{% include "includes/merkletooltip.html" %}
|
||||
</th>
|
||||
<th class="column-action" style="width: 50px; padding: 10px;">
|
||||
<button type="button" class="export-btn btn" style="width: 50px;">
|
||||
{{ _("Export") }}
|
||||
</button>
|
||||
</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>
|
||||
<tr class="summary-row hidden">
|
||||
<td class="column-callbackaction"></td>
|
||||
<td class="column-category" ></td>
|
||||
<td class="column-txid summary-tx-count"></td>
|
||||
<td class="column-label optional"></td>
|
||||
<td class="column-amount summary-amount"></td>
|
||||
<td class="column-time optional"></td>
|
||||
<td class="column-confirmations"> </td>
|
||||
<td class="column-blockhash optional hidden blockhash-summary"></td>
|
||||
<td class="column-action"></td>
|
||||
</tr>
|
||||
<tbody class="tx-tbody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
/**
|
||||
* Custom element for showing a table of transactions.
|
||||
* Can be configured to show:
|
||||
* - The transaction history of a wallet (set attribute `type` to "txlist" and `wallet` to the wallet's alias)
|
||||
* - The UTXO list of a wallet (set attribute `type` to "utxo" and `wallet` to the wallet's alias)
|
||||
* - The transaction history of all the user's wallets (set attribute `type` to "txlist" and `wallet` to null)
|
||||
* - The UTXO list of all the user's wallets (set attribute `type` to "utxo" and `wallet` to null)
|
||||
* The uses Specter's API to fetch transactions every time the user changes its parameters.
|
||||
* These parameters are configured using components included in this table element:
|
||||
* - Tabs for switching between History (`type="txlist"`) and UTXO (`type="utxo"`) list types
|
||||
* - Sorting transactions by any column (ascending or descending)
|
||||
* - Search bar to filter transactions using free text search
|
||||
* - Configuring limit for transactions displayed per page
|
||||
* - Pagination, jumping between pages and showing the total pages count
|
||||
* - Export - export the full (unfiltered) list from which the table is drawing its data as CSV (optionally with price history)
|
||||
*
|
||||
* The table also supports:
|
||||
* - Showing amounts in either BTC or sats (set attribute `btc-unit` to either "btc" or "sat")
|
||||
* - Showing prices next to the amounts (set attribute `price` to the BTC price and symbol to the symbol of the currency you're pricing at)
|
||||
* - Showing validated blockhash column (set attribute `blockhash` to either "true" or "false")
|
||||
* - Limiting txs/utxos to just those associated with the specified `data-service-id`.
|
||||
* - Hide columns by hide-columns="category time"
|
||||
*/
|
||||
class TxTableElement extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
|
@ -237,6 +343,7 @@
|
|||
var clone = style.cloneNode(true);
|
||||
this.el = clone.querySelector(".tx-table");
|
||||
this.tbody = clone.querySelector(".tx-tbody");
|
||||
this.tableContainer = clone.getElementById("table-container")
|
||||
|
||||
// Empty row element
|
||||
this.emptyRow = clone.querySelector(".empty");
|
||||
|
|
@ -292,31 +399,46 @@
|
|||
this.composeTxBtn = clone.querySelector(".compose-tx-btn");
|
||||
this.freezeTxForm = clone.querySelector(".freeze-tx-form");
|
||||
this.freezeTxBtn = clone.querySelector(".freeze-tx-btn");
|
||||
this.managePSBTBtn = clone.getElementById("manage-psbt-btn")
|
||||
this.selectedRows = [];
|
||||
|
||||
// Action teaser
|
||||
this.actionTeaserDiv = clone.getElementById("action-teaser")
|
||||
this.actionTeaserText = clone.getElementById("action-teaser-text")
|
||||
|
||||
// Scrollbar checkbox
|
||||
this.scrollbarCheckboxDiv = clone.getElementById("scrollbar-checkbox-div")
|
||||
this.scrollbarCheckbox = clone.getElementById("scrollbar-checkbox")
|
||||
|
||||
// CustomAction Preselection default
|
||||
this.selectedCoins = []
|
||||
|
||||
this.txlistViewBtn.onclick = () => {
|
||||
if (!this.txlistViewBtn.classList.contains("checked")) {
|
||||
this.idx = 0
|
||||
this.utxoViewBtn.classList.remove("checked");
|
||||
this.txlistViewBtn.classList.add("checked");
|
||||
this.setAttribute("type","txlist");
|
||||
this.actionTeaserDiv.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
this.utxoViewBtn.onclick = () => {
|
||||
if (!this.utxoViewBtn.classList.contains("checked")) {
|
||||
this.idx = 0
|
||||
this.txlistViewBtn.classList.remove("checked");
|
||||
this.utxoViewBtn.classList.add("checked");
|
||||
this.setAttribute("type","utxo");
|
||||
// The action teaser should not be visible on wallets overview
|
||||
if (this.wallet != null) {
|
||||
this.actionTeaserDiv.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Init call id to avoid fetch returning after another one triggered
|
||||
this.callId = 0
|
||||
|
||||
|
||||
// Initialize default sort, search, limit and page parameters
|
||||
this.idx = 0;
|
||||
this.limit = 50;
|
||||
|
|
@ -329,7 +451,7 @@
|
|||
this.paginationNext.onclick = () => {
|
||||
this.idx++;
|
||||
console.log("paginationNext fetch");
|
||||
|
||||
this.update();
|
||||
}
|
||||
|
||||
// Previous page action
|
||||
|
|
@ -421,6 +543,7 @@
|
|||
this.export.href = this.export.href.split('?')[0] + `?exportPrices=${this.exportPricesSwitch.checked}&search=${this.search}&sortby=${this.sortby}&sortdir=${this.sortdir}`;
|
||||
}
|
||||
|
||||
// Create transaction with selected UTXO
|
||||
this.composeTxBtn.onclick = () => {
|
||||
for (let tx of this.selectedRows) {
|
||||
this.composeTxForm.innerHTML += `
|
||||
|
|
@ -431,6 +554,7 @@
|
|||
this.composeTxForm.submit();
|
||||
}
|
||||
|
||||
// Freeze selected UTXO
|
||||
this.freezeTxBtn.onclick = () => {
|
||||
for (let tx of this.selectedRows) {
|
||||
this.freezeTxForm.innerHTML += `
|
||||
|
|
@ -440,6 +564,7 @@
|
|||
this.freezeTxForm.innerHTML += `<input type="hidden" name="action" value="freezeutxo"/>`
|
||||
this.freezeTxForm.action = `{{ url_for('wallets_endpoint.history', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", this.wallet);
|
||||
this.freezeTxForm.submit();
|
||||
|
||||
}
|
||||
|
||||
// Attach the created element to the shadow dom
|
||||
|
|
@ -447,11 +572,23 @@
|
|||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['blockhash', 'btc-unit', 'price', 'symbol', 'type', 'hide-sensitive-info', 'wallet', 'service-id', 'selected-coins'];
|
||||
return ['blockhash', 'btc-unit', 'price', 'symbol', 'type', 'hide-sensitive-info', 'wallet', 'service-id', 'selected-coins', 'scrollbar-height', 'scrollbar-default', 'compact-switch'];
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// Hide columns
|
||||
// Manage PSBT
|
||||
this.managePSBTBtn.href = `{{ url_for('wallets_endpoint.send_pending', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", this.wallet);
|
||||
|
||||
// Listerning for changes in compact table
|
||||
this.scrollbarCheckbox.addEventListener("click", () => {
|
||||
if (this.scrollbarCheckbox.checked) {
|
||||
this.tableContainer.classList.add("scrollbar")
|
||||
}
|
||||
else {
|
||||
this.tableContainer.classList.remove("scrollbar")
|
||||
}
|
||||
})
|
||||
|
||||
this.hideColumns = this.getAttribute("hide-columns") ?? "callbackaction"
|
||||
if (this.hideColumns && this.hideColumns != "null") {
|
||||
this.hideColumns.split(" ").forEach((column_name) => {
|
||||
|
|
@ -486,6 +623,7 @@
|
|||
})
|
||||
}
|
||||
|
||||
// TODO: Remove? Not used anywhere and looks incomplete
|
||||
select_coins(selected_coins) {
|
||||
this.shadowRoot.querySelectorAll(".txrowitem").forEach((item) => {
|
||||
selected_coins.forEach((selected_coin) => {
|
||||
|
|
@ -507,8 +645,18 @@
|
|||
* - type: Transactions list type to load. Either "txlist" or "utxo"
|
||||
* - hide-sensitive-info: Mask user sensitive info. Either "true" or "false"
|
||||
* - wallet: The wallet alias (null to get all wallets combined)
|
||||
* - scrollbar-height: Height of the table container with the scrollbar
|
||||
* - scrollbar-default: Whether scrollbar is enabled by default
|
||||
* - compact-switch: Offer a switch to change between long and compact list
|
||||
*/
|
||||
attributeChangedCallback(attrName, oldValue, newValue) {
|
||||
this.tableContainer.style.height = this.getAttribute("scrollbar-height")
|
||||
if (this.getAttribute("scrollbar-default") == "on") {
|
||||
this.tableContainer.classList.add("scrollbar")
|
||||
}
|
||||
if (this.getAttribute("compact-switch") == "true") {
|
||||
this.scrollbarCheckboxDiv.classList.remove("hidden")
|
||||
}
|
||||
if (attrName != "selected-coins") {
|
||||
if (this.getAttribute('blockhash') == 'true' && this.getAttribute('type') == "txlist" && this.getAttribute('wallet')) {
|
||||
this.blockhashHeader.classList.remove('hidden');
|
||||
|
|
@ -558,8 +706,8 @@
|
|||
}
|
||||
|
||||
/**
|
||||
* searches this.selectedCoin for a transactionId and removes that if found and returns True in that case.
|
||||
* Explanation: An initial pre-ticking the checkboxes is not that easy as the state of the component is
|
||||
* searches this.selectedCoins for a transactionId and removes that if found and returns True in that case.
|
||||
* Explanation: An initial pre ticking of the checkboxes is not that easy as the state of the component is
|
||||
* created dynamically. This also involves the preselected checkboxes which get set by setAttribute("selected-coins",...)
|
||||
* In order to solve this, this.selectedCoins is an array of txids which nedd to be checked and they get "consumed" when the
|
||||
* the check is positive.
|
||||
|
|
@ -572,7 +720,7 @@
|
|||
if (filtered.length == this.selectedCoins.length) {
|
||||
return false
|
||||
} else {
|
||||
console.log("Found txid"+txid)
|
||||
console.log(`Found txid: ${txid}`)
|
||||
this.selectedCoins = filtered
|
||||
return true
|
||||
}
|
||||
|
|
@ -641,6 +789,7 @@
|
|||
formData.append('service_id', this.serviceId);
|
||||
}
|
||||
formData.append('csrf_token', '{{ csrf_token() }}');
|
||||
console.log(url)
|
||||
try {
|
||||
let callId = this.callId;
|
||||
const response = await fetch(
|
||||
|
|
@ -659,7 +808,6 @@
|
|||
}
|
||||
const jsonResponse = await response.json();
|
||||
let showBlockhash = !this.blockhashHeader.classList.contains('hidden');
|
||||
|
||||
if ("txlist" in jsonResponse) {
|
||||
this.render(jsonResponse, showBlockhash)
|
||||
return
|
||||
|
|
@ -671,7 +819,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
render(jsonResponse, showBlockhash) {
|
||||
async render(jsonResponse, showBlockhash) {
|
||||
// jsonresponse is something like:
|
||||
// {
|
||||
// "pageCount": 1,
|
||||
|
|
@ -776,31 +924,154 @@
|
|||
|
||||
// if preselectedCoins are there, we need to dispatch an event
|
||||
let dispatchLater = false
|
||||
for (let txRow of this.shadowRoot.querySelectorAll('tx-row')) {
|
||||
|
||||
// Get txids of pending PSBTs
|
||||
if (this.wallet) {
|
||||
var pending_psbts = await this.fetchWalletData();
|
||||
var pending_psbts_txids = []
|
||||
const keys = Object.keys(pending_psbts)
|
||||
keys.forEach((key, index) => {
|
||||
for (let input of pending_psbts[key]["inputs"]) {
|
||||
let txid = input["txid"]
|
||||
pending_psbts_txids.push(txid)
|
||||
}
|
||||
});
|
||||
} else {
|
||||
pending_psbts = []
|
||||
}
|
||||
for (let txRow of this.shadowRoot.querySelectorAll('tx-row')) {
|
||||
// EventListener when hovering over checkboxes
|
||||
txRow.selectTxImg.addEventListener("mouseover", () => {
|
||||
this.actionTeaserDiv.style.color = 'orange';
|
||||
})
|
||||
txRow.selectTxImg.addEventListener("mouseout", () => {
|
||||
this.actionTeaserDiv.style.color = 'grey';
|
||||
})
|
||||
// EventListener when hovering over categories
|
||||
txRow.category.addEventListener("mouseover", () => {
|
||||
if (txRow.tx.confirmations == 0) {
|
||||
txRow.iconInfoText.innerText = `${txRow.tx.category} (unconfirmed)`
|
||||
}
|
||||
else {
|
||||
txRow.iconInfoText.innerText = txRow.tx.category
|
||||
}
|
||||
})
|
||||
// Adding additional text to special UTXO in coin selection and UTXO list
|
||||
if (txRow.tx.locked && pending_psbts_txids.includes(txRow.tx.txid) == false) {
|
||||
// Replaces checkbox for locked UTXO in coin selection by snowflake image
|
||||
txRow.callbackaction.innerHTML = ""
|
||||
// Need to clone otherwise we're ripping the node from the UTXO overview
|
||||
let frozenImgClone = txRow.frozenImg.cloneNode()
|
||||
txRow.callbackaction.append(frozenImgClone)
|
||||
// Adding "Frozen" in coin selection and UTXO overview
|
||||
txRow.callbackaction.append("Frozen")
|
||||
txRow.categoryCell.append("Frozen")
|
||||
}
|
||||
if (txRow.tx.locked && pending_psbts_txids.includes(txRow.tx.txid) == true) {
|
||||
txRow.callbackaction.innerHTML = ""
|
||||
let frozenImgClone = txRow.frozenImg.cloneNode()
|
||||
txRow.callbackaction.append(frozenImgClone)
|
||||
// Adding "Unsigned" in coin selection and UTXO overview
|
||||
txRow.callbackaction.append("Unsigned")
|
||||
txRow.categoryCell.append("Unsigned")
|
||||
}
|
||||
if (txRow.tx.confirmations == 0) {
|
||||
var el = txRow.callbackaction.childNodes
|
||||
var nodeArray = Array.from(el)
|
||||
function checkNodes(node) {
|
||||
return node.textContent == "Frozen" || node.textContent == "Unsigned"
|
||||
}
|
||||
if (nodeArray.some(checkNodes)) {
|
||||
txRow.callbackaction.append(" & unconfirmed");
|
||||
}
|
||||
else {
|
||||
txRow.callbackaction.append("Unconfirmed");
|
||||
}
|
||||
}
|
||||
|
||||
// Preselect if necessary
|
||||
if (this.checkForSelectedCoin(txRow.tx.txid)) {
|
||||
txRow.select()
|
||||
dispatchLater = true
|
||||
}
|
||||
txRow.addEventListener('txRowCustomSelected', (e) => {
|
||||
// Listening for (un-) selecting coin selection checkboxes
|
||||
txRow.addEventListener('coinSelectRowSelected', (e) => {
|
||||
this.fireCustomSelectedEvent()
|
||||
})
|
||||
|
||||
txRow.addEventListener('txRowSelected', e=>{
|
||||
let selectedRow = e.target;
|
||||
// Listening for (un-) selecting standard checkboxes
|
||||
txRow.addEventListener('txRowSelected', (e) => {
|
||||
let selectedRow = e.target;
|
||||
if (e.detail.selected) {
|
||||
this.selectedRows.push({
|
||||
txid: e.detail.txid,
|
||||
vout: e.detail.vout,
|
||||
amount: e.detail.amount.toFixed(8)
|
||||
amount: e.detail.amount.toFixed(8),
|
||||
locked: e.detail.locked,
|
||||
});
|
||||
} else {
|
||||
// Remove unselected rows from array
|
||||
this.selectedRows = this.selectedRows.filter(row => !(row.txid === e.detail.txid && row.vout === e.detail.vout))
|
||||
}
|
||||
if (this.selectedRows.length == 0) {
|
||||
this.selectedRowsActionBox.classList.add('hidden');
|
||||
} else {
|
||||
// Returns true if a selected UTXO is locked
|
||||
function checkLocked(row) {
|
||||
return row.locked == true
|
||||
}
|
||||
// Returns true if a selected UTXO is unsigned-locked
|
||||
function checkUnsignedLocked(row) {
|
||||
if (row.locked == true && pending_psbts_txids.includes(row.txid) == true) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Locked UTXO is selected
|
||||
if (this.selectedRows.some(checkLocked) && !this.selectedRows.some(checkUnsignedLocked)) {
|
||||
this.composeTxForm.classList.add('hidden');
|
||||
this.actionTeaserDiv.classList.add("hidden");
|
||||
this.selectedRowsActionBox.classList.remove('hidden');
|
||||
this.freezeTxForm.classList.remove('hidden');
|
||||
for (let txRow of this.shadowRoot.querySelectorAll('tx-row')) {
|
||||
if ((!txRow.tx.locked || pending_psbts_txids.includes(txRow.tx.txid) == true)) {
|
||||
txRow.selectTxImg.classList.add("hidden")
|
||||
txRow.selectTxImg.src = "{{ url_for('static', filename='img/checkbox-untick.svg') }}"
|
||||
txRow.selectTxValue.value = ""
|
||||
// Remove rows that were selected previously
|
||||
this.selectedRows = this.selectedRows.filter(row => (row.locked))
|
||||
}
|
||||
}
|
||||
}
|
||||
// Unsigned-locked UTXO is selected
|
||||
else if (this.selectedRows.some(checkUnsignedLocked)) {
|
||||
this.composeTxForm.classList.add('hidden');
|
||||
this.freezeTxForm.classList.add('hidden');
|
||||
this.selectedRowsActionBox.classList.remove('hidden');
|
||||
this.managePSBTBtn.classList.remove('hidden');
|
||||
this.actionTeaserDiv.classList.add("hidden");
|
||||
for (let txRow of this.shadowRoot.querySelectorAll('tx-row')) {
|
||||
if ((!pending_psbts_txids.includes(txRow.tx.txid) == true)) {
|
||||
txRow.selectTxImg.classList.add("hidden")
|
||||
txRow.selectTxImg.src = "{{ url_for('static', filename='img/checkbox-untick.svg') }}"
|
||||
txRow.selectTxValue.value = ""
|
||||
// Remove rows that were selected previously
|
||||
this.selectedRows = this.selectedRows.filter(row => (checkUnsignedLocked(row)))
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.selectedRowsActionBox.classList.remove('hidden');
|
||||
this.composeTxForm.classList.remove('hidden');
|
||||
this.freezeTxForm.classList.remove('hidden');
|
||||
this.managePSBTBtn.classList.add('hidden');
|
||||
this.actionTeaserDiv.classList.add("hidden");
|
||||
for (let txRow of this.shadowRoot.querySelectorAll('tx-row')) {
|
||||
txRow.selectTxImg.classList.remove("hidden")
|
||||
}
|
||||
}
|
||||
if (this.selectedRows.length == 0) {
|
||||
this.composeTxForm.classList.add('hidden');
|
||||
this.freezeTxForm.classList.add('hidden');
|
||||
this.managePSBTBtn.classList.add('hidden');
|
||||
this.actionTeaserDiv.classList.remove("hidden");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -809,21 +1080,38 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If this event is fired, then some row got ticked/unticked in the custom-selection-column
|
||||
*/
|
||||
|
||||
// If this event is fired, then some row got ticked/unticked in the custom-selection-column
|
||||
// This is picked up by coin selection
|
||||
fireCustomSelectedEvent() {
|
||||
let event = new CustomEvent('CustomSelected', { detail: {
|
||||
txs: this.getSelectedTxs()
|
||||
} }
|
||||
);
|
||||
let event = new CustomEvent('CustomSelected')
|
||||
this.dispatchEvent(event);
|
||||
}
|
||||
|
||||
async fetchWalletData() {
|
||||
// Currently only fetching the list of pending PSBT
|
||||
if (this.wallet) {
|
||||
var url = `{{ url_for('wallets_endpoint_api.pending_psbt_list', wallet_alias='WALLET_ALIAS') }}`.replace('WALLET_ALIAS', this.wallet);
|
||||
}
|
||||
try {
|
||||
const response = await fetch(url, {method: 'GET',});
|
||||
if (response.status != 200) {
|
||||
showError(`{{ _("Error while fetching wallet data:") }} ${response.statusText}`)
|
||||
}
|
||||
else {
|
||||
let pending_psbts_json = await response.json()
|
||||
var pending_psbts = pending_psbts_json["pending_psbts"]
|
||||
// This returns a promise
|
||||
return pending_psbts
|
||||
}
|
||||
}
|
||||
catch(e) {
|
||||
console.log("Caught error: ", e);
|
||||
showError(`{{ _("Failed to fetch wallet data! <br> Use this to ask about this error:") }} ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Shows a <tx-data> element popup for specified txid.
|
||||
* @param btcUnit - Bitcoin unit to display amounts with. Either "btc" or "sat"
|
||||
|
|
|
|||
|
|
@ -52,13 +52,12 @@
|
|||
</div>
|
||||
{{ _("Specter Data Backup") }}:
|
||||
<div class="note">
|
||||
{{ _("Warning: This backup does not include private seed of the hot wallets or, obviously, the private seed of your Hardwarewallets.") }}
|
||||
{{ _("Warning: This backup does not include the private keys of any hot wallet or the BIP 39 seed of your hardware wallets.") }}
|
||||
</div>
|
||||
<div class="row">
|
||||
<a href="{{ url_for('settings_endpoint.backup_file') }}" class="btn" style="width: 100%; margin-top: -5px;">{{ _("Download Specter backup files") }}</a>
|
||||
<a href="{{ url_for('settings_endpoint.backup_file') }}" class="btn" style="max-width: 500px">{{ _("Download Specter backup files") }}</a>
|
||||
</div>
|
||||
<br>
|
||||
<span style="margin: auto 5px auto 0px;">Load Specter backup:</span>
|
||||
<div class="tool-tip" style="float: right; margin-bottom: 5px;">
|
||||
<i class="tool-tip__icon">i</i>
|
||||
<p class="tool-tip__info">
|
||||
|
|
@ -70,12 +69,12 @@
|
|||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<span style="margin: auto 5px auto 0px;">{{ _("Load Specter backup") }}:</span><br><br>
|
||||
<span style="margin: auto 5px auto 0px;">{{ _("Load Specter backup") }}:</span><br>
|
||||
<div class="row">
|
||||
<input type="file" id="file" class="inputfile" webkitdirectory mozdirectory/>
|
||||
<input type="hidden" id="restorewallets" name="restorewallets"/>
|
||||
<input type="hidden" id="restoredevices" name="restoredevices"/>
|
||||
<label for="file" class="btn" style="margin-top: -5px;">
|
||||
<label for="file" class="btn" style="min-width: 500px; margin-top: 5px;">
|
||||
{{ _("Choose backup folder") }}
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,27 +6,78 @@
|
|||
- specter: Specter object
|
||||
#}
|
||||
{% macro total_wallet_balances(wallet, specter) -%}
|
||||
<h1>
|
||||
<small style="line-height:30px">Total balance:</small><br>
|
||||
<span style="color: #fff">
|
||||
<span id="fullbalance_amount">{{ wallet.fullbalance | btcunitamount }}</span>
|
||||
{% if specter.unit == 'sat' %}
|
||||
sats
|
||||
{% else %}
|
||||
{% if specter.is_testnet %}t{%endif%}{% if specter.is_liquid %}L{%endif%}BTC
|
||||
{% endif %}
|
||||
</span><br>
|
||||
<span class="note">{{ wallet.fullbalance | altunit }}</span>
|
||||
{% if wallet.balance.get("untrusted_pending", 0) or wallet.balance.get("immature", 0) %}<br>
|
||||
<small>( {{ wallet.balance.get("trusted", 0) | btcunitamount }} confirmed,
|
||||
{% if wallet.balance.get("untrusted_pending", 0) %}
|
||||
{{ wallet.balance.get("untrusted_pending", 0) | btcunitamount }} pending
|
||||
{% endif %}
|
||||
{% if wallet.balance.get("immature", 0) %}
|
||||
{{ wallet.balance.get("immature", 0) | btcunitamount }} immature
|
||||
{% endif %}
|
||||
)</small>
|
||||
{% endif %}
|
||||
<div style="display: flex; flex-direction: column; min-width: 300px; align-items: center">
|
||||
<span style="line-height:30px; font-size: 1.1em;">Balance:</span>
|
||||
<div class="smaller-card">
|
||||
<div>
|
||||
Total amount: <span id="fullbalance_amount">{{ wallet.amount_total | btcunitamount }}</span>
|
||||
{% if specter.unit == 'sat' %}
|
||||
sats
|
||||
{% else %}
|
||||
{% if specter.is_testnet %}t{%endif%}{% if specter.is_liquid %}L{%endif%}BTC
|
||||
{% endif %}
|
||||
{% if specter.price_check %}
|
||||
<span class="note">({{ wallet.amount_total | altunit }})</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if wallet.amount_unconfirmed > 0 or wallet.amount_immature > 0 %}
|
||||
<div>
|
||||
Confirmed: {{ wallet.amount_confirmed | btcunitamount }}
|
||||
{% if specter.unit == 'sat' %}
|
||||
sats
|
||||
{% else %}
|
||||
{% if specter.is_testnet %}t{%endif%}{% if specter.is_liquid %}L{%endif%}BTC
|
||||
{% endif %}
|
||||
{% if specter.price_check %}
|
||||
<span class="note">({{ wallet.amount_confirmed | altunit }})</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if wallet.amount_unconfirmed > 0 %}
|
||||
<div>
|
||||
Unconfirmed: {{ wallet.amount_unconfirmed | btcunitamount }}
|
||||
{% if specter.unit == 'sat' %}
|
||||
sats
|
||||
{% else %}
|
||||
{% if specter.is_testnet %}t{%endif%}{% if specter.is_liquid %}L{%endif%}BTC
|
||||
{% endif %}
|
||||
{% if specter.price_check %}
|
||||
<span class="note">({{ wallet.amount_unconfirmed | altunit }})</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if wallet.amount_immature > 0 and specter.chain == "regtest" %}
|
||||
<div style="color: grey">
|
||||
Immature: {{ wallet.amount_immature | btcunitamount }}
|
||||
{% if specter.unit == 'sat' %}
|
||||
sats
|
||||
{% else %}
|
||||
{% if specter.is_testnet %}t{%endif%}{% if specter.is_liquid %}L{%endif%}BTC
|
||||
{% endif %}
|
||||
{% if specter.price_check %}
|
||||
<span class="note" style="color: grey">({{ wallet.amount_immature | altunit }})</span>
|
||||
{% endif %}
|
||||
<tool-tip style="color: #fff" title='{{ _("What are immature outputs?") }}'>
|
||||
{{ _('The UTXO of coinbase transactions (mining rewards) have the special condition that they cannot be spent for at least 100 blocks. Their amount is not included in the total balance here. You see these outputs because you are using regtest and have mined blocks.') }}
|
||||
</tool-tip>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elif wallet.amount_immature > 0 and specter.chain == "main" %}
|
||||
<div style="color: #ccc">
|
||||
Immature: {{ wallet.amount_immature | btcunitamount }}
|
||||
{% if specter.unit == 'sat' %}
|
||||
sats
|
||||
{% else %}
|
||||
{% if specter.is_testnet %}t{%endif%}{% if specter.is_liquid %}L{%endif%}BTC
|
||||
{% endif %}
|
||||
{% if specter.price_check %}
|
||||
<span class="note" style="color: grey">({{ wallet.amount_immature | altunit }})</span>
|
||||
{% endif %}
|
||||
<tool-tip style="color: #fff" title='{{ _("What are immature outputs?") }}'>
|
||||
{{ _('The UTXO of coinbase transactions (mining rewards) have the special condition that they cannot be spent for at least 100 blocks. Their amount is not included in the total balance here.') }}
|
||||
</tool-tip>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if specter.is_liquid %}
|
||||
{% if wallet.balance.get("assets", {}) %}
|
||||
<div style="margin-bottom: 15px;">
|
||||
|
|
@ -47,7 +98,7 @@
|
|||
</small>
|
||||
<br>
|
||||
{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
{% if wallet.rescan_progress or specter.utxorescanwallet == wallet.alias %}
|
||||
<span id="wallet_rescan_data" style="display: contents;">
|
||||
<h2>Rescanning blockchain: <span id="{{ wallet.alias }}_balances_wallet_rescan_percents">{{ "%.2f"|format(specter.info["utxorescan"] if specter.utxorescanwallet == wallet.alias else wallet.rescan_progress * 100) }}</span>%</h2>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@
|
|||
blockhash="{{ specter.config.validate_merkle_proofs | lower }}"
|
||||
type="{{ tx_list_type }}"
|
||||
hide-sensitive-info="{{ specter.hide_sensitive_info | lower }}"
|
||||
wallet="{{ wallet.alias }}">
|
||||
wallet="{{ wallet.alias }}"
|
||||
scrollbar-height="500px"
|
||||
scrollbar-default="on"
|
||||
compact-switch="false">
|
||||
</tx-table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -27,44 +27,38 @@
|
|||
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="hidden" name="rbf_tx_id" value="{{ rbf_tx_id }}"/>
|
||||
<h1 class="padded">{{ _("Create Transaction") }}</h1>
|
||||
<p class="center">{{ _("Available funds:") }} {{wallet.full_available_balance | btcunitamount}}
|
||||
{% if specter.unit == 'sat' %}
|
||||
sats
|
||||
{% else %}
|
||||
{% if specter.is_testnet %}t{%endif%}{% if specter.is_liquid %}L{%endif%}BTC
|
||||
{% endif %}<br>
|
||||
{% if specter.price_check %}
|
||||
<span class="note">{{ wallet.full_available_balance | altunit }}</span>
|
||||
{% endif %}
|
||||
{% if specter.is_liquid and wallet.balance.get("assets", {}) %}
|
||||
<span>
|
||||
<br>{{ _("Assets:") }}<br>
|
||||
{% for asset in wallet.balance.get("assets",{}).keys() | sort %}
|
||||
{% set balance = wallet.balance.get("assets",{}).get(asset, {}) %}
|
||||
<span style="margin: 0 10px;">
|
||||
{{ (balance.get("trusted", 0) + balance.get("untrusted_pending", 0) + balance.get("immature", 0)) | btcamount }}
|
||||
<asset-label data-asset="{{asset}}" data-label="{{asset | assetlabel}}"></asset-label>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if wallet.locked_amount > 0 %}
|
||||
<div style="margin: auto; text-align: center; max-width: 90%;">
|
||||
<div class="tool-tip" style="text-align: center;">
|
||||
<i class="tool-tip__icon">i</i>
|
||||
<p class="tool-tip__info">
|
||||
<span class="info">
|
||||
<span class="info__title">{{ _("Funds waiting in unsigned transactions") }}<br><br></span>
|
||||
{{ _('To make funds available to spend in new transactions, just click on the "Unsigned" tab and delete unsigned transactions you have no need for anymore.') }}
|
||||
</span>
|
||||
</p>
|
||||
<div style="display: flex; justify-content: center; margin-bottom: 15px">
|
||||
<div>{{ _("Available funds:") }} {{wallet.amount_available | btcunitamount}}
|
||||
{% if specter.unit == 'sat' %}
|
||||
sats
|
||||
{% else %}
|
||||
{% if specter.is_testnet %}t{%endif%}{% if specter.is_liquid %}L{%endif%}BTC
|
||||
{% endif %}<br>
|
||||
{% if specter.price_check %}
|
||||
<span class="note">{{ wallet.amount_available | altunit }}</span>
|
||||
{% endif %}
|
||||
{% if specter.is_liquid and wallet.balance.get("assets", {}) %}
|
||||
<span>
|
||||
<br>{{ _("Assets:") }}<br>
|
||||
{% for asset in wallet.balance.get("assets",{}).keys() | sort %}
|
||||
{% set balance = wallet.balance.get("assets",{}).get(asset, {}) %}
|
||||
<span style="margin: 0 10px;">
|
||||
{{ (balance.get("trusted", 0) + balance.get("untrusted_pending", 0) + balance.get("immature", 0)) | btcamount }}
|
||||
<asset-label data-asset="{{asset}}" data-label="{{asset | assetlabel}}"></asset-label>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p style="display: inline; margin: auto; text-align: center;"> {{ _("You have") }} {{wallet.locked_amount | btcunitamount}} {% if specter.unit == 'sat' %}sats{% else %}{% if specter.is_testnet %}t{%endif%}BTC{% endif %} {{ _("waiting in unsigned transactions.") }}</p>
|
||||
<br>
|
||||
<br>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if wallet.amount_locked_unsigned > 0 or wallet.amount_frozen > 0 %}
|
||||
<div>
|
||||
<tool-tip title='{{ _("Why is this less than mal total balance?") }}'>
|
||||
{{ _(' Frozen outputs and outputs locked in unsigned transactions cannot be used here.
|
||||
To make more funds available, click on the "Unsigned" tab and delete not required transactions or unfreeze UTXO under "Transactions".') }}
|
||||
</tool-tip>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card" style="margin: auto;">
|
||||
<div id="recipients" {% if ui_option != 'ui' %}class="hidden"{% endif %}></div>
|
||||
<div id="recipients-txt-container" {% if ui_option == 'ui' %}class="hidden"{% endif %}>
|
||||
|
|
|
|||
|
|
@ -8,5 +8,5 @@
|
|||
{% from 'wallet/send/pending/components/pending_psbt_table.jinja' import pending_psbt_table %}
|
||||
{{ pending_psbt_table(pending_psbts, wallet_alias, wallet, specter) }}
|
||||
</div><br>
|
||||
<span class="center note padded">{{ _("Here you can manage PSBTs (Partially Signed Bitcoin Transactions) you have created but did not finish signing or broadcasted to the network.") }}</span>
|
||||
<span class="center note padded" style="margin-top: 12px;">{{ _("Here you can manage PSBTs (Partially Signed Bitcoin Transactions) you have created but did not finish signing or did not broadcast to the network.") }}</span>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -811,9 +811,9 @@ class Wallet:
|
|||
tx["category"] = tx_data.get("category") or "send"
|
||||
if "locked" not in tx:
|
||||
tx["locked"] = False
|
||||
self.full_utxo = sorted(utxo, key=lambda utxo: utxo["time"], reverse=True)
|
||||
self._full_utxo = sorted(utxo, key=lambda utxo: utxo["time"], reverse=True)
|
||||
except Exception as e:
|
||||
self.full_utxo = []
|
||||
self._full_utxo = []
|
||||
raise SpecterError(f"Failed to load utxos, {e}")
|
||||
|
||||
def getdata(self):
|
||||
|
|
@ -841,9 +841,21 @@ class Wallet:
|
|||
self.change_address = self.get_address(self.change_index, change=True)
|
||||
self.save_to_file()
|
||||
|
||||
@property
|
||||
def full_utxo(self):
|
||||
if hasattr(self, "_full_utxo"):
|
||||
return self._full_utxo
|
||||
else:
|
||||
self.check_utxo()
|
||||
return self._full_utxo
|
||||
|
||||
@property
|
||||
def utxo(self):
|
||||
return [utxo for utxo in self.full_utxo if not utxo["locked"]]
|
||||
return [utxo for utxo in self._full_utxo if not utxo["locked"]]
|
||||
|
||||
@property
|
||||
def locked_utxo(self):
|
||||
return [utxo for utxo in self._full_utxo if utxo["locked"]]
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
|
|
@ -914,10 +926,8 @@ class Wallet:
|
|||
|
||||
@property
|
||||
def locked_amount(self):
|
||||
amount = 0
|
||||
for psbt in self.pending_psbts.values():
|
||||
amount += sum([inp.float_amount for inp in psbt.inputs])
|
||||
return amount
|
||||
"""Deprecated, please use amount_locked_unsigned"""
|
||||
return self.amount_locked_unsigned
|
||||
|
||||
def delete_spent_pending_psbts(self, txs: list):
|
||||
"""
|
||||
|
|
@ -1543,22 +1553,6 @@ class Wallet:
|
|||
if self.use_descriptors
|
||||
else self.rpc.getbalances()["watchonly"]
|
||||
)
|
||||
# calculate available balance
|
||||
locked_utxo = self.rpc.listlockunspent()
|
||||
available = {}
|
||||
available.update(balance)
|
||||
for tx in locked_utxo:
|
||||
tx_data = self.gettransaction(tx["txid"])
|
||||
raw_tx = decoderawtransaction(tx_data["hex"], self.manager.chain)
|
||||
delta = raw_tx["vout"][tx["vout"]]["value"]
|
||||
if "confirmations" not in tx_data or tx_data["confirmations"] == 0:
|
||||
available["untrusted_pending"] -= delta
|
||||
else:
|
||||
available["trusted"] -= delta
|
||||
available["trusted"] = round(available["trusted"], 8)
|
||||
available["untrusted_pending"] = round(available["untrusted_pending"], 8)
|
||||
balance["trusted"] = available["trusted"]
|
||||
balance["untrusted_pending"] = available["untrusted_pending"]
|
||||
except Exception as e:
|
||||
raise SpecterError(f"was not able to get wallet_balance because {e}")
|
||||
self.balance = balance
|
||||
|
|
@ -1664,13 +1658,58 @@ class Wallet:
|
|||
# TODO: remove
|
||||
return self.getlabel(address)
|
||||
|
||||
@property
|
||||
def amount_confirmed(self):
|
||||
"""Confirmed outputs (and outputs created by the wallet for Bitcoin Core Hot Wallets)"""
|
||||
return round(self.balance["trusted"], 8)
|
||||
|
||||
@property
|
||||
def amount_unconfirmed(self):
|
||||
"""Unconfirmed outputs"""
|
||||
return round(self.balance["untrusted_pending"], 8)
|
||||
|
||||
@property
|
||||
def amount_frozen(self):
|
||||
"""Only frozen outputs, no outputs locked in unsigned PSBTS"""
|
||||
amount = 0
|
||||
frozen_txid = [utxo.split(":")[0] for utxo in self.frozen_utxo]
|
||||
for utxo in self.locked_utxo:
|
||||
if utxo["txid"] in frozen_txid:
|
||||
amount += utxo["amount"]
|
||||
return amount
|
||||
|
||||
@property
|
||||
def amount_locked_unsigned(self):
|
||||
"""Outputs locked in unsigned PSBTs"""
|
||||
amount = 0
|
||||
for psbt in self.pending_psbts.values():
|
||||
amount += sum([inp.float_amount for inp in psbt.inputs])
|
||||
return amount
|
||||
|
||||
@property
|
||||
def amount_immature(self):
|
||||
"""Immature coinbase outputs"""
|
||||
return round(self.balance["immature"], 8)
|
||||
|
||||
@property
|
||||
def amount_total(self):
|
||||
"""All outputs, including unconfirmed outputs, except for immature outputs"""
|
||||
return self.amount_confirmed + self.amount_unconfirmed
|
||||
|
||||
@property
|
||||
def amount_available(self):
|
||||
"""All outputs minus UTXO locked in unsigned transactions and frozen outputs"""
|
||||
return self.amount_total - self.amount_locked_unsigned - self.amount_frozen
|
||||
|
||||
@property
|
||||
def fullbalance(self):
|
||||
"""Deprecated, please use amount_total"""
|
||||
balance = self.balance
|
||||
return round(balance["trusted"], 8)
|
||||
|
||||
@property
|
||||
def full_available_balance(self):
|
||||
"""Deprecated, please use amount_available"""
|
||||
return round(self.balance["trusted"] + self.balance["untrusted_pending"], 8)
|
||||
|
||||
@property
|
||||
|
|
@ -1712,7 +1751,7 @@ class Wallet:
|
|||
total_sats = round(sum(amounts) * 1e8)
|
||||
|
||||
# if creating new tx - check we have enough balance
|
||||
if not rbf_edit_mode and self.full_available_balance < total_btc:
|
||||
if not rbf_edit_mode and self.amount_available < total_btc:
|
||||
raise SpecterError(
|
||||
f"Wallet {self.name} does not have sufficient funds to make the transaction."
|
||||
)
|
||||
|
|
@ -2043,7 +2082,7 @@ class Wallet:
|
|||
addr_amount = 0
|
||||
|
||||
for utxo in [
|
||||
utxo for utxo in self.full_utxo if utxo["address"] == addr.address
|
||||
utxo for utxo in self._full_utxo if utxo["address"] == addr.address
|
||||
]:
|
||||
addr_amount = addr_amount + utxo["amount"]
|
||||
addr_utxo = addr_utxo + 1
|
||||
|
|
|
|||
|
|
@ -470,36 +470,6 @@ def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder):
|
|||
specter.user_manager.save()
|
||||
specter.check()
|
||||
|
||||
assert not someuser.wallet_manager.working_folder is None
|
||||
|
||||
# Create a Wallet
|
||||
wallet_json = '{"label": "a_simple_wallet", "blockheight": 0, "descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/0/*)#xp8lv5nr", "devices": [{"type": "trezor", "label": "trezor"}]} '
|
||||
wallet_importer = WalletImporter(
|
||||
wallet_json, specter, device_manager=someuser.device_manager
|
||||
)
|
||||
wallet_importer.create_nonexisting_signers(
|
||||
someuser.device_manager,
|
||||
{"unknown_cosigner_0_name": "trezor", "unknown_cosigner_0_type": "trezor"},
|
||||
)
|
||||
dm: DeviceManager = someuser.device_manager
|
||||
wallet = wallet_importer.create_wallet(someuser.wallet_manager)
|
||||
try:
|
||||
# fund it with some coins
|
||||
bitcoin_regtest.testcoin_faucet(address=wallet.getnewaddress())
|
||||
# make sure it's confirmed
|
||||
bitcoin_regtest.mine()
|
||||
# Realize that the wallet has funds:
|
||||
wallet.update()
|
||||
except SpecterError as se:
|
||||
if str(se).startswith("Timeout"):
|
||||
pytest.fail(
|
||||
"We got a Bitcoin-RPC timeout while setting up the test, minting some coins. Test Error! Check cpu/mem utilastion and btc/elem logs!"
|
||||
)
|
||||
return
|
||||
else:
|
||||
raise se
|
||||
|
||||
assert wallet.fullbalance >= 20
|
||||
assert not specter.wallet_manager.working_folder is None
|
||||
try:
|
||||
yield specter
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import pytest
|
||||
import random
|
||||
import time
|
||||
|
||||
from cryptoadvance.specter.util.mnemonic import generate_mnemonic
|
||||
from cryptoadvance.specter.process_controller.node_controller import NodeController
|
||||
|
|
@ -15,16 +16,6 @@ mnemonic_zoo = (
|
|||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hot_wallet_device_1(specter_regtest_configured):
|
||||
return create_hot_wallet_device(specter_regtest_configured)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hot_wallet_device_2(specter_regtest_configured):
|
||||
return create_hot_wallet_device(specter_regtest_configured)
|
||||
|
||||
|
||||
def create_hot_wallet_device(
|
||||
specter_regtest_configured, name=None, mnemonic=None
|
||||
) -> Device:
|
||||
|
|
@ -58,6 +49,29 @@ def create_hot_wallet_device(
|
|||
return device
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hot_wallet_device_1(specter_regtest_configured):
|
||||
return create_hot_wallet_device(specter_regtest_configured)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hot_wallet_device_2(specter_regtest_configured):
|
||||
return create_hot_wallet_device(specter_regtest_configured)
|
||||
|
||||
|
||||
def create_hot_wallet_with_ID(
|
||||
specter_regtest_configured: Specter, hot_wallet_device_1, wallet_id
|
||||
) -> Wallet:
|
||||
device = hot_wallet_device_1
|
||||
wallet_manager = specter_regtest_configured.wallet_manager
|
||||
assert device.taproot_available(specter_regtest_configured.rpc)
|
||||
|
||||
# create the wallet
|
||||
keys = [key for key in device.keys if key.key_type == "wpkh"]
|
||||
source_wallet = wallet_manager.create_wallet(wallet_id, 1, "wpkh", keys, [device])
|
||||
return source_wallet
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def unfunded_hot_wallet_1(specter_regtest_configured, hot_wallet_device_1) -> Wallet:
|
||||
return create_hot_wallet_with_ID(
|
||||
|
|
@ -76,19 +90,6 @@ def unfunded_hot_wallet_2(specter_regtest_configured, hot_wallet_device_1) -> Wa
|
|||
)
|
||||
|
||||
|
||||
def create_hot_wallet_with_ID(
|
||||
specter_regtest_configured: Specter, hot_wallet_device_1, wallet_id
|
||||
) -> Wallet:
|
||||
device = hot_wallet_device_1
|
||||
wallet_manager = specter_regtest_configured.wallet_manager
|
||||
assert device.taproot_available(specter_regtest_configured.rpc)
|
||||
|
||||
# create the wallet
|
||||
keys = [key for key in device.keys if key.key_type == "wpkh"]
|
||||
source_wallet = wallet_manager.create_wallet(wallet_id, 1, "wpkh", keys, [device])
|
||||
return source_wallet
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def funded_hot_wallet_1(
|
||||
bitcoin_regtest: NodeController, unfunded_hot_wallet_1: Wallet
|
||||
|
|
@ -96,16 +97,15 @@ def funded_hot_wallet_1(
|
|||
funded_hot_wallet_1 = unfunded_hot_wallet_1
|
||||
assert len(funded_hot_wallet_1.txlist()) == 0
|
||||
for i in range(0, 10):
|
||||
bitcoin_regtest.testcoin_faucet(
|
||||
funded_hot_wallet_1.getnewaddress(), amount=random.randint(1, 4)
|
||||
)
|
||||
bitcoin_regtest.testcoin_faucet(funded_hot_wallet_1.getnewaddress(), amount=1)
|
||||
funded_hot_wallet_1.update()
|
||||
for i in range(0, 2):
|
||||
bitcoin_regtest.testcoin_faucet(
|
||||
funded_hot_wallet_1.getnewaddress(),
|
||||
amount=random.randint(1, 4),
|
||||
amount=2.5,
|
||||
confirm_payment=False,
|
||||
)
|
||||
time.sleep(0.5) # needed for tx to propagate
|
||||
funded_hot_wallet_1.update()
|
||||
# 12 txs
|
||||
assert len(funded_hot_wallet_1.txlist()) == 12
|
||||
|
|
|
|||
|
|
@ -1,146 +1,67 @@
|
|||
import json, logging, pytest, time, os
|
||||
from cryptoadvance.specter.specter import Specter
|
||||
from cryptoadvance.specter.wallet import Wallet
|
||||
from cryptoadvance.specter.managers.wallet_manager import WalletManager
|
||||
from conftest import instantiate_bitcoind_controller
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_import_address_labels(
|
||||
caplog, docker, request, devices_filled_data_folder, device_manager
|
||||
caplog, specter_regtest_configured: Specter, funded_hot_wallet_1: Wallet
|
||||
):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
wallet = funded_hot_wallet_1
|
||||
|
||||
# ==== Specter-specific: do custom setup ====
|
||||
# Instantiate a new bitcoind w/limited mempool. Use a different port to not interfere
|
||||
# with existing instance for other tests.
|
||||
bitcoind_controller = instantiate_bitcoind_controller(
|
||||
docker,
|
||||
request,
|
||||
rpcport=18968,
|
||||
extra_args=["-acceptnonstdtxn=1", "-maxmempool=5", "-spendzeroconfchange=0"],
|
||||
# the utxo is only available after the 100 mined blocks
|
||||
utxos = wallet.rpc.listunspent()
|
||||
logger.debug(f"these are the utxos: {utxos}.")
|
||||
# txid of the funding of test_address
|
||||
txid = utxos[0]["txid"]
|
||||
logger.debug(f"this is the txid: {txid}.")
|
||||
test_address = utxos[0]["address"]
|
||||
logger.debug(f"these are the addresses: {wallet.addresses}.")
|
||||
logger.debug(f"these are the _addresses: {wallet._addresses}.")
|
||||
assert wallet._addresses[test_address]["label"] is None
|
||||
number_of_addresses = len(wallet._addresses)
|
||||
|
||||
# Electrum
|
||||
# Test it with a txid label that does not belong to the wallet -> should be ignored
|
||||
wallet.import_address_labels(
|
||||
json.dumps(
|
||||
{
|
||||
"8d0958cb8701fac7421eb077e44b36809b90c7ad4a35e0c607c2cd591c522668": "txid label"
|
||||
}
|
||||
)
|
||||
)
|
||||
try:
|
||||
assert bitcoind_controller.get_rpc().test_connection()
|
||||
rpcconn = bitcoind_controller.rpcconn
|
||||
rpc = rpcconn.get_rpc()
|
||||
assert rpc is not None
|
||||
assert rpc.ipaddress != None
|
||||
assert wallet._addresses[test_address]["label"] is None
|
||||
assert len(wallet._addresses) == number_of_addresses
|
||||
|
||||
# Note: Our utxo creation is simpler than mempool_limit.py's approach since we're
|
||||
# running in regtest and can just use generatetoaddress().
|
||||
# Test it with an address label that does not belong to the wallet -> should be ignored
|
||||
wallet.import_address_labels(
|
||||
json.dumps({"12dRugNcdxK39288NjcDV4GX7rMsKCGn6B": "address label"})
|
||||
)
|
||||
assert wallet._addresses[test_address]["label"] is None
|
||||
assert len(wallet._addresses) == number_of_addresses
|
||||
|
||||
# Instantiate a new Specter instance to talk to this bitcoind
|
||||
config = {
|
||||
"rpc": {
|
||||
"autodetect": False,
|
||||
"datadir": "",
|
||||
"user": rpcconn.rpcuser,
|
||||
"password": rpcconn.rpcpassword,
|
||||
"port": rpcconn.rpcport,
|
||||
"host": rpcconn.ipaddress,
|
||||
"protocol": "http",
|
||||
},
|
||||
"auth": {
|
||||
"method": "rpcpasswordaspin",
|
||||
},
|
||||
}
|
||||
specter = Specter(data_folder=devices_filled_data_folder, config=config)
|
||||
specter.check()
|
||||
# Test it with a txid label
|
||||
wallet.import_address_labels(json.dumps({txid: "txid label"}))
|
||||
assert wallet._addresses[test_address]["label"] == "txid label"
|
||||
|
||||
# Largely copy-and-paste from test_wallet_manager.test_wallet_createpsbt.
|
||||
# TODO: Make a test fixture in conftest.py that sets up already funded wallets
|
||||
# for a bitcoin core hot wallet.
|
||||
wallet_manager = WalletManager(
|
||||
200100,
|
||||
devices_filled_data_folder,
|
||||
rpc,
|
||||
"regtest",
|
||||
device_manager,
|
||||
allow_threading=False,
|
||||
)
|
||||
# The txid label should now be replaced by the address label
|
||||
wallet.import_address_labels(json.dumps({test_address: "address label"}))
|
||||
assert wallet._addresses[test_address]["label"] == "address label"
|
||||
|
||||
# Create a new device that can sign psbts (Bitcoin Core hot wallet)
|
||||
device = device_manager.add_device(
|
||||
name="bitcoin_core_hot_wallet", device_type="bitcoincore", keys=[]
|
||||
)
|
||||
device.setup_device(file_password=None, wallet_manager=wallet_manager)
|
||||
device.add_hot_wallet_keys(
|
||||
mnemonic="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
passphrase="",
|
||||
paths=["m/49h/0h/0h"],
|
||||
file_password=None,
|
||||
wallet_manager=wallet_manager,
|
||||
testnet=True,
|
||||
keys_range=[0, 1000],
|
||||
keys_purposes=[],
|
||||
)
|
||||
# Specter JSON
|
||||
wallet._addresses[test_address].set_label("some_fancy_label_json")
|
||||
specter_json = json.dumps(wallet.to_json(for_export=True))
|
||||
wallet._addresses[test_address].set_label("label_got_lost")
|
||||
wallet.import_address_labels(specter_json)
|
||||
assert wallet._addresses[test_address]["label"] == "some_fancy_label_json"
|
||||
|
||||
wallet = wallet_manager.create_wallet(
|
||||
"bitcoincore_test_wallet", 1, "sh-wpkh", [device.keys[0]], [device]
|
||||
)
|
||||
|
||||
# Fund the wallet. Going to need a LOT of utxos to play with.
|
||||
logger.info("Generating utxos to wallet")
|
||||
test_address = wallet.getnewaddress() # 2NCSZrX49HHyzUy6oj8ggm9WD19hFvjzzou
|
||||
|
||||
wallet.rpc.generatetoaddress(1, test_address)[0]
|
||||
|
||||
# newly minted coins need 100 blocks to get spendable
|
||||
# let's mine another 100 blocks to get these coins spendable
|
||||
trash_address = wallet.getnewaddress()
|
||||
wallet.rpc.generatetoaddress(100, trash_address)
|
||||
|
||||
# the utxo is only available after the 100 mined blocks
|
||||
utxos = wallet.rpc.listunspent()
|
||||
# txid of the funding of test_address
|
||||
txid = utxos[0]["txid"]
|
||||
|
||||
assert wallet._addresses[test_address]["label"] is None
|
||||
number_of_addresses = len(wallet._addresses)
|
||||
|
||||
# Electrum
|
||||
# Test it with a txid label that does not belong to the wallet -> should be ignored
|
||||
wallet.import_address_labels(
|
||||
json.dumps(
|
||||
{
|
||||
"8d0958cb8701fac7421eb077e44b36809b90c7ad4a35e0c607c2cd591c522668": "txid label"
|
||||
}
|
||||
)
|
||||
)
|
||||
assert wallet._addresses[test_address]["label"] is None
|
||||
assert len(wallet._addresses) == number_of_addresses
|
||||
|
||||
# Test it with an address label that does not belong to the wallet -> should be ignored
|
||||
wallet.import_address_labels(
|
||||
json.dumps({"12dRugNcdxK39288NjcDV4GX7rMsKCGn6B": "address label"})
|
||||
)
|
||||
assert wallet._addresses[test_address]["label"] is None
|
||||
assert len(wallet._addresses) == number_of_addresses
|
||||
|
||||
# Test it with a txid label
|
||||
wallet.import_address_labels(json.dumps({txid: "txid label"}))
|
||||
assert wallet._addresses[test_address]["label"] == "txid label"
|
||||
|
||||
# The txid label should now be replaced by the address label
|
||||
wallet.import_address_labels(json.dumps({test_address: "address label"}))
|
||||
assert wallet._addresses[test_address]["label"] == "address label"
|
||||
|
||||
# Specter JSON
|
||||
wallet._addresses[test_address].set_label("some_fancy_label_json")
|
||||
specter_json = json.dumps(wallet.to_json(for_export=True))
|
||||
wallet._addresses[test_address].set_label("label_got_lost")
|
||||
wallet.import_address_labels(specter_json)
|
||||
assert wallet._addresses[test_address]["label"] == "some_fancy_label_json"
|
||||
|
||||
# Specter CSV
|
||||
csv_string = """Index,Address,Type,Label,Used,UTXO,Amount (BTC)
|
||||
0,2NCSZrX49HHyzUy6oj8ggm9WD19hFvjzzou,receive,some_fancy_label_csv,Yes,0,0"""
|
||||
wallet._addresses[test_address].set_label("label_got_lost")
|
||||
wallet.import_address_labels(csv_string)
|
||||
assert wallet._addresses[test_address]["label"] == "some_fancy_label_csv"
|
||||
|
||||
finally:
|
||||
# Clean up
|
||||
bitcoind_controller.stop_bitcoind()
|
||||
# Specter CSV
|
||||
csv_string = f"""Index,Address,Type,Label,Used,UTXO,Amount (BTC)
|
||||
0,{test_address},receive,some_fancy_label_csv,Yes,0,0"""
|
||||
wallet._addresses[test_address].set_label("label_got_lost")
|
||||
wallet.import_address_labels(csv_string)
|
||||
assert wallet._addresses[test_address]["label"] == "some_fancy_label_csv"
|
||||
|
|
|
|||
|
|
@ -16,178 +16,154 @@ from conftest import instantiate_bitcoind_controller
|
|||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_WalletManager(docker, request, devices_filled_data_folder, device_manager):
|
||||
# Instantiate a fresh bitcoind instance to isolate this test.
|
||||
bitcoind_controller = instantiate_bitcoind_controller(
|
||||
docker, request, rpcport=18998
|
||||
def test_WalletManager(
|
||||
docker, request, devices_filled_data_folder, device_manager, bitcoin_regtest
|
||||
):
|
||||
wm = WalletManager(
|
||||
200100,
|
||||
devices_filled_data_folder,
|
||||
bitcoin_regtest.get_rpc(),
|
||||
"regtest",
|
||||
device_manager,
|
||||
allow_threading=False,
|
||||
)
|
||||
try:
|
||||
wm = WalletManager(
|
||||
200100,
|
||||
devices_filled_data_folder,
|
||||
bitcoind_controller.get_rpc(),
|
||||
"regtest",
|
||||
device_manager,
|
||||
allow_threading=False,
|
||||
)
|
||||
# A wallet-creation needs a device
|
||||
device = device_manager.get_by_alias("trezor")
|
||||
assert device != None
|
||||
# Lets's create a wallet with the WalletManager
|
||||
wm.create_wallet("a_test_wallet", 1, "wpkh", [device.keys[5]], [device])
|
||||
# The wallet-name gets its filename and therefore its alias
|
||||
wallet = wm.wallets["a_test_wallet"]
|
||||
assert wallet != None
|
||||
assert wallet.balance["trusted"] == 0
|
||||
assert wallet.balance["untrusted_pending"] == 0
|
||||
# this is a sum of both
|
||||
assert wallet.fullbalance == 0
|
||||
address = wallet.getnewaddress()
|
||||
# newly minted coins need 100 blocks to get spendable
|
||||
wallet.rpc.generatetoaddress(1, address)
|
||||
# let's mine another 100 blocks to get these coins spendable
|
||||
random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r"
|
||||
wallet.rpc.generatetoaddress(100, random_address)
|
||||
# update the balance
|
||||
wallet.update_balance()
|
||||
assert wallet.fullbalance >= 25
|
||||
# A wallet-creation needs a device
|
||||
device = device_manager.get_by_alias("trezor")
|
||||
assert device != None
|
||||
# Lets's create a wallet with the WalletManager
|
||||
wm.create_wallet("a_test_wallet", 1, "wpkh", [device.keys[5]], [device])
|
||||
# The wallet-name gets its filename and therefore its alias
|
||||
wallet = wm.wallets["a_test_wallet"]
|
||||
assert wallet != None
|
||||
assert wallet.balance["trusted"] == 0
|
||||
assert wallet.balance["untrusted_pending"] == 0
|
||||
# this is a sum of both
|
||||
assert wallet.amount_total == 0
|
||||
address = wallet.getnewaddress()
|
||||
bitcoin_regtest.testcoin_faucet(address, amount=3)
|
||||
# update the balance
|
||||
wallet.update_balance()
|
||||
assert wallet.amount_total == 3
|
||||
assert wallet.amount_immature == 0
|
||||
|
||||
# You can create a multisig wallet with the wallet manager like this
|
||||
second_device = device_manager.get_by_alias("specter")
|
||||
multisig_wallet = wm.create_wallet(
|
||||
"a_multisig_test_wallet",
|
||||
1,
|
||||
"wsh",
|
||||
[device.keys[7], second_device.keys[0]],
|
||||
[device, second_device],
|
||||
)
|
||||
# You can create a multisig wallet with the wallet manager like this
|
||||
second_device = device_manager.get_by_alias("specter")
|
||||
multisig_wallet = wm.create_wallet(
|
||||
"a_multisig_test_wallet",
|
||||
1,
|
||||
"wsh",
|
||||
[device.keys[7], second_device.keys[0]],
|
||||
[device, second_device],
|
||||
)
|
||||
|
||||
assert len(wm.wallets) == 2
|
||||
assert multisig_wallet != None
|
||||
assert multisig_wallet.fullbalance == 0
|
||||
multisig_address = multisig_wallet.getnewaddress()
|
||||
multisig_wallet.rpc.generatetoaddress(1, multisig_address)
|
||||
multisig_wallet.rpc.generatetoaddress(100, random_address)
|
||||
# update balance
|
||||
multisig_wallet.update_balance()
|
||||
assert multisig_wallet.fullbalance >= 12.5
|
||||
# The WalletManager also has a `wallets_names` property, returning a sorted list of the names of all wallets
|
||||
assert wm.wallets_names == ["a_multisig_test_wallet", "a_test_wallet"]
|
||||
assert len(wm.wallets) == 2
|
||||
assert multisig_wallet != None
|
||||
assert multisig_wallet.amount_total == 0
|
||||
multisig_address = multisig_wallet.getnewaddress()
|
||||
bitcoin_regtest.testcoin_faucet(multisig_address, amount=4)
|
||||
# update balance
|
||||
multisig_wallet.update_balance()
|
||||
assert multisig_wallet.amount_total == 4
|
||||
# The WalletManager also has a `wallets_names` property, returning a sorted list of the names of all wallets
|
||||
assert wm.wallets_names == ["a_multisig_test_wallet", "a_test_wallet"]
|
||||
|
||||
# You can rename a wallet using the wallet manager using `rename_wallet`, passing the wallet object and the new name to assign to it
|
||||
wm.rename_wallet(multisig_wallet, "new_name_test_wallet")
|
||||
assert multisig_wallet.name == "new_name_test_wallet"
|
||||
assert wm.wallets_names == ["a_test_wallet", "new_name_test_wallet"]
|
||||
# You can rename a wallet using the wallet manager using `rename_wallet`, passing the wallet object and the new name to assign to it
|
||||
wm.rename_wallet(multisig_wallet, "new_name_test_wallet")
|
||||
assert multisig_wallet.name == "new_name_test_wallet"
|
||||
assert wm.wallets_names == ["a_test_wallet", "new_name_test_wallet"]
|
||||
|
||||
# you can also delete a wallet by passing it to the wallet manager's `delete_wallet` method
|
||||
# it will delete the json and attempt to remove it from Bitcoin Core
|
||||
wallet_fullpath = multisig_wallet.fullpath
|
||||
assert os.path.exists(wallet_fullpath)
|
||||
wm.delete_wallet(multisig_wallet)
|
||||
assert not os.path.exists(wallet_fullpath)
|
||||
assert len(wm.wallets) == 1
|
||||
finally:
|
||||
# cleanup
|
||||
bitcoind_controller.stop_bitcoind()
|
||||
# you can also delete a wallet by passing it to the wallet manager's `delete_wallet` method
|
||||
# it will delete the json and attempt to remove it from Bitcoin Core
|
||||
wallet_fullpath = multisig_wallet.fullpath
|
||||
assert os.path.exists(wallet_fullpath)
|
||||
wm.delete_wallet(multisig_wallet)
|
||||
assert not os.path.exists(wallet_fullpath)
|
||||
assert len(wm.wallets) == 1
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_wallet_createpsbt(docker, request, devices_filled_data_folder, device_manager):
|
||||
# Instantiate a fresh bitcoind instance to isolate this test.
|
||||
bitcoind_controller = instantiate_bitcoind_controller(
|
||||
docker, request, rpcport=18978
|
||||
def test_wallet_createpsbt(
|
||||
docker, request, devices_filled_data_folder, device_manager, bitcoin_regtest
|
||||
):
|
||||
wm = WalletManager(
|
||||
200100,
|
||||
devices_filled_data_folder,
|
||||
bitcoin_regtest.get_rpc(),
|
||||
"regtest",
|
||||
device_manager,
|
||||
allow_threading=False,
|
||||
)
|
||||
# A wallet-creation needs a device
|
||||
device = device_manager.get_by_alias("specter")
|
||||
key = Key.from_json(
|
||||
{
|
||||
"derivation": "m/48h/1h/0h/2h",
|
||||
"original": "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM",
|
||||
"fingerprint": "08686ac6",
|
||||
"type": "wsh",
|
||||
"xpub": "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL",
|
||||
}
|
||||
)
|
||||
wallet = wm.create_wallet("a_second_test_wallet", 1, "wpkh", [key], [device])
|
||||
# Let's fund the wallet
|
||||
address = wallet.getnewaddress()
|
||||
assert address == "bcrt1qtnrv2jpygx2ef3zqfjhqplnycxak2m6ljnhq6z"
|
||||
bitcoin_regtest.testcoin_faucet(address, amount=5)
|
||||
address = wallet.getnewaddress()
|
||||
bitcoin_regtest.testcoin_faucet(address, amount=10)
|
||||
address = wallet.getnewaddress()
|
||||
bitcoin_regtest.testcoin_faucet(address, amount=15)
|
||||
# update the wallet data
|
||||
wallet.update_balance()
|
||||
assert wallet.amount_total == 30
|
||||
unspents = wallet.rpc.listunspent(0)
|
||||
# Let's take 3 UTXO:
|
||||
selected_coins = [
|
||||
{"txid": u["txid"], "vout": u["vout"]}
|
||||
for u in [unspents[0], unspents[1], unspents[2]]
|
||||
]
|
||||
selected_coins_amount_sum = (
|
||||
unspents[0]["amount"] + unspents[1]["amount"] + unspents[2]["amount"]
|
||||
)
|
||||
number_of_coins_to_spend = (
|
||||
selected_coins_amount_sum - 0.1
|
||||
) # Let's spend almost all of them
|
||||
random_address = "bcrt1qwxhn49vkppudjcgrd4mylxczgjtuwayg5ckp5s"
|
||||
psbt = wallet.createpsbt(
|
||||
[random_address],
|
||||
[number_of_coins_to_spend],
|
||||
True,
|
||||
0,
|
||||
10,
|
||||
selected_coins=selected_coins,
|
||||
)
|
||||
assert len(psbt["tx"]["vin"]) == 3
|
||||
psbt_txs = [tx["txid"] for tx in psbt["tx"]["vin"]]
|
||||
for coin in selected_coins:
|
||||
assert coin["txid"] in psbt_txs
|
||||
|
||||
# Now let's spend more coins than we have selected. This should result in an exception:
|
||||
try:
|
||||
wm = WalletManager(
|
||||
200100,
|
||||
devices_filled_data_folder,
|
||||
bitcoind_controller.rpcconn.get_rpc(),
|
||||
"regtest",
|
||||
device_manager,
|
||||
allow_threading=False,
|
||||
)
|
||||
# A wallet-creation needs a device
|
||||
device = device_manager.get_by_alias("specter")
|
||||
key = Key.from_json(
|
||||
{
|
||||
"derivation": "m/48h/1h/0h/2h",
|
||||
"original": "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM",
|
||||
"fingerprint": "08686ac6",
|
||||
"type": "wsh",
|
||||
"xpub": "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL",
|
||||
}
|
||||
)
|
||||
wallet = wm.create_wallet("a_second_test_wallet", 1, "wpkh", [key], [device])
|
||||
# Let's fund the wallet with ... let's say 40 blocks a 50 coins each --> 200 coins
|
||||
address = wallet.getnewaddress()
|
||||
assert address == "bcrt1qtnrv2jpygx2ef3zqfjhqplnycxak2m6ljnhq6z"
|
||||
wallet.rpc.generatetoaddress(20, address)
|
||||
# in two addresses
|
||||
address = wallet.getnewaddress()
|
||||
wallet.rpc.generatetoaddress(20, address)
|
||||
# newly minted coins need 100 blocks to get spendable
|
||||
# let's mine another 100 blocks to get these coins spendable
|
||||
random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r"
|
||||
wallet.rpc.generatetoaddress(110, random_address)
|
||||
# update the wallet data
|
||||
wallet.update_balance()
|
||||
# Now we have loads of potential inputs
|
||||
# Let's spend 500 coins
|
||||
assert wallet.fullbalance >= 250
|
||||
# From this print-statement, let's grab some txids which we'll use for coinselect
|
||||
unspents = wallet.rpc.listunspent(0)
|
||||
# Lets take 3 more or less random txs from the unspents:
|
||||
selected_coins = [
|
||||
{"txid": u["txid"], "vout": u["vout"]}
|
||||
for u in [unspents[5], unspents[9], unspents[12]]
|
||||
]
|
||||
selected_coins_amount_sum = (
|
||||
unspents[5]["amount"] + unspents[9]["amount"] + unspents[12]["amount"]
|
||||
)
|
||||
number_of_coins_to_spend = (
|
||||
selected_coins_amount_sum - 0.1
|
||||
) # Let's spend almost all of them
|
||||
psbt = wallet.createpsbt(
|
||||
[random_address],
|
||||
[number_of_coins_to_spend],
|
||||
[number_of_coins_to_spend + 1],
|
||||
True,
|
||||
0,
|
||||
10,
|
||||
selected_coins=selected_coins,
|
||||
)
|
||||
assert len(psbt["tx"]["vin"]) == 3
|
||||
psbt_txs = [tx["txid"] for tx in psbt["tx"]["vin"]]
|
||||
for coin in selected_coins:
|
||||
assert coin["txid"] in psbt_txs
|
||||
assert False, "should throw an exception!"
|
||||
except SpecterError as e:
|
||||
pass
|
||||
|
||||
# Now let's spend more coins than we have selected. This should result in an exception:
|
||||
try:
|
||||
psbt = wallet.createpsbt(
|
||||
[random_address],
|
||||
[number_of_coins_to_spend + 1],
|
||||
True,
|
||||
0,
|
||||
10,
|
||||
selected_coins=selected_coins,
|
||||
)
|
||||
assert False, "should throw an exception!"
|
||||
except SpecterError as e:
|
||||
pass
|
||||
assert wallet.amount_locked_unsigned == selected_coins_amount_sum
|
||||
assert len(wallet.rpc.listlockunspent()) == 3
|
||||
assert wallet.amount_available == wallet.amount_total - selected_coins_amount_sum
|
||||
|
||||
assert wallet.locked_amount == selected_coins_amount_sum
|
||||
assert len(wallet.rpc.listlockunspent()) == 3
|
||||
assert (
|
||||
wallet.full_available_balance
|
||||
== wallet.fullbalance - selected_coins_amount_sum
|
||||
)
|
||||
|
||||
wallet.delete_pending_psbt(psbt["tx"]["txid"])
|
||||
assert wallet.locked_amount == 0
|
||||
assert len(wallet.rpc.listlockunspent()) == 0
|
||||
assert wallet.full_available_balance == wallet.fullbalance
|
||||
finally:
|
||||
# cleanup
|
||||
bitcoind_controller.stop_bitcoind()
|
||||
wallet.delete_pending_psbt(psbt["tx"]["txid"])
|
||||
assert wallet.amount_locked_unsigned == 0
|
||||
assert len(wallet.rpc.listlockunspent()) == 0
|
||||
assert wallet.amount_total == wallet.amount_available
|
||||
|
||||
|
||||
def test_WalletManager_check_duplicate_keys(empty_data_folder):
|
||||
|
|
@ -335,26 +311,33 @@ def test_wallet_labeling(bitcoin_regtest, devices_filled_data_folder, device_man
|
|||
)
|
||||
wallet = wm.create_wallet("a_second_test_wallet", 1, "wpkh", [key], [device])
|
||||
|
||||
address = wallet.address
|
||||
assert wallet.getlabel(address) == "Address #0"
|
||||
wallet.setlabel(address, "Random label")
|
||||
assert wallet.getlabel(address) == "Random label"
|
||||
first_address = wallet.address
|
||||
assert wallet.getlabel(first_address) == "Address #0"
|
||||
wallet.setlabel(first_address, "Random label")
|
||||
assert wallet.getlabel(first_address) == "Random label"
|
||||
|
||||
wallet.rpc.generatetoaddress(20, address)
|
||||
second_address = wallet.getnewaddress()
|
||||
wallet.setlabel(second_address, "")
|
||||
bitcoin_regtest.testcoin_faucet(second_address, amount=0.6)
|
||||
|
||||
random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r"
|
||||
wallet.rpc.generatetoaddress(100, random_address)
|
||||
third_address = wallet.getnewaddress()
|
||||
wallet.setlabel(third_address, "")
|
||||
bitcoin_regtest.testcoin_faucet(third_address, amount=0.4)
|
||||
|
||||
assert sorted(wallet.addresses) == sorted(
|
||||
[first_address, second_address, third_address]
|
||||
)
|
||||
|
||||
# Make 20 UTXO
|
||||
for i in range(0, 20):
|
||||
_address = wallet.getnewaddress()
|
||||
bitcoin_regtest.testcoin_faucet(_address, amount=0.1)
|
||||
|
||||
# update utxo
|
||||
wallet.getdata()
|
||||
# update balance
|
||||
wallet.update_balance()
|
||||
assert len(wallet.full_utxo) == 22
|
||||
|
||||
address_balance = wallet.fullbalance
|
||||
assert len(wallet.full_utxo) == 20
|
||||
|
||||
print(wallet.full_utxo[4])
|
||||
# Something like:
|
||||
# An entry in full_utxo looks something like this:
|
||||
# { 'txid': 'fab823558781745179916b4bfdfd65b382bfc0e70e85188f1b9538604202f537',
|
||||
# 'vout': 0, 'address': 'bcrt1qmlrraffw0evkjy2yrxmt263ksgfgv2gqhcddrt',
|
||||
# 'label': 'Random label', 'scriptPubKey': '0014dfc63ea52e7e5969114419b6b56a368212862900',
|
||||
|
|
@ -363,24 +346,6 @@ def test_wallet_labeling(bitcoin_regtest, devices_filled_data_folder, device_man
|
|||
# 'safe': True, 'time': 1637091575, 'category': 'generate', 'locked': False
|
||||
# }
|
||||
|
||||
new_address = wallet.getnewaddress()
|
||||
wallet.setlabel(new_address, "")
|
||||
wallet.rpc.generatetoaddress(20, new_address)
|
||||
|
||||
random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r"
|
||||
wallet.rpc.generatetoaddress(100, random_address)
|
||||
|
||||
wallet.getdata()
|
||||
wallet.update_balance()
|
||||
|
||||
assert len(wallet.full_utxo) == 40
|
||||
|
||||
wallet.setlabel(new_address, "")
|
||||
third_address = wallet.getnewaddress()
|
||||
|
||||
wallet.getdata()
|
||||
assert sorted(wallet.addresses) == sorted([address, new_address, third_address])
|
||||
|
||||
|
||||
def test_wallet_change_addresses(
|
||||
bitcoin_regtest, devices_filled_data_folder, device_manager
|
||||
|
|
@ -410,11 +375,9 @@ def test_wallet_change_addresses(
|
|||
change_address = wallet.change_address
|
||||
assert wallet.addresses == [address]
|
||||
assert wallet.change_addresses == [change_address]
|
||||
|
||||
wallet.rpc.generatetoaddress(20, change_address)
|
||||
random_address = "mruae2834buqxk77oaVpephnA5ZAxNNJ1r"
|
||||
wallet.rpc.generatetoaddress(110, random_address)
|
||||
wallet.getdata()
|
||||
bitcoin_regtest.testcoin_faucet(change_address, amount=0.1)
|
||||
wallet.update_balance()
|
||||
assert wallet.amount_total == 0.1
|
||||
|
||||
# new change address should be genrated automatically after receiving
|
||||
# assert wallet.change_addresses == [change_address, wallet.change_address]
|
||||
|
|
@ -442,20 +405,15 @@ def test_singlesig_wallet_backup_and_restore(caplog, specter_regtest_configured)
|
|||
|
||||
# create a wallet
|
||||
wallet = wallet_manager.create_wallet(
|
||||
name="my_test_wallet",
|
||||
name="my_singlesig_test_wallet",
|
||||
sigs_required=1,
|
||||
key_type=key.key_type,
|
||||
keys=[key],
|
||||
devices=[device],
|
||||
)
|
||||
|
||||
# Fund the wallet
|
||||
address = wallet.getnewaddress()
|
||||
wallet.rpc.generatetoaddress(101, address)
|
||||
|
||||
# update the wallet data
|
||||
balance = wallet.update_balance()
|
||||
assert balance["trusted"] > 0.0
|
||||
# Wallet was prefunded in specter_regtest_configured fixture, not sure, though, why the amount is 23 and not 20 ...
|
||||
amount = wallet.amount_total
|
||||
|
||||
# Save the json backup
|
||||
wallet_backup = json.loads(wallet.account_map)
|
||||
|
|
@ -512,7 +470,8 @@ def test_singlesig_wallet_backup_and_restore(caplog, specter_regtest_configured)
|
|||
wallet.rpc.rescanblockchain(0)
|
||||
|
||||
# We restored the wallet's utxos
|
||||
assert wallet.update_balance()["trusted"] > 0.0
|
||||
wallet.update_balance()
|
||||
assert wallet.amount_total == amount
|
||||
|
||||
# Now do it again, but without the newer "devices" attr
|
||||
del wallet_backup["devices"]
|
||||
|
|
@ -567,10 +526,13 @@ def test_singlesig_wallet_backup_and_restore(caplog, specter_regtest_configured)
|
|||
wallet.rpc.rescanblockchain(0)
|
||||
|
||||
# We restored the wallet's utxos
|
||||
assert wallet.update_balance()["trusted"] > 0.0
|
||||
wallet.update_balance()
|
||||
assert wallet.amount_total == amount
|
||||
|
||||
|
||||
def test_multisig_wallet_backup_and_restore(caplog, specter_regtest_configured):
|
||||
def test_multisig_wallet_backup_and_restore(
|
||||
bitcoin_regtest, caplog, specter_regtest_configured
|
||||
):
|
||||
"""
|
||||
Multisig wallets should be able to be backed up and re-imported
|
||||
with or without the "devices" attr in the json backup.
|
||||
|
|
@ -620,20 +582,18 @@ def test_multisig_wallet_backup_and_restore(caplog, specter_regtest_configured):
|
|||
|
||||
# create the multisig wallet
|
||||
wallet = wallet_manager.create_wallet(
|
||||
name="my_test_wallet",
|
||||
name="my_multisig_test_wallet",
|
||||
sigs_required=2,
|
||||
key_type=key.key_type,
|
||||
keys=[key, hot_wallet_1_device.keys[0], hot_wallet_2_device.keys[0]],
|
||||
devices=[device, hot_wallet_1_device, hot_wallet_2_device],
|
||||
)
|
||||
|
||||
# Fund the wallet
|
||||
# Wallet is unfunded
|
||||
address = wallet.getnewaddress()
|
||||
wallet.rpc.generatetoaddress(101, address)
|
||||
|
||||
# update the wallet data
|
||||
balance = wallet.update_balance()
|
||||
assert balance["trusted"] > 0.0
|
||||
bitcoin_regtest.testcoin_faucet(address, amount=3.3)
|
||||
wallet.update_balance()
|
||||
assert wallet.amount_total == 3.3
|
||||
|
||||
# Save the json backup
|
||||
wallet_backup = json.loads(wallet.account_map.replace("\\\\", "").replace("'", "h"))
|
||||
|
|
@ -694,7 +654,7 @@ def test_multisig_wallet_backup_and_restore(caplog, specter_regtest_configured):
|
|||
wallet.rpc.rescanblockchain(0)
|
||||
|
||||
# We restored the wallet's utxos
|
||||
assert wallet.update_balance()["trusted"] > 0.0
|
||||
assert wallet.amount_total == 3.3
|
||||
|
||||
# Now do it again, but without the newer "devices" attr
|
||||
del wallet_backup["devices"]
|
||||
|
|
@ -754,11 +714,11 @@ def test_multisig_wallet_backup_and_restore(caplog, specter_regtest_configured):
|
|||
wallet.rpc.rescanblockchain(0)
|
||||
|
||||
# We restored the wallet's utxos
|
||||
assert wallet.update_balance()["trusted"] > 0.0
|
||||
assert wallet.amount_total == 3.3
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_taproot_wallet(caplog, specter_regtest_configured):
|
||||
def test_taproot_wallet(bitcoin_regtest, caplog, specter_regtest_configured):
|
||||
"""
|
||||
Taproot m/86h/ derivation path xpubs should be able to create wallets and
|
||||
receive funds
|
||||
|
|
@ -784,7 +744,7 @@ def test_taproot_wallet(caplog, specter_regtest_configured):
|
|||
)
|
||||
|
||||
taproot_wallet = wallet_manager.create_wallet(
|
||||
name="my_test_wallet",
|
||||
name="my_taproot_test_wallet",
|
||||
sigs_required=1,
|
||||
key_type=taproot_device.keys[0].key_type,
|
||||
keys=[taproot_device.keys[0]],
|
||||
|
|
@ -793,12 +753,9 @@ def test_taproot_wallet(caplog, specter_regtest_configured):
|
|||
|
||||
# Fund the wallet
|
||||
address = taproot_wallet.getnewaddress()
|
||||
bitcoin_regtest.testcoin_faucet(address, amount=2.2)
|
||||
taproot_wallet.update_balance()
|
||||
assert taproot_wallet.amount_total == 2.2
|
||||
|
||||
# Taproot test addrs are bcrt1p vs native segwit bcrt1q
|
||||
assert address.startswith("bcrt1p")
|
||||
|
||||
taproot_wallet.rpc.generatetoaddress(101, address)
|
||||
|
||||
# update the wallet data
|
||||
balance = taproot_wallet.update_balance()
|
||||
assert balance["trusted"] > 0.0
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
import pytest
|
||||
import json
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
from numbers import Number
|
||||
|
||||
import pytest
|
||||
from cryptoadvance.specter.managers.device_manager import DeviceManager
|
||||
from cryptoadvance.specter.specter import Specter
|
||||
from cryptoadvance.specter.specter_error import SpecterError
|
||||
from cryptoadvance.specter.util.wallet_importer import WalletImporter
|
||||
|
||||
from devices_and_wallets import create_hot_wallet_with_ID
|
||||
|
||||
|
||||
def almost_equal(a: Number, b: Number, precision: float = 0.01) -> bool:
|
||||
"""
|
||||
|
|
@ -16,7 +23,8 @@ def almost_equal(a: Number, b: Number, precision: float = 0.01) -> bool:
|
|||
return (diff < precision) and (diff > -precision)
|
||||
|
||||
|
||||
def test_rr_psbt_get(client, caplog):
|
||||
def test_rr_psbt_get(client, specter_regtest_configured, bitcoin_regtest, caplog):
|
||||
create_a_simple_wallet(specter_regtest_configured, bitcoin_regtest)
|
||||
caplog.set_level(logging.DEBUG)
|
||||
""" testing the registration """
|
||||
# Unauthorized
|
||||
|
|
@ -70,7 +78,8 @@ def test_rr_psbt_get(client, caplog):
|
|||
assert data["result"] == {}
|
||||
|
||||
|
||||
def test_rr_psbt_post(specter_regtest_configured, client, caplog):
|
||||
def test_rr_psbt_post(specter_regtest_configured, bitcoin_regtest, client, caplog):
|
||||
create_a_simple_wallet(specter_regtest_configured, bitcoin_regtest)
|
||||
caplog.set_level(logging.DEBUG)
|
||||
""" testing the registration """
|
||||
|
||||
|
|
@ -142,3 +151,37 @@ def test_rr_psbt_post(specter_regtest_configured, client, caplog):
|
|||
assert data["result"]["base64"]
|
||||
assert data["result"]["time"]
|
||||
assert data["result"]["sigs_count"] == 0
|
||||
|
||||
|
||||
def create_a_simple_wallet(specter: Specter, bitcoin_regtest):
|
||||
"""ToDo: Could potentially do this with a a fixture but this is only relevant for this file only"""
|
||||
someuser = specter.user_manager.get_user_by_username("someuser")
|
||||
assert not someuser.wallet_manager.working_folder is None
|
||||
# Create a Wallet
|
||||
wallet_json = '{"label": "a_simple_wallet", "blockheight": 0, "descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/0/*)#xp8lv5nr", "devices": [{"type": "trezor", "label": "trezor"}]} '
|
||||
wallet_importer = WalletImporter(
|
||||
wallet_json, specter, device_manager=someuser.device_manager
|
||||
)
|
||||
wallet_importer.create_nonexisting_signers(
|
||||
someuser.device_manager,
|
||||
{"unknown_cosigner_0_name": "trezor", "unknown_cosigner_0_type": "trezor"},
|
||||
)
|
||||
dm: DeviceManager = someuser.device_manager
|
||||
wallet = wallet_importer.create_wallet(someuser.wallet_manager)
|
||||
try:
|
||||
# fund it with some coins
|
||||
bitcoin_regtest.testcoin_faucet(address=wallet.getnewaddress())
|
||||
# make sure it's confirmed
|
||||
bitcoin_regtest.mine()
|
||||
# Realize that the wallet has funds:
|
||||
wallet.update()
|
||||
except SpecterError as se:
|
||||
if str(se).startswith("Timeout"):
|
||||
pytest.fail(
|
||||
"We got a Bitcoin-RPC timeout while setting up the test, minting some coins. Test Error! Check cpu/mem utilastion and btc/elem logs!"
|
||||
)
|
||||
return
|
||||
else:
|
||||
raise se
|
||||
|
||||
assert wallet.fullbalance >= 20
|
||||
|
|
|
|||
|
|
@ -7,92 +7,49 @@ from conftest import instantiate_bitcoind_controller
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def test_import_address_labels(caplog, specter_regtest_configured):
|
||||
caplog.set_level(logging.INFO)
|
||||
|
||||
specter = specter_regtest_configured
|
||||
# Create a new device that can sign psbts (Bitcoin Core hot wallet)
|
||||
device = specter.device_manager.add_device(
|
||||
name="bitcoin_core_hot_wallet", device_type="bitcoincore", keys=[]
|
||||
)
|
||||
device.setup_device(file_password=None, wallet_manager=specter.wallet_manager)
|
||||
device.add_hot_wallet_keys(
|
||||
mnemonic="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
passphrase="",
|
||||
paths=["m/49h/0h/0h"],
|
||||
file_password=None,
|
||||
wallet_manager=specter.wallet_manager,
|
||||
testnet=True,
|
||||
keys_range=[0, 1000],
|
||||
keys_purposes=[],
|
||||
)
|
||||
|
||||
wallet: Wallet = specter.wallet_manager.create_wallet(
|
||||
"bitcoincore_test_wallet", 1, "sh-wpkh", [device.keys[0]], [device]
|
||||
)
|
||||
|
||||
# Fund the wallet. Going to need a LOT of utxos to play with.
|
||||
logger.info("Generating utxos to wallet")
|
||||
test_address = wallet.getnewaddress() # 2NCSZrX49HHyzUy6oj8ggm9WD19hFvjzzou
|
||||
|
||||
wallet.rpc.generatetoaddress(1, test_address)[0]
|
||||
|
||||
# newly minted coins need 100 blocks to get spendable
|
||||
# let's mine another 100 blocks to get these coins spendable
|
||||
trash_address = wallet.getnewaddress()
|
||||
wallet.rpc.generatetoaddress(100, trash_address)
|
||||
|
||||
# the utxo is only available after the 100 mined blocks
|
||||
utxos = wallet.rpc.listunspent()
|
||||
# txid of the funding of test_address
|
||||
txid = utxos[0]["txid"]
|
||||
|
||||
assert wallet._addresses[test_address]["label"] is None
|
||||
number_of_addresses = len(wallet._addresses)
|
||||
|
||||
assert wallet.txlist()[0]["blockheight"] != None
|
||||
assert wallet.txlist()[0]["blocktime"] != None
|
||||
|
||||
|
||||
def test_check_utxo(specter_regtest_configured: Specter, funded_hot_wallet_1: Wallet):
|
||||
def test_check_utxo_and_amounts(
|
||||
specter_regtest_configured: Specter, funded_hot_wallet_1: Wallet
|
||||
):
|
||||
wl = funded_hot_wallet_1
|
||||
|
||||
# Let's first prepare some locked txids
|
||||
unspent_list_orig = wl.rpc.listunspent() # to be able to ref in stable way
|
||||
|
||||
wl.check_utxo()
|
||||
# 10 transactions + 2 unconfirmed
|
||||
assert len(wl.full_utxo) == 12
|
||||
for tx in wl.full_utxo:
|
||||
print(f"txid: {tx['txid']}")
|
||||
# non are locked
|
||||
# none are locked
|
||||
assert len([tx for tx in wl.full_utxo if tx["locked"]]) == 0
|
||||
|
||||
unspent_list = wl.rpc.listunspent()
|
||||
print(f"unspent_list: {len(unspent_list)}")
|
||||
print([tx["txid"] for tx in unspent_list])
|
||||
|
||||
print(f"\n\nLocking this one: {unspent_list_orig[0]['txid']}")
|
||||
print(wl.rpc.gettransaction(unspent_list_orig[0]["txid"]))
|
||||
wl.rpc.lockunspent(
|
||||
False,
|
||||
[{"txid": unspent_list_orig[0]["txid"], "vout": unspent_list_orig[0]["vout"]}],
|
||||
# Freeze 2 UTXO
|
||||
# ["txid:vout", "txid:vout"]
|
||||
wl.toggle_freeze_utxo(
|
||||
[
|
||||
f"{unspent_list_orig[0]['txid']}:{unspent_list_orig[0]['vout']}",
|
||||
f"{unspent_list_orig[1]['txid']}:{unspent_list_orig[1]['vout']}",
|
||||
]
|
||||
)
|
||||
print(f"\n\nLocking this one: {unspent_list_orig[1]['txid']}")
|
||||
print(wl.rpc.gettransaction(unspent_list_orig[1]["txid"]))
|
||||
wl.rpc.lockunspent(
|
||||
False,
|
||||
[{"txid": unspent_list_orig[1]["txid"], "vout": unspent_list_orig[1]["vout"]}],
|
||||
)
|
||||
|
||||
unspent_list = wl.rpc.listunspent()
|
||||
print(f"\n\nunspent_list: {len(unspent_list)}")
|
||||
print([tx["txid"] for tx in unspent_list])
|
||||
|
||||
# still 12 utxo with 2 unconfirmed
|
||||
wl.check_utxo()
|
||||
# still 2 transactions + 2 unconfirmed
|
||||
for tx in wl.full_utxo:
|
||||
print(f"txid: {tx['txid']}")
|
||||
assert len(wl.full_utxo) == 12
|
||||
# 2 are locked
|
||||
assert len([tx for tx in wl.full_utxo if tx["locked"]]) == 2
|
||||
|
||||
# Check total amount
|
||||
assert wl.amount_total == 15
|
||||
# Check confirmed amount
|
||||
assert wl.amount_confirmed == 10
|
||||
# Check unconfirmed amount
|
||||
assert wl.amount_unconfirmed == 5
|
||||
# Check frozen amount
|
||||
assert wl.amount_frozen == 2
|
||||
# Check available amount
|
||||
assert wl.amount_available == 13 # total - frozen
|
||||
# Check immature amount
|
||||
address = wl.getnewaddress()
|
||||
wl.rpc.generatetoaddress(1, address)
|
||||
wl.update_balance()
|
||||
assert (
|
||||
round(wl.amount_immature, 1) == 50
|
||||
or round(wl.amount_immature, 1) == 25
|
||||
or round(wl.amount_immature, 1) == 12.5
|
||||
or round(wl.amount_immature, 2) == 6.25
|
||||
)
|
||||
# amount_locked_unsigned is tested in test_wallet_createpsbt (in test_managers_wallet.py)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue