diff --git a/.cirrus.yml b/.cirrus.yml index 702af2e55..6b8a7c1dc 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -70,7 +70,7 @@ test_task: cypress_test_task: container: - image: registry.gitlab.com/cryptoadvance/specter-desktop/cypress-python:v9.5.4 + image: registry.gitlab.com/cryptoadvance/specter-desktop/cypress-python:v9.7.0 cpu: 6 memory: 6G pre_prep_script: diff --git a/.gitignore b/.gitignore index 47f6b0d8b..5d7ed114e 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ tests/elements signing_dir site docs/README.md +cypresstest-output.xml diff --git a/cypress.json b/cypress.json index 4bf293805..162793692 100644 --- a/cypress.json +++ b/cypress.json @@ -25,5 +25,4 @@ "mochaFile": "cypresstest-output.xml", "toConsole": true } - } diff --git a/cypress/integration/spec_elm_single_segwit_wallet.js b/cypress/integration/spec_elm_single_segwit_wallet.js index 128456a0a..653b3c873 100644 --- a/cypress/integration/spec_elm_single_segwit_wallet.js +++ b/cypress/integration/spec_elm_single_segwit_wallet.js @@ -37,11 +37,6 @@ describe('Operating with an elements singlesig wallet', () => { 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('send unconfidential transaction from segwit', () => { @@ -83,11 +78,6 @@ describe('Operating with an elements singlesig wallet', () => { 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('send unconfidential transaction from nested segwit', () => { diff --git a/cypress/integration/spec_ghost_machine.js b/cypress/integration/spec_ghost_machine.js index e9c39693d..25dadeb16 100644 --- a/cypress/integration/spec_ghost_machine.js +++ b/cypress/integration/spec_ghost_machine.js @@ -3,6 +3,6 @@ describe('Ghost machine', () => { cy.viewport(1200,660) cy.visit('/') cy.addDevice('DIY ghost', 'Specter-DIY', 'ghost_machine') - cy.addWallet('Ghost wallet', null, 'segwit', 'funded', 'btc') + cy.addWallet('Ghost wallet', 'segwit', 'funded', 'btc', 'singlesig', 'DIY ghost') }) }) diff --git a/cypress/integration/spec_rescan.js b/cypress/integration/spec_rescan.js index 0976949e1..0b07945bf 100644 --- a/cypress/integration/spec_rescan.js +++ b/cypress/integration/spec_rescan.js @@ -13,7 +13,7 @@ describe('Test the UI related to a blockchain rescan', () => { it('Go to the rescan section from a fresh wallet', () => { // Create a completely fresh wallet which is not receiving funds from the continous mining cy.addDevice('Trezor hold', 'Trezor', 'hold_accident') - cy.addWallet('Fresh wallet', 'Trezor hold', 'segwit', false) + cy.addWallet('Fresh wallet', 'segwit', false, 'btc', 'singlesig', 'Trezor hold') cy.get('#btn_transactions').click() cy.get('#go-to-rescan-btn').click() cy.get('#blockchain-rescan').should('be.visible') diff --git a/cypress/integration/spec_wallet_send.js b/cypress/integration/spec_wallet_send.js index b3be62541..26ff1e0b8 100644 --- a/cypress/integration/spec_wallet_send.js +++ b/cypress/integration/spec_wallet_send.js @@ -11,33 +11,13 @@ describe('Test sending transactions', () => { }) it('Send a standard transaction', () => { - // empty so far cy.addHotDevice("Hot Device 1","bitcoin") - cy.get('body').then(($body) => { - if ($body.text().includes('Test Hot Wallet 1')) { - cy.get('#wallets_list > .item > svg').click() - cy.get(':nth-child(6) > .right').click() - cy.get('#advanced_settings_tab_btn').click() - cy.get('.card > :nth-child(9) > .btn').click() - } - }) - cy.get('#btn_new_wallet').click() - cy.get('[href="./simple/"]').click() - cy.get('#hot_device_1').click() - cy.get('#wallet_name').type("Test Hot Wallet 1") - cy.get('#keysform > .centered').click() - cy.get('body').contains("New wallet was created successfully!") - // Download PDF - // unfortunately this results in weird effects in cypress run - //cy.get('#pdf-wallet-download > img').click() - cy.get('#btn_continue').click() - //get some funds - cy.mine2wallet("btc") - + cy.addWallet('Test Hot Wallet 1', 'segwit', 'funded', 'btc', 'singlesig', 'Hot Device 1') + cy.selectWallet("Test Hot Wallet 1") cy.get('#btn_send').click() - cy.get('#address_0').type("bcrt1qsj30deg0fgzckvlrn5757yk55yajqv6dqx0x7u") - cy.get('#label_0').type("Burn address") - cy.get('#send_max_0').click() + cy.get('#recipient_0').find('#address').type("bcrt1qsj30deg0fgzckvlrn5757yk55yajqv6dqx0x7u", { force: true }) + cy.get('#recipient_0').find('#label').type("Burn address", { force: true }) + cy.get('#recipient_0').get('#send_max').click() cy.get('#create_psbt_btn').click() cy.get('body').contains("Paste signed transaction") cy.get('#hot_device_1_tx_sign_btn').click() @@ -50,48 +30,129 @@ describe('Test sending transactions', () => { expect(n).to.be.equals(0) }) }) - - it('Create a transaction with multiple recipients', () => { + + it('Adding and deleting recipients', () => { // We need new sats but mine2wallet only works if a wallet is selected cy.selectWallet("Test Hot Wallet 1") cy.mine2wallet("btc") cy.get('#btn_send').click() - /// The addresses are the first three from DIY ghost - cy.get('#address_0').type("bcrt1qvtdx75y4554ngrq6aff3xdqnvjhmct5wck95qs") - cy.get('#label_0').type("Recipient 1") - cy.get('#amount_0').type(10) + + // The addresses are the first 5 from DIY ghost + cy.get('#recipient_0').find('#address').invoke('val', "bcrt1qvtdx75y4554ngrq6aff3xdqnvjhmct5wck95qs") // will be deleted, so address doesnt matter + cy.get('#recipient_0').find('#label').type("Recipient 1 to be deleted", { force: true }) + cy.get('#recipient_0').find('#amount').type(1, { force: true }) + cy.get('main').scrollTo('bottom') + + // Adding 4 more recipients + cy.get('#add-recipient').click() + cy.get('#recipient_1').find('#address').invoke('val', "bcrt1qgzmq6e3tn67kveryf2je6nd3nv4txef4sl8wre") // pasting the address is faster than typing + cy.get('#recipient_1').find('#label').type("Recipient 2", { force: true }) + cy.get('#recipient_1').find('#amount').type(2, { force: true }) + cy.get('main').scrollTo('bottom') + + cy.get('#add-recipient').click() + cy.get('#recipient_2').find('#address').invoke('val', "bcrt1q9mkrhmxcn7rslzfv6lke8859m7ntwudfjqmcx7") // will be deleted, so address doesnt matter + cy.get('#recipient_2').find('#label').type("Recipient 3 to be deleted", { force: true }) + cy.get('#recipient_2').find('#amount').type(3, { force: true }) + + cy.get('#add-recipient').click() + cy.get('#recipient_3').find('#address').invoke('val', "bcrt1q4gs9fsf8fh4s4s8w39hxtupafm2q047fytmnxp") // pasting the address is faster than typing + cy.get('#recipient_3').find('#label').type("Recipient 4", { force: true }) + cy.get('#recipient_3').find('#amount').type(4, { force: true }) + + cy.get('#add-recipient').click() + cy.get('#recipient_4').find('#address').invoke('val', "bcrt1q4e8p7x6n7uhtthtelhv3mle52vsc4pqre7ddwm") // pasting the address is faster than typing + cy.get('#recipient_4').find('#label').type("Recipient 5", { force: true }) + cy.get('#recipient_4').find('#amount').type(5, { force: true }) + cy.get('main').scrollTo('bottom') + + // Check the fee selection cy.get('#toggle_advanced').click() - cy.get('main').scrollTo('bottom') - cy.get('#add-recipient').click() - cy.get('#address_1').type("bcrt1qgzmq6e3tn67kveryf2je6nd3nv4txef4sl8wre") - cy.get('#label_1').type("Recipient 2") - cy.get('#amount_1').type(5) - cy.get('main').scrollTo('bottom') - cy.get('#add-recipient').click() - cy.get('#address_2').type("bcrt1q9mkrhmxcn7rslzfv6lke8859m7ntwudfjqmcx7") - cy.get('#label_2').type("Recipient 3") - cy.get('#send_max_2').click() - cy.get('main').scrollTo('bottom') - // Shadow DOM - // Check whether the subtract fees box is ticked + cy.get('#fee-selection-component').find('.fee_container').find('input#subtract').click() cy.get('#fee-selection-component').find('.fee_container').find('input#subtract').invoke('prop', 'checked').should('eq', true) - // Check whether the recipient number input field is visible (shadow DOM) + + // Check whether the recipient number select field is visible cy.get('#fee-selection-component').find('.fee_container').find('span#subtract_from').should('be.visible') - // Light DOM + // Check the values of the hidden inputs in the light DOM which are used for the form // Note: Despite identical ids the hidden inputs seem to be fetched first since they are higher up in the DOM cy.get('#fee-selection-component').find('#subtract').invoke('attr', 'value').should('eq', 'true') - // Send max was applied to the third recipient, so the value should be 3 - cy.get('#fee-selection-component').find('#subtract_from_input').invoke('attr', 'value').should('eq', '3') - // Change recipient number to 2 - // Note: No easy way to increment / decrement by clicking, see: https://stackoverflow.com/questions/47180137/incrementing-and-decrementing-the-value-of-an-input-type-number-in-cypress - cy.get('#fee-selection-component').find('.fee_container').find('#subtract_from_input').clear() - cy.get('#fee-selection-component').find('.fee_container').find('#subtract_from_input').click().type('2{enter}') - cy.get('#fee-selection-component').find('#subtract_from_input').invoke('attr', 'value').should('eq', '2') + // Remove two recipients + cy.get('#recipient_0').find('#remove').click({ force: true }) + cy.get('#recipient_2').find('#remove').click({ force: true }) - // Change it back to recipient 3 - cy.get('#send_max_2').click() + // Select different recipients to subtract the fees from + cy.get('#fee-selection-component').find('.fee_container').find('#subtract_from_recipient_id_select').select('Recipient 4') // html select with cypress: https://www.cypress.io/blog/2020/03/20/working-with-select-elements-and-select2-widgets-in-cypress/ + cy.get('#fee-selection-component').find('.fee_container').find('#subtract_from_recipient_id_select').select('Recipient 5') + cy.get('#fee-selection-component').find('.fee_container').find('#subtract_from_recipient_id_select').select('Recipient 2') + cy.get('#fee-selection-component').find('.fee_container').find('#subtract_from_recipient_id_select').should('have.value', '1'); + cy.get('#fee-selection-component').find('.fee_container').find('#subtract_from_recipient_id_select').find(':selected').should('have.text', 'Recipient 2'); + + cy.get('#create_psbt_btn').click() + var amount = 0 + + // The fee should be subtracted from recipient 2, so the amount should be less than 2 + cy.get('div.tx_info > :nth-child(1) > :nth-child(1)').then(($div) => { // nth-child is indexed from 1 https://css-tricks.com/almanac/selectors/n/nth-child/ + amount = parseFloat($div.text()) + expect(amount).to.be.lt(2) + expect(amount).to.be.gt(1) + }) + cy.get('div.tx_info > :nth-child(2) > :nth-child(1)').then(($div) => { + amount = parseFloat($div.text()) + expect(amount).to.be.equal(4) + }) + cy.get('div.tx_info > :nth-child(3) > :nth-child(1)').then(($div) => { + amount = parseFloat($div.text()) + expect(amount).to.be.equal(5) + }) + + // Delete the PSBT so the utxos can be used in the next test again + cy.get('#deletepsbt_btn').click() + }) + + it('Create a transaction with multiple recipients and use send max', () => { + cy.selectWallet("Test Hot Wallet 1") + cy.get('#btn_send').click() + /// The addresses are the first three from DIY ghost + cy.get('#recipient_0').find('#address').type("bcrt1qvtdx75y4554ngrq6aff3xdqnvjhmct5wck95qs", { force: true }) + cy.get('#recipient_0').find('#label').type("Recipient 1", { force: true }) + cy.get('#recipient_0').find('#amount').type(10, { force: true }) + cy.get('main').scrollTo('bottom') + cy.get('#add-recipient').click() + cy.get('#recipient_1').find('#address').type("bcrt1qgzmq6e3tn67kveryf2je6nd3nv4txef4sl8wre", { force: true }) + cy.get('#recipient_1').find('#label').type("Recipient 2", { force: true }) + cy.get('#recipient_1').find('#amount').type(5, { force: true }) + cy.get('main').scrollTo('bottom') + cy.get('#add-recipient').click() + cy.get('#recipient_2').find('#address').type("bcrt1q9mkrhmxcn7rslzfv6lke8859m7ntwudfjqmcx7", { force: true }) + cy.get('#recipient_2').find('#label').type("Recipient 3", { force: true }) + // Using send max + cy.get('#recipient_2').find('#send_max').click() + cy.get('main').scrollTo('bottom') + + // Check whether the subtract fees box is ticked (we used send max) + cy.get('#toggle_advanced').click() + cy.get('#fee-selection-component').find('.fee_container').find('input#subtract').invoke('prop', 'checked').should('eq', true) + + // Check whether the recipient number input field is visible + cy.get('#fee-selection-component').find('.fee_container').find('span#subtract_from').should('be.visible') + + // Check the values of the hidden inputs in the light DOM which are used for the form + // Note: Despite identical ids the hidden inputs seem to be fetched first since they are higher up in the DOM + cy.get('#fee-selection-component').find('#subtract').invoke('attr', 'value').should('eq', 'true') + + // Check whether send max set subtract_from to Recipient 3 + cy.get('#fee-selection-component').find('.fee_container').find('#subtract_from_recipient_id_select').should('have.value', '2'); + cy.get('#fee-selection-component').find('.fee_container').find('#subtract_from_recipient_id_select').find(':selected').should('have.text', 'Recipient 3'); + + // Select Recipient 2 to subract the fee from + cy.get('#fee-selection-component').find('.fee_container').find('#subtract_from_recipient_id_select').select('Recipient 2') + cy.get('#fee-selection-component').find('.fee_container').find('#subtract_from_recipient_id_select').should('have.value', '1'); + cy.get('#fee-selection-component').find('.fee_container').find('#subtract_from_recipient_id_select').find(':selected').should('have.text', 'Recipient 2'); + + // Change it back to Recipient 3 + cy.get('#recipient_2').find('#send_max').click() // The fee should be subtracted from the third recipient cy.get('#create_psbt_btn').click() @@ -104,39 +165,46 @@ describe('Test sending transactions', () => { cy.deleteWallet("Test Hot Wallet 1") }) - it('Send a transaction from a multisig wallet', () => { - cy.get('body').then(($body) => { - if ($body.text().includes('Test Multisig Wallet 1')) { - cy.get('#wallets_list > .item > svg').click() - cy.get(':nth-child(6) > .right').click() - cy.get('#advanced_settings_tab_btn').click() - cy.get('.card > :nth-child(9) > .btn').click() - } - }) - cy.get('#btn_new_wallet').click() - cy.get('[href="./multisig/"]').click() - cy.get('#hot_device_1').click() - cy.get('#diy_ghost').click() - cy.get('#submit-device').click() - cy.get('#wallet_name').type("Test Multisig Wallet 1") - - cy.get('#keysform > .centered').click() - cy.get('body').contains("New wallet was created successfully!") - cy.get('#page_overlay_popup_cancel_button').click() - // Send transaction - - //get some funds - cy.mine2wallet("btc") - + it('No remove button if there is only one recipient', () => { + cy.selectWallet("Ghost wallet") cy.get('#btn_send').click() - cy.get('#address_0').type("bcrt1qsj30deg0fgzckvlrn5757yk55yajqv6dqx0x7u") - cy.get('#label_0').type("Burn address") - cy.get('#send_max_0').click() + // No remove button when the send dialog is started with only one recipient + cy.get('#recipient_0').find('#remove').should('not.be.visible') + cy.get('#add-recipient').click() + // Now both remove buttons should be visible + cy.get('#recipient_0').find('#remove').should('be.visible') + cy.get('#recipient_1').find('#remove').should('be.visible') + // Remove button should disappear again if only one recipient (here: Recipient 3) remains + cy.get('#add-recipient').click() + cy.get('#recipient_0').find('#remove').click({ force: true }) + cy.get('#recipient_1').find('#remove').click({ force: true }) + cy.get('#recipient_2').find('#remove').should('not.be.visible') + }) + + it('Use an address belonging to the wallet', () => { + cy.selectWallet("Ghost wallet") + cy.get('#btn_send').click() + cy.get('#recipients').find('#recipient_0').find('#address').type("bcrt1qvtdx75y4554ngrq6aff3xdqnvjhmct5wck95qs", { force: true }) + // Checking that the background colour of the address is green as it belongs to the wallet + cy.get('#recipients').find('#recipient_0').find('#address').should('have.css', 'background-color','rgb(48, 109, 48)') + }) + + it('Send a transaction from a multisig wallet', () => { + // We need a second hot wallet + cy.addHotDevice("Hot Device 2","bitcoin") + cy.addWallet('Test Multisig Wallet', 'segwit', 'funded', 'btc', 'multisig', 'Hot Device 1', 'Hot Device 2', 'DIY ghost') + cy.get('#btn_send').click() + cy.get('#recipient_0').find('#address').type("bcrt1qsj30deg0fgzckvlrn5757yk55yajqv6dqx0x7u", { force: true }) + cy.get('#recipient_0').find('#label').type("Burn address", { force: true }) + cy.get('#recipient_0').get('#send_max').click() cy.get('#create_psbt_btn').click() cy.get('body').contains("Paste signed transaction") cy.get('#hot_device_1_tx_sign_btn').click() cy.get('#hot_device_1_hot_sign_btn').click() cy.get('#hot_enter_passphrase__submit').click() + cy.get('#hot_device_2_tx_sign_btn').click() + cy.get('#hot_device_2_hot_sign_btn').click() + cy.get('#hot_enter_passphrase__submit').click() cy.get('#broadcast_local_btn').click() cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") }) .should(($div) => { @@ -144,7 +212,7 @@ describe('Test sending transactions', () => { expect(n).to.be.equals(0) }) // Clean up - cy.deleteWallet("Test Multisig Wallet 1") + cy.deleteWallet("Test Multisig Wallet") cy.deleteDevice("Hot Device 1") }) }) \ No newline at end of file diff --git a/cypress/integration/spec_wallet_utxo.js b/cypress/integration/spec_wallet_utxo.js index dd60da4d0..a295ec201 100644 --- a/cypress/integration/spec_wallet_utxo.js +++ b/cypress/integration/spec_wallet_utxo.js @@ -98,8 +98,8 @@ describe('Test the actions in UTXO list', () => { it('Managing unsigned transactions', () => { // Make an unsigned tx - cy.get('#address_0').type("bcrt1qvtdx75y4554ngrq6aff3xdqnvjhmct5wck95qs") // address from "Ghost wallet" - cy.get('#send_max_0').click() + cy.get('#recipient_0').find('#address').type("bcrt1qvtdx75y4554ngrq6aff3xdqnvjhmct5wck95qs") // address from "Ghost wallet" + cy.get('#recipient_0').get('#send_max').click() cy.get('#create_psbt_btn').click() // Check the labeling of the unsigned UTXO cy.log("Check the labeling of the unsigned UTXO") diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 6b1f5835c..5775f0721 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -61,12 +61,8 @@ Cypress.Commands.add("addDevice", (name, device_type, mnemonic) => { Cypress.Commands.add("addHotDevice", (name, node_type) => { // node_type is either elements or bitcoin cy.get('body').then(($body) => { - 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( {force: true} ) - cy.get('#forget_device').click() + cy.deleteDevice(name) // We might get an error here, if the device is used in a wallet // We assume therefore that this is ok (see below) } @@ -149,40 +145,60 @@ Cypress.Commands.add("addHotWallet", (wallet_name, device_name, node_type, walle }) }) -Cypress.Commands.add("addWallet", (wallet_name, device_name, wallet_type, funded, node_type) => { - if (wallet_type == null) { - wallet_type = "segwit" +Cypress.Commands.add("addWallet", (walletName, walletType, funded, nodeType, keyType, deviceNameOne, deviceNameTwo, deviceNameThree) => { + if (walletType == null) { + walletType = "segwit" } - if (device_name == null) { - device_name = "DIY ghost" + if (deviceNameOne == null) { + deviceNameOne = "DIY ghost" } cy.get('body').then(($body) => { - if ($body.text().includes(wallet_name)) { - cy.contains(wallet_name).click() + if ($body.text().includes(walletName)) { + cy.contains(walletName).click() cy.get('#btn_settings' ).click( {force: true}) cy.get('#advanced_settings_tab_btn').click() cy.get('#delete_wallet').click() } cy.get('#side-content').click() cy.get('#btn_new_wallet').click() - cy.get('[href="./simple/"]').click() - var device_button = "#"+device_name.toLowerCase().replace(/ /g,"_") - cy.get(device_button).click() - cy.get('#wallet_name').type(wallet_name) - if (wallet_type == "nested_segwit") { - cy.get('#type_nested_segwit_btn').click() + if (keyType == 'singlesig') { + cy.get('[href="./simple/"]').click() + var device_button = "#"+deviceNameOne.toLowerCase().replace(/ /g,"_") + cy.get(device_button).click() + cy.get('#wallet_name').type(walletName) + if (walletType == "nested_segwit") { + cy.get('#type_nested_segwit_btn').click() + } + if (walletType == "taproot") { + cy.get('#type_taproot_btn').click() + } } - if (wallet_type == "taproot") { - cy.get('#type_taproot_btn').click() + // Makes a 2 out 3 multisig + else if (keyType == "multisig") { + cy.get('[href="./multisig/"]').click() + var deviceButtonOne = "#"+deviceNameOne.toLowerCase().replace(/ /g,"_") + cy.get(deviceButtonOne).click() + var deviceButtonTwo = "#"+deviceNameTwo.toLowerCase().replace(/ /g,"_") + cy.get(deviceButtonTwo).click() + var deviceButtonThree = "#"+deviceNameThree.toLowerCase().replace(/ /g,"_") + cy.get(deviceButtonThree).click() + cy.get('#submit-device').click() + cy.get('#wallet_name').type(walletName) + if (walletType == "nested_segwit") { + cy.get('#type_nested_segwit_btn').click() + } + cy.get(':nth-child(9) > .inline').clear() + cy.get(':nth-child(9) > .inline').type(2) } cy.get('#keysform > .centered').click() cy.get('body').contains("New wallet was created successfully!") - cy.get('#btn_continue').click() + cy.get('#page_overlay_popup_cancel_button').click() if (funded) { - cy.mine2wallet(node_type) + cy.mine2wallet(nodeType) } }) }) + Cypress.Commands.add("deleteWallet", (name) => { cy.get('body').then(($body) => { if ($body.text().includes(name)) { @@ -234,9 +250,10 @@ Cypress.Commands.add("mine2wallet", (chain) => { // Quick and easy way to fill out the send form and create a psbt Cypress.Commands.add("createPsbt", (address, label="a_label", amount=0.01) => { cy.get('#btn_send').click() - cy.get('#address_0').type(address) - cy.get('#label_0').type(label) + // it is not clear why .shadow(), or { includeShadowDom: true } is needed here to find the elements in the ShadowDOM, but not in the other cypresss tests + cy.get('#recipient_0').find('#address', { includeShadowDom: true }).type(address) + cy.get('#recipient_0').find('#label', { includeShadowDom: true }).type(label) //cy.get('#send_max_0').click() - cy.get('#amount_0').type(amount) + cy.get('#recipient_0').find('#amount', { includeShadowDom: true }).type(amount) cy.get('#create_psbt_btn').click() }) diff --git a/docker/cypress-python/Dockerfile b/docker/cypress-python/Dockerfile index 8b4ad9575..875113675 100644 --- a/docker/cypress-python/Dockerfile +++ b/docker/cypress-python/Dockerfile @@ -10,5 +10,5 @@ RUN DEBIAN_FRONTEND="noninteractive" apt-get install --no-install-recommends -y WORKDIR /test RUN rm -rf node_modules package-lock.json ~/.cache/Cypress -RUN npm install --save-dev cypress@9.5.4 +RUN npm install --save-dev cypress@9.7.0 RUN $(npm bin)/cypress verify diff --git a/docker/cypress-python/Readme.md b/docker/cypress-python/Readme.md index faf0671d3..2e2c315e7 100644 --- a/docker/cypress-python/Readme.md +++ b/docker/cypress-python/Readme.md @@ -1,7 +1,10 @@ An image, ready to be used with cypress but also provides all the dependencies we need for testing specter-desktop. +Use versions of cypress as the version part of the tag. So e.g.: ``` -docker build . -t registry.gitlab.com/cryptoadvance/specter-desktop/cypress-python -docker push registry.gitlab.com/cryptoadvance/specter-desktop/cypress-python -``` \ No newline at end of file +docker build . -t registry.gitlab.com/cryptoadvance/specter-desktop/cypress-python:v9.7.0 +docker push registry.gitlab.com/cryptoadvance/specter-desktop/cypress-python:v9.7.0 +``` + +Search for `cypress-python` on where this is used in the project. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index bed9184b9..5e9ea9df0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "wait-on": "^5.3.0" }, "devDependencies": { - "cypress": "^9.5.4", + "cypress": "^9.7.0", "cypress-wait-until": "^1.7.1" } }, @@ -535,9 +535,9 @@ } }, "node_modules/cypress": { - "version": "9.5.4", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-9.5.4.tgz", - "integrity": "sha512-6AyJAD8phe7IMvOL4oBsI9puRNOWxZjl8z1lgixJMcgJ85JJmyKeP6uqNA0dI1z14lmJ7Qklf2MOgP/xdAqJ/Q==", + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-9.7.0.tgz", + "integrity": "sha512-+1EE1nuuuwIt/N1KXRR2iWHU+OiIt7H28jJDyyI4tiUftId/DrXYEwoDa5+kH2pki1zxnA0r6HrUGHV5eLbF5Q==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -2261,9 +2261,9 @@ } }, "cypress": { - "version": "9.5.4", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-9.5.4.tgz", - "integrity": "sha512-6AyJAD8phe7IMvOL4oBsI9puRNOWxZjl8z1lgixJMcgJ85JJmyKeP6uqNA0dI1z14lmJ7Qklf2MOgP/xdAqJ/Q==", + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-9.7.0.tgz", + "integrity": "sha512-+1EE1nuuuwIt/N1KXRR2iWHU+OiIt7H28jJDyyI4tiUftId/DrXYEwoDa5+kH2pki1zxnA0r6HrUGHV5eLbF5Q==", "dev": true, "requires": { "@cypress/request": "^2.88.10", diff --git a/package.json b/package.json index 701b28bca..3dbb9f55e 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "wait-on": "^5.3.0" }, "devDependencies": { - "cypress": "^9.5.4", + "cypress": "^9.7.0", "cypress-wait-until": "^1.7.1" }, "scripts": { diff --git a/src/cryptoadvance/specter/commands/psbt_creator.py b/src/cryptoadvance/specter/commands/psbt_creator.py index fad2db1f9..55a1e7870 100644 --- a/src/cryptoadvance/specter/commands/psbt_creator.py +++ b/src/cryptoadvance/specter/commands/psbt_creator.py @@ -28,8 +28,7 @@ class PsbtCreator: ): """ * depending of ui_option = (ui|text) Fill the payment-details in either of these: - * request_form: expects the payment-details in a dict request_form: - { "address_1":"bc1...","btc_amount_1":"0.2", "amount_unit_1":"btc", "label_1":"someLabel","address_2": ...} + * request_form: For details on the structure of the data for each recipient (amounts, addresses, etc.) see below at paymentinfo_from_ui * recipients_txt: expects the payment-details in textblock "recipients" and recipients_amount_unit for all amounts in recipients_txt either "sats" or "btc" * in both cases, the request_form also contains: @@ -135,32 +134,39 @@ class PsbtCreator: @classmethod def paymentinfo_from_ui(cls, specter, wallet, request_form): - """calculates the correct format needed by wallet.createpsbt() out of a request-form - returns something like (addresses, labels, amounts, amount_units) (all arrays) + """Calculates the correct format needed by wallet.createpsbt() out of a request form. + The recipient_dicts part in the form is a list of dicts and looks like this: + [{'unit': 'btc', 'amount': 1, 'btc_amount': 1, 'recipient_id': 0, 'label': '', 'address': 'bcrt1q ... 58qwn'}, + {'unit': 'btc', 'amount': 2, 'btc_amount': 2, 'recipient_id': 1, 'label': '', 'address': 'bcrt1q ... vaa3p'}, + {'unit': 'btc', 'amount': 3, 'btc_amount': 3, 'recipient_id': 2, 'label': '', 'address': 'bcrt1q ... n0a85'}] + + Returns (addresses, labels, amounts, amount_units) (all arrays) """ - i = 0 addresses = [] labels = [] amounts = [] amount_units = [] - while "address_{}".format(i) in request_form: - addresses.append(request_form["address_{}".format(i)]) + + recipient_dicts = json.loads(request_form["recipient_dicts"]) + print(recipient_dicts) + for recipient_dict in recipient_dicts: + addresses.append(recipient_dict["address"]) amount = 0.0 try: - amount = float(request_form["btc_amount_{}".format(i)]) + amount = float(recipient_dict["btc_amount"]) except ValueError: pass if isnan(amount): amount = 0.0 amounts.append(amount) - unit = request_form["amount_unit_{}".format(i)] + unit = recipient_dict["unit"] if specter.is_liquid and unit in ["sat", "btc"]: unit = specter.default_asset amount_units.append(unit) - labels.append(request_form["label_{}".format(i)]) - if request_form["label_{}".format(i)] != "": - wallet.setlabel(addresses[i], labels[i]) - i += 1 + labels.append(recipient_dict["label"]) + if recipient_dict["label"] != "": + wallet.setlabel(addresses[-1], labels[-1]) + return addresses, labels, amounts, amount_units @classmethod @@ -170,7 +176,6 @@ class PsbtCreator: """calculates the correct format needed by wallet.createpsbt() out of a request-form out of a textbox holding addresses and amounts. """ - i = 0 addresses = [] labels = [] amounts = [] @@ -264,7 +269,7 @@ class PsbtCreator: """calculates the needed kwargs fow wallet.createpsbt() out of a request_form""" # Who pays the fees? subtract = str2bool(request_form.get("subtract", False)) - subtract_from = int(request_form.get("subtract_from", 1)) + subtract_from = int(request_form.get("subtract_from", 0)) fee_option = request_form.get("fee_option") fee_rate = None if fee_option: @@ -296,7 +301,7 @@ class PsbtCreator: rbf_tx_id = request_form.get("rbf_tx_id", "") kwargs = { "subtract": subtract, - "subtract_from": subtract_from - 1, + "subtract_from": subtract_from, "fee_rate": fee_rate, "rbf": rbf, "selected_coins": selected_coins, @@ -319,14 +324,14 @@ class PsbtCreator: except JSONDecodeError as e: raise SpecterError(f"Error parsing json: {e}") subtract = bool(json_data.get("subtract", False)) - subtract_from = int(json_data.get("subtract_from", 1)) + subtract_from = int(json_data.get("subtract_from", 0)) fee_rate = float(json_data.get("fee_rate", None)) rbf = bool(json_data.get("rbf", False)) rbf_tx_id = json_data.get("rbf_tx_id", "") kwargs = { "subtract": subtract, - "subtract_from": subtract_from - 1, + "subtract_from": subtract_from, "fee_rate": fee_rate, "rbf": rbf, "selected_coins": [], diff --git a/src/cryptoadvance/specter/liquid/addresslist.py b/src/cryptoadvance/specter/liquid/addresslist.py index 7c37c958e..cfbe8b8ed 100644 --- a/src/cryptoadvance/specter/liquid/addresslist.py +++ b/src/cryptoadvance/specter/liquid/addresslist.py @@ -59,3 +59,6 @@ class LAddressList(AddressList): return self[addr] except KeyError: return default + except TypeError: + logger.warning(f"{addr} seems to be invalid") + return default diff --git a/src/cryptoadvance/specter/server_endpoints/wallets.py b/src/cryptoadvance/specter/server_endpoints/wallets.py index 3c445fac4..960a5a487 100644 --- a/src/cryptoadvance/specter/server_endpoints/wallets.py +++ b/src/cryptoadvance/specter/server_endpoints/wallets.py @@ -462,7 +462,7 @@ def send_new(wallet_alias): recipients_txt = "" fillform = False subtract = False - subtract_from = 1 + subtract_from = 0 fee_options = "dynamic" rbf = not app.specter.is_liquid rbf_utxo = [] diff --git a/src/cryptoadvance/specter/server_endpoints/wallets_api.py b/src/cryptoadvance/specter/server_endpoints/wallets_api.py index 04eda5c51..241abe0dc 100644 --- a/src/cryptoadvance/specter/server_endpoints/wallets_api.py +++ b/src/cryptoadvance/specter/server_endpoints/wallets_api.py @@ -669,6 +669,24 @@ def utxo_csv(wallet_alias): return _("Failed to export wallet utxo. Error: {}").format(e), 500 +@wallets_endpoint_api.route( + "/wallet//is_address_mine/
", methods=["GET"] +) +@login_required +def is_address_mine(wallet_alias, address): + wallet = app.specter.wallet_manager.get_by_alias(wallet_alias) + + # filter out invalid input + if (not address) or not isinstance(address, str): + return jsonify(False) + + # Segwit addresses are always between 14 and 74 characters long. + if len(address) < 14: + return jsonify(False) + + return jsonify(wallet.is_address_mine(address)) + + @wallets_endpoint_api.route("/wallet//send/estimatefee", methods=["POST"]) @login_required def estimate_fee(wallet_alias): diff --git a/src/cryptoadvance/specter/static/helpers.js b/src/cryptoadvance/specter/static/helpers.js index e646821a9..b51ea0523 100644 --- a/src/cryptoadvance/specter/static/helpers.js +++ b/src/cryptoadvance/specter/static/helpers.js @@ -131,3 +131,25 @@ function numberWithCommas(x) { } return x.replace(/\B(?=(\d{3})+(?!\d))/g, ","); } + + +async function send_request(url, method_str, csrf_token, formData) { + if (!formData) { + formData = new FormData(); + } + formData.append("csrf_token", csrf_token) + d = { + method: method_str, + } + if (method_str == 'POST') { + d['body'] = formData; + } + + const response = await fetch(url, d); + if(response.status != 200){ + showError(await response.text()); + console.log(`Error while calling ${url} with ${method_str} ${formData}`) + return + } + return await response.json(); +} \ No newline at end of file diff --git a/src/cryptoadvance/specter/static/img/add-person.svg b/src/cryptoadvance/specter/static/img/add-person.svg new file mode 100644 index 000000000..7c83907ab --- /dev/null +++ b/src/cryptoadvance/specter/static/img/add-person.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/cryptoadvance/specter/static/img/close.svg b/src/cryptoadvance/specter/static/img/close.svg new file mode 100644 index 000000000..989837ca0 --- /dev/null +++ b/src/cryptoadvance/specter/static/img/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/cryptoadvance/specter/static/styles.css b/src/cryptoadvance/specter/static/styles.css index 9b1c73d0c..336c269c2 100644 --- a/src/cryptoadvance/specter/static/styles.css +++ b/src/cryptoadvance/specter/static/styles.css @@ -14,7 +14,9 @@ html, body{ --cmap-bg-lighter: #263044; --cmap-bg-lightest: #313E50; --cmap-border: #506072; + --cmap-border-highlight:#4B8CD8; --cmap-border-darker: #405062; + --cmap-bg-address-is-mine:#306d30; margin: 0; padding: 0; @@ -1244,3 +1246,43 @@ input:checked + .slider:before { -ms-transform: translateX(26px); transform: translateX(26px); } +.recipient_wrapper{ + width: 100%; + border-radius: 3px; + box-shadow: 0px 10px 15px rgba(0,0,0,0.1); + } +.recipient_wrapper_item{ + border-radius: 5px; + border: 1.5px solid var(--default-color); + padding: 5px; + margin-bottom: 5px; + } + .recipient_wrapper_inner_box{ + padding: 5px; + margin-top: 5px; + } +.recipient_button{ + background: var(--cmap-border); + padding-left: 2px; + padding-right: 2px; + margin-left: 2px; + margin-right: 2px; + border-radius: 3px; + height: 22px; + border: none; + } +.recipient_button:hover { + background: var(--cmap-border-darker); +} +.recipient_dragger { + cursor: grab; +} +.recipient_add { + margin-right: 3px; + height: 25px; +} +.recipient_move { + cursor: default; + padding-left: 6px; + padding-right: 6px; +} diff --git a/src/cryptoadvance/specter/templates/includes/fee-selection.html b/src/cryptoadvance/specter/templates/includes/fee-selection.html index 565b10543..e86f6089d 100644 --- a/src/cryptoadvance/specter/templates/includes/fee-selection.html +++ b/src/cryptoadvance/specter/templates/includes/fee-selection.html @@ -12,8 +12,9 @@
@@ -53,7 +54,7 @@ * The API for this component works in a way that it manages 5 hidden inputs which will expose the * choice of the user. * * - * * + * * * * * * * * @@ -106,7 +107,7 @@ // Subtract this.subtract = clone.querySelector("#subtract") this.subtractFrom = clone.querySelector("#subtract_from") - this.subtractFromInput = clone.querySelector("#subtract_from_input") + this.subtractFromRecipientIdSelect = clone.querySelector("#subtract_from_recipient_id_select") // Presets this.feeOptionPreset = this.getAttribute('fee-option-preset') == null ? "dynamic" : this.getAttribute('fee-option-preset') @@ -169,16 +170,11 @@ this.ld.subtract.value = this.subtract.value; this.appendChild(this.ld.subtract); - // subtract_from (the actual input field) - this.ld.subtractFromInput = this.subtractFromInput.cloneNode(true); - this.ld.subtractFromInput.type = "hidden"; - this.ld.subtractFromInput.name = "subtract_from"; - this.appendChild(this.ld.subtractFromInput); - - // This is just for exposure to the jinja template for displaying, not needed for the form POST - this.ld.subtractFrom = this.subtractFrom.cloneNode(true); - this.ld.subtractFrom.type = "hidden"; - this.appendChild(this.ld.subtractFrom); + // subtract_from (for the form) + this.ld.subtractFromRecipientIdSelect = document.createElement("input"); + this.ld.subtractFromRecipientIdSelect.type = "hidden"; + this.ld.subtractFromRecipientIdSelect.name = "subtract_from"; + this.appendChild(this.ld.subtractFromRecipientIdSelect); } /** @@ -206,7 +202,7 @@ this.rbfUpdated(); }) - this.subtractFromInput.addEventListener("change", (event) => { + this.subtractFromRecipientIdSelect.addEventListener("change", (event) => { this.subtractUpdated(); }) @@ -330,9 +326,9 @@ } catch(e) {console.log(e)} } - - setSubtractFrom(recipient) { - this.subtractFromInput.value = recipient + setSubtractFrom(recipientId) { + this.subtractFromRecipientIdSelect.value = recipientId + this.ld.subtractFromRecipientIdSelect.value = recipientId } // Updates Light DOM values for the form @@ -340,7 +336,7 @@ if (this.subtract.checked) { this.ld.subtract.value = "true" this.ld.subtract.checked = true - this.ld.subtractFromInput.value = this.subtractFromInput.value + this.ld.subtractFromRecipientIdSelect.value = this.subtractFromRecipientIdSelect.value } else { this.ld.subtract.value = "false" diff --git a/src/cryptoadvance/specter/templates/includes/recipient-box.html b/src/cryptoadvance/specter/templates/includes/recipient-box.html new file mode 100644 index 000000000..a1621cba8 --- /dev/null +++ b/src/cryptoadvance/specter/templates/includes/recipient-box.html @@ -0,0 +1,484 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/cryptoadvance/specter/templates/wallet/send/new/wallet_send.jinja b/src/cryptoadvance/specter/templates/wallet/send/new/wallet_send.jinja index facfa97fa..8b722ed58 100644 --- a/src/cryptoadvance/specter/templates/wallet/send/new/wallet_send.jinja +++ b/src/cryptoadvance/specter/templates/wallet/send/new/wallet_send.jinja @@ -2,11 +2,7 @@ {% set tab = 'send' %} {% block content %} - {% include "includes/qr-scanner.html" %} + {% include "includes/recipient-box.html" %} {% from 'wallet/send/components/send_nav.jinja' import send_nav %} {{ send_nav('send_new', wallet_alias) }} @@ -26,6 +23,7 @@
+

{{ _("Create Transaction") }}

{{ _("Available funds:") }} {{wallet.amount_available | btcunitamount}} @@ -62,12 +60,13 @@
{% endif %}
-
-
+
+
+
{{ _("Unit:") }} - -
+ +

@@ -77,10 +76,9 @@ <{{ _("ADDRESS") }}3>, <{{ _("AMOUNT") }}3>

-

{{ _("Calculate estimated fee") }}

{{ _("Advanced") }} {% if show_advanced_settings %}▼{% else %}▶{% endif %} -

+

{{ _("Transaction editor:") }} @@ -91,9 +89,7 @@


-
- -

+
{% include "includes/tx-row.html" %} {% include "includes/tx-data.html" %} @@ -112,10 +108,8 @@ {% endblock %} - {% block scripts %} {% endblock %} diff --git a/src/cryptoadvance/specterext/devhelp/templates/devhelp/html/fee-selection.jinja b/src/cryptoadvance/specterext/devhelp/templates/devhelp/html/fee-selection.jinja index 3e21cd600..dab0e1f97 100644 --- a/src/cryptoadvance/specterext/devhelp/templates/devhelp/html/fee-selection.jinja +++ b/src/cryptoadvance/specterext/devhelp/templates/devhelp/html/fee-selection.jinja @@ -28,10 +28,9 @@ It's designed to be used in a form and will "drop" what the user has chosen in f - diff --git a/tests/test_commands_psbt_creator.py b/tests/test_commands_psbt_creator.py index f37293ca6..2de628947 100644 --- a/tests/test_commands_psbt_creator.py +++ b/tests/test_commands_psbt_creator.py @@ -17,23 +17,16 @@ def test_PsbtCreator_ui(caplog): # Let's mock the request.form which behaves like a dict but also needs getlist() request_form_data = { "rbf_tx_id": "", - "address_0": "BCRT1qgc6h85z43g3ss2dl5zdrzrp3ef6av4neqcqhh8", # will need normalisation - "label_0": "someLabel", - "amount_0": "0.1", - "btc_amount_0": "0.1", - "amount_unit_0": "btc", - "address_1": "bcrt1q3kfetuxpxvujasww6xas94nawklvpz0e52uw8a", - "label_1": "someOtherLabel", - "amount_1": "111211", - "btc_amount_1": "0.00111211", - "amount_unit_1": "sat", "amount_unit_text": "btc", - "subtract_from": "1", + "subtract_from": "0", "fee_option": "dynamic", "fee_rate": "", "fee_rate_dynamic": "64", "rbf": "on", "action": "createpsbt", + "recipient_dicts": '[{"unit":"btc","amount":0.1,"recipient_id":0,"address":"BCRT1qgc6h85z43g3ss2dl5zdrzrp3ef6av4neqcqhh8","label":"someLabel","btc_amount":"0.1"},' + '{"unit":"sat","amount":111211,"recipient_id":1,"address":"bcrt1q3kfetuxpxvujasww6xas94nawklvpz0e52uw8a","label":"someOtherLabel","btc_amount":"0.00111211"},' + '{"unit":"btc","amount":0.003,"recipient_id":2,"address":"bcrt1qfvkcy2keql72s8ev87ek93uxuq3xxsx9l0n03r","label":"","btc_amount":"0.003"}]', } psbt_creator: PsbtCreator = PsbtCreator( @@ -43,10 +36,15 @@ def test_PsbtCreator_ui(caplog): assert psbt_creator.addresses == [ "bcrt1qgc6h85z43g3ss2dl5zdrzrp3ef6av4neqcqhh8", "bcrt1q3kfetuxpxvujasww6xas94nawklvpz0e52uw8a", + "bcrt1qfvkcy2keql72s8ev87ek93uxuq3xxsx9l0n03r", ] - assert psbt_creator.amounts == [0.1, 0.00111211] - assert psbt_creator.labels == ["someLabel", "someOtherLabel"] - assert psbt_creator.amount_units == ["btc", "sat"] + assert psbt_creator.amounts == [0.1, 0.00111211, 0.003] + assert psbt_creator.labels == [ + "someLabel", + "someOtherLabel", + "", + ] + assert psbt_creator.amount_units == ["btc", "sat", "btc"] assert psbt_creator.kwargs == { "fee_rate": 64.0, "rbf": True, @@ -72,7 +70,7 @@ def test_PsbtCreator_text(caplog): # Let's mock the request.form which behaves like a dict but also needs getlist() request_form_data = { "rbf_tx_id": "", - "subtract_from": "1", + "subtract_from": "0", "fee_option": "dynamic", "fee_rate": "", "fee_rate_dynamic": "64", @@ -142,7 +140,7 @@ def test_PsbtCreator_json(caplog): } ], "rbf_tx_id": "", - "subtract_from": "1", + "subtract_from": "0", "fee_rate": "64", "rbf": true } diff --git a/tests/test_ep_controller.py b/tests/test_ep_controller.py index 2dcb5cd04..555dea100 100644 --- a/tests/test_ep_controller.py +++ b/tests/test_ep_controller.py @@ -309,6 +309,7 @@ wallets_endpoint_api.fees wallets_endpoint_api.generatemnemonic wallets_endpoint_api.get_label wallets_endpoint_api.get_scantxoutset_status +wallets_endpoint_api.is_address_mine wallets_endpoint_api.pending_psbt_list wallets_endpoint_api.rescan_progress wallets_endpoint_api.set_label diff --git a/tests/test_rest.py b/tests/test_rest.py index 049f20c58..9b6f483fc 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -128,7 +128,7 @@ def test_rr_psbt_post(specter_regtest_configured, bitcoin_regtest, client, caplo } ], "rbf_tx_id": "", - "subtract_from": "1", + "subtract_from": "0", "fee_rate": "64", "rbf": true }