mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
UIUX: Easier adding and deleting of recipients (#1782)
* Using a new web component for recipients Co-authored-by: moneymanolis <moneymanolis@protonmail.com>
This commit is contained in:
parent
f739d9bb7a
commit
a82eddfe7f
28 changed files with 1048 additions and 445 deletions
|
|
@ -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:
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -39,3 +39,4 @@ tests/elements
|
|||
signing_dir
|
||||
site
|
||||
docs/README.md
|
||||
cypresstest-output.xml
|
||||
|
|
|
|||
|
|
@ -25,5 +25,4 @@
|
|||
"mochaFile": "cypresstest-output.xml",
|
||||
"toConsole": true
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
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.
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": [],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -669,6 +669,24 @@ def utxo_csv(wallet_alias):
|
|||
return _("Failed to export wallet utxo. Error: {}").format(e), 500
|
||||
|
||||
|
||||
@wallets_endpoint_api.route(
|
||||
"/wallet/<wallet_alias>/is_address_mine/<address>", 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/<wallet_alias>/send/estimatefee", methods=["POST"])
|
||||
@login_required
|
||||
def estimate_fee(wallet_alias):
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
1
src/cryptoadvance/specter/static/img/add-person.svg
Normal file
1
src/cryptoadvance/specter/static/img/add-person.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><path d="M13,8c0-2.21-1.79-4-4-4S5,5.79,5,8s1.79,4,4,4S13,10.21,13,8z M11,8c0,1.1-0.9,2-2,2S7,9.1,7,8s0.9-2,2-2S11,6.9,11,8z M1,18v2h16v-2c0-2.66-5.33-4-8-4S1,15.34,1,18z M3,18c0.2-0.71,3.3-2,6-2c2.69,0,5.78,1.28,6,2H3z M20,15v-3h3v-2h-3V7h-2v3h-3v2 h3v3H20z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 459 B |
1
src/cryptoadvance/specter/static/img/close.svg
Normal file
1
src/cryptoadvance/specter/static/img/close.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
||||
|
After Width: | Height: | Size: 258 B |
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@
|
|||
</tool-tip>
|
||||
</div>
|
||||
<span id="subtract_from" style="display: none">
|
||||
<br>{{ _("Subtract from recipient number:") }}
|
||||
<input id="subtract_from_input" name="subtract_from_stale" type="number" min="1" value="1" step="1" style="width: 80px; min-width: 80px;"><br>
|
||||
<br>{{ _("Subtract from ") }}
|
||||
<select id="subtract_from_recipient_id_select" name="subtract_from_stale" style="width: 140px; min-width: 140px;"></select>
|
||||
<br>
|
||||
</span>
|
||||
<br>
|
||||
<div>
|
||||
|
|
@ -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.
|
||||
* * <input type="checkbox" class="rbf-checkbox inline hidden" name="rbf" id="rbf">
|
||||
* * <input id="subtract_from_input" name="subtract_from" type="number" class="hidden">
|
||||
* * <input id="subtract_from_recipient_id_select" name="subtract_from" type="number" class="hidden">
|
||||
* * <input type="hidden" value="dynamic" name="fee_option">
|
||||
* * <input type="number" class="fee_rate hidden" name="fee_rate" id="fee_rate" value="0.1">
|
||||
* * <input type="hidden" id="fee_rate_dynamic" name="fee_rate_dynamic" value="0.1" class="hidden">
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
484
src/cryptoadvance/specter/templates/includes/recipient-box.html
Normal file
484
src/cryptoadvance/specter/templates/includes/recipient-box.html
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
<!--
|
||||
A recipient form. The .value property returns a dictionary of address, label, amount and more
|
||||
|
||||
{#- Usage -
|
||||
|
||||
<recipient-box id=`recipient_0` recipientId=0 title=`Recipient 1`></recipient-box>
|
||||
|
||||
or via the class:
|
||||
let newRecipient = new RecipientBox();
|
||||
newRecipient.id = `recipient_${recipientId}`;
|
||||
newRecipient.recipientId = recipientId;
|
||||
newRecipient.title = `Recipient ${recipientId+1}`;
|
||||
newRecipient.addEventListener('address-input', (event) => {
|
||||
validateForm()
|
||||
})
|
||||
|
||||
|
||||
It exposes the events:
|
||||
- 'remove' # is called when the remove button is clicked
|
||||
- 'address-input'
|
||||
- 'unit-change'
|
||||
- 'send-max'
|
||||
|
||||
|
||||
It has the attributes: ["value", "name", "id", "title", "address", "label",
|
||||
"recipientId", "max", "step", "amount", "hiddenRemoveButton"]
|
||||
These attributes will be assigned to the appropriate html elements
|
||||
|
||||
It has the properties:
|
||||
- 'value' returns a dict with important attributes
|
||||
|
||||
#}
|
||||
-->
|
||||
|
||||
<template id="recipient-box">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}">
|
||||
|
||||
<style>
|
||||
.max-btn:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
.address {
|
||||
padding: 7px;
|
||||
}
|
||||
.address-label {
|
||||
padding: 7px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="recipient" class="recipient_wrapper_item" >
|
||||
<span id='title'><!-- the title comes in here--></span>
|
||||
<img src="{{ url_for('static', filename='img/close.svg') }}" class="recipient_button recipient_remove" title="Remove recipient" id="remove" style="float:right">
|
||||
<div class="recipient_wrapper_inner_box" >
|
||||
<div class="row" style="gap: 5px">
|
||||
<input type="text" id="address" name="address" class="address" value="" placeholder='{{ _("Recipient address") }}'>
|
||||
<qr-scanner id="address-scan" style="margin-top: 3px;">
|
||||
<a slot="button" class="btn" style="height: 35px;">
|
||||
<img src="{{ url_for('static', filename='img/qr-code.svg') }}" style="width: 26px; margin: 0px;" class="svg-white"> {{ _("Scan") }}</a>
|
||||
</qr-scanner>
|
||||
</div>
|
||||
<input type="text" id="label" class="address-label" name="label" value="" placeholder='{{ _("Address label") }}'>
|
||||
<br>
|
||||
{{ _("Amount:") }}<br>
|
||||
<input style="width: 200px" type="number" name="amount" id="amount" min=0 step='1e-8' autocomplete="off" placeholder="0"> <!-- the value comes in here-->
|
||||
<div class="mobile-only" style="margin-top: 10px;"></div>
|
||||
<span id='asset_selector'><!-- the assetSelector comes in here--></span>
|
||||
<span class="note max-btn" style="margin-left: 5px;" id="send_max">({{ _("send max") }})</span>
|
||||
<div>
|
||||
<span class="note" id="converted_unit_amount">-</span> <span class="note" id="converted_unit_label"><!-- the convertedUnit comes in here--></span> <span class="note" id="converted_unit_alt"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
</template>
|
||||
|
||||
|
||||
<template id="liquid-asset-selector">
|
||||
<select id="amount_unit" name="amount_unit" style="width: 100px;" >
|
||||
<option value="btc">LBTC</option>
|
||||
<option value="sat">L-sat</option>
|
||||
{% for asset in wallet.balance.get("assets",{}).keys() | sort %}
|
||||
<option value="{{asset}}">{{asset | assetlabel}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</template>
|
||||
|
||||
<template id="bitcoin-asset-selector">
|
||||
<label><input id="amount_unit_sat" type="radio" class="inline" style="margin: 0 5px;" name="amount_unit" value="sat">sat</label>
|
||||
<label><input id="amount_unit_btc" type="radio" class="inline" style="margin: 0 5px;" name="amount_unit" value="btc">BTC</label>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
class RecipientBox extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.internals = this.attachInternals();
|
||||
this.recipientId = null; // MUST be set for a functioning instance
|
||||
|
||||
this.buildHTML(); // created the html
|
||||
this.attachListeners(); // attaches the listeners to some html objects
|
||||
}
|
||||
|
||||
|
||||
|
||||
// creates the entire html code (without event listeners) from the template
|
||||
buildHTML(){
|
||||
// Create a shadow root
|
||||
this.attachShadow({
|
||||
mode: 'open'
|
||||
});
|
||||
|
||||
var template_content = document.getElementById('recipient-box').content;
|
||||
let clone = template_content.cloneNode(true);
|
||||
|
||||
this.amountElement = clone.getElementById('amount');
|
||||
this.addressElement = clone.getElementById('address');
|
||||
this.labelElement = clone.getElementById('label');
|
||||
this.titleElement = clone.getElementById('title');
|
||||
this.removeElement = clone.getElementById('remove');
|
||||
|
||||
this.shadowRoot.appendChild(clone);
|
||||
this.addAssetSelectorToShadowRoot()
|
||||
}
|
||||
|
||||
|
||||
|
||||
// add the html code for the assetSelector
|
||||
addAssetSelectorToShadowRoot() {
|
||||
// add different asset selector template for bitcoin and liquid
|
||||
{% if specter.is_liquid and wallet.balance.get("assets", {}) %}
|
||||
var template_content = document.getElementById('liquid-asset-selector').content;
|
||||
var clone = template_content.cloneNode(true);
|
||||
|
||||
this.amountUnitElement = clone.getElementById('amount_unit');
|
||||
// create custom getter and setter functions
|
||||
this.setUnit = (newValue) => {
|
||||
this.amountUnitElement.value = newValue;
|
||||
this.updateUnitLabelAndStep();
|
||||
}
|
||||
this.getUnit = () => {
|
||||
return this.amountUnitElement.value;
|
||||
}
|
||||
{% else %}
|
||||
var template_content = document.getElementById('bitcoin-asset-selector').content;
|
||||
var clone = template_content.cloneNode(true);
|
||||
|
||||
this.amountUnitSatElement = clone.getElementById('amount_unit_sat');
|
||||
this.amountUnitBtcElement = clone.getElementById('amount_unit_btc');
|
||||
// create custom getter and setter functions
|
||||
this.setUnit = (newValue) => {
|
||||
this.amountUnitBtcElement.checked = newValue == 'btc';
|
||||
this.amountUnitSatElement.checked = newValue == 'sat';
|
||||
this.updateUnitLabelAndStep();
|
||||
}
|
||||
this.getUnit = () => {
|
||||
return this.amountUnitBtcElement.checked ? this.amountUnitBtcElement.value : this.amountUnitSatElement.value ;
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
|
||||
this.shadowRoot.getElementById('asset_selector').appendChild(clone);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// adds EventListeners
|
||||
attachListeners(){
|
||||
// I cannot use getElementById , but have to use querySelectorAll, because there are multiple unit buttons,
|
||||
// that need to trigger the same event
|
||||
this.shadowRoot.querySelectorAll('[name="amount_unit"]').forEach(input => {
|
||||
input.addEventListener('change',() => {
|
||||
this.updateUnitLabelAndStep();
|
||||
|
||||
this.dispatchEvent(new CustomEvent('unit-change'));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
this.addressElement.addEventListener('input', event=>{
|
||||
this.dispatchAddressOnInput();
|
||||
});
|
||||
|
||||
|
||||
this.shadowRoot.getElementById('address-scan').addEventListener('scan', event=>{
|
||||
let addr = event.detail.result;
|
||||
if(addr == null){
|
||||
return;
|
||||
}
|
||||
// remove bitcoin: stuff
|
||||
if(addr.indexOf("bitcoin:") >= 0){
|
||||
addr = addr.substr(addr.indexOf("bitcoin:")+8);
|
||||
}
|
||||
let arr = addr.split("?");
|
||||
addr = arr[0];
|
||||
this.addressElement.value = addr;
|
||||
let evt = new Event('input');
|
||||
this.addressElement.dispatchEvent(evt);
|
||||
// parse metadata like amount and message
|
||||
if(arr.length > 1){
|
||||
arr = arr[1].split("&");
|
||||
arr.forEach((e)=>{
|
||||
if(e.startsWith("amount=")){
|
||||
let val = parseFloat(e.substr(7));
|
||||
if(this.unit == 'sat'){
|
||||
val = Math.round(val*1e8);
|
||||
}
|
||||
this.amountElement.value = val;
|
||||
let evt = new Event('input');
|
||||
this.amountElement.dispatchEvent(evt);
|
||||
}
|
||||
if(e.startsWith("message=") || e.startsWith("label=")){
|
||||
this.labelElement.value = e.split("=")[1];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.shadowRoot.getElementById('send_max').addEventListener('click', event=>{
|
||||
this.dispatchSendMax()
|
||||
});
|
||||
|
||||
|
||||
this.removeElement.addEventListener('click', event=>{
|
||||
this.dispatchRemove()
|
||||
});
|
||||
|
||||
this.amountElement.addEventListener('input', event=>{
|
||||
this.calculateConvertedUnit()
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
markAdddressGreenIfMine(){
|
||||
// mark own addresses green
|
||||
this.isAddressMyOwn().then((isMine) => {
|
||||
this.markRecipient(isMine);
|
||||
});
|
||||
}
|
||||
|
||||
dispatchAddressOnInput(result) {
|
||||
this.markAdddressGreenIfMine();
|
||||
this.dispatchEvent(new CustomEvent('address-input'));
|
||||
}
|
||||
|
||||
dispatchRemove(result) {
|
||||
this.dispatchEvent(new CustomEvent('remove'));
|
||||
}
|
||||
|
||||
dispatchSendMax(result) {
|
||||
this.dispatchEvent(new CustomEvent('send-max'));
|
||||
}
|
||||
|
||||
|
||||
|
||||
// this function together with attributeChangedCallback ensures that setting attributes directly in html
|
||||
// like <recipient-box title='title'></div> works.
|
||||
static get observedAttributes() {
|
||||
// define the attributes that can be defined via <recipient-box name="recipient-box" id='4'></recipient-box>
|
||||
var attributes = ["value", "name", "id", "title", "address", "label", "unit",
|
||||
"recipientId", "max", "step", "amount", "hiddenRemoveButton"];
|
||||
return attributes;
|
||||
}
|
||||
|
||||
attributeChangedCallback(attribute, oldValue, newValue) {
|
||||
if (oldValue == newValue){return}
|
||||
if (attribute=="value"){
|
||||
this.value = newValue;
|
||||
} else if (attribute=="name"){
|
||||
this.name = newValue;
|
||||
} else if (attribute=="address"){
|
||||
this.address = newValue;
|
||||
} else if (attribute=="title"){
|
||||
this.title = newValue;
|
||||
} else if (attribute=="recipientId"){
|
||||
this.recipientId = newValue;
|
||||
} else if (attribute=="label"){
|
||||
this.label = newValue;
|
||||
} else if (attribute=="unit"){
|
||||
this.unit = newValue;
|
||||
} else if (attribute=="step"){
|
||||
this.step = newValue;
|
||||
} else if (attribute=="max"){
|
||||
this.max = newValue;
|
||||
} else if (attribute=="amount"){
|
||||
this.amount = newValue;
|
||||
} else if (attribute=="id"){
|
||||
this.id = newValue;
|
||||
} else if (attribute=="hiddenRemoveButton"){
|
||||
this.hiddenRemoveButton = newValue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// title
|
||||
set title(newValue){
|
||||
this.titleElement.innerText = newValue;
|
||||
}
|
||||
get title(){
|
||||
return this.titleElement.innerText;
|
||||
}
|
||||
|
||||
// address
|
||||
set address(newValue){
|
||||
this.addressElement.value = newValue;
|
||||
this.markAdddressGreenIfMine();
|
||||
}
|
||||
get address(){
|
||||
return this.addressElement.value;
|
||||
}
|
||||
|
||||
// label
|
||||
set label(newValue){
|
||||
this.labelElement.value = newValue;
|
||||
}
|
||||
get label(){
|
||||
return this.labelElement.value;
|
||||
}
|
||||
|
||||
// unit
|
||||
set unit(newValue){
|
||||
// this function is different for bitcoin and liquid
|
||||
this.setUnit(newValue);
|
||||
}
|
||||
get unit(){
|
||||
// this function is different for bitcoin and liquid
|
||||
return this.getUnit();
|
||||
}
|
||||
|
||||
// step
|
||||
set step(newValue){
|
||||
this.amountElement.step = newValue;
|
||||
}
|
||||
get step(){
|
||||
return parseFloat(this.amountElement.step );
|
||||
}
|
||||
|
||||
// amount
|
||||
set amount(newValue){
|
||||
this.amountElement.value = newValue;
|
||||
this.calculateConvertedUnit()
|
||||
}
|
||||
get amount(){
|
||||
var value = parseFloat(this.amountElement.value );
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
// max
|
||||
set max(newValue){
|
||||
this.amountElement.max = newValue;
|
||||
}
|
||||
get max(){
|
||||
return parseFloat(this.amountElement.max );
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// value
|
||||
set value(newValue) {
|
||||
// loop through all the keys and set the attributes
|
||||
for (var key in newValue) {
|
||||
this.attributeChangedCallback(key, null, newValue[key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a dict with important attributes
|
||||
// the keys are in python style, because they will be later sent to the python server
|
||||
get value() {
|
||||
return {
|
||||
"unit":this.unit,
|
||||
"amount":this.amount,
|
||||
"btc_amount":this.btcAmount,
|
||||
"recipient_id":this.recipientId,
|
||||
"label":this.label,
|
||||
"address":this.address,
|
||||
}
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
value: {
|
||||
type: String
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// btcAmount
|
||||
get btcAmount(){
|
||||
return (this.unit == 'sat' ? this.amount / 1e8 : this.amount);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// hiddenRemoveButton
|
||||
set hiddenRemoveButton(newValue){
|
||||
if (!this.shadowRoot) {return }
|
||||
this.removeElement.hidden = newValue;
|
||||
}
|
||||
get hiddenRemoveButton(){
|
||||
return this.removeElement.hidden;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
updateUnitLabelAndStep() {
|
||||
let unitLabelEl = this.shadowRoot.getElementById('converted_unit_label');
|
||||
if(this.unit == 'sat'){
|
||||
unitLabelEl.innerHTML = 'BTC';
|
||||
}else if(this.unit == 'btc'){
|
||||
unitLabelEl.innerHTML = 'sat';
|
||||
}else{
|
||||
unitLabelEl.innerHTML = '';
|
||||
}
|
||||
this.step = this.unit == 'sat' ? 1 : 1e-8;
|
||||
this.calculateConvertedUnit();
|
||||
}
|
||||
|
||||
calculateConvertedUnit() {
|
||||
let convertedAmount = parseFloat(Number.parseFloat(this.amount / (this.unit == 'sat' ? 1e8 : 1e-8)).toFixed((this.unit == 'sat' ? 8 : 0)));
|
||||
if (convertedAmount == 'NaN') {
|
||||
convertedAmount = '-'
|
||||
}
|
||||
|
||||
if(this.unit == 'sat' || this.unit == 'btc'){
|
||||
this.shadowRoot.getElementById('converted_unit_amount').innerHTML = (isNaN(convertedAmount) ? '-' : convertedAmount.toFixed(8).replace(/(\.0+|0+)$/, ''));
|
||||
{% if specter.price_check %}
|
||||
let altRate = parseFloat('{{ specter.alt_rate }}');
|
||||
let altSymbol = '{{ specter.alt_symbol }}';
|
||||
let altAmount = parseFloat((altRate * this.btcAmount).toFixed(2))
|
||||
if (!isNaN(altAmount) && (altSymbol && altRate)) {
|
||||
this.shadowRoot.getElementById('converted_unit_alt').innerHTML = ' (' + altAmount + altSymbol + ')';
|
||||
} else {
|
||||
this.shadowRoot.getElementById('converted_unit_alt').innerHTML = '';
|
||||
}
|
||||
{% endif %}
|
||||
}else{
|
||||
this.shadowRoot.getElementById('converted_unit_amount').innerHTML = ' ';
|
||||
this.shadowRoot.getElementById('converted_unit_alt').innerHTML = ' ';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
async isAddressMyOwn() {
|
||||
let address = this.addressElement.value;
|
||||
if (!address){return;}
|
||||
|
||||
let url="{{ url_for('wallets_endpoint_api.is_address_mine', wallet_alias=wallet.alias, address='this_address') }}"
|
||||
url = url.replace('this_address', address)
|
||||
|
||||
let is_address_mine = send_request(url, 'GET', "{{ csrf_token() }}");
|
||||
return is_address_mine;
|
||||
}
|
||||
|
||||
markRecipient(is_address_mine){
|
||||
let html_address = this.addressElement;
|
||||
if (is_address_mine) {
|
||||
html_address.style.backgroundColor= 'var(--cmap-bg-address-is-mine)';
|
||||
} else {
|
||||
html_address.style.backgroundColor= "rgba(1, 1, 1, 0)";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
customElements.define('recipient-box', RecipientBox);
|
||||
</script>
|
||||
|
|
@ -2,11 +2,7 @@
|
|||
{% set tab = 'send' %}
|
||||
{% block content %}
|
||||
|
||||
<style>
|
||||
.max-btn:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
<style>
|
||||
#calculated_tx_fee_label:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
|
|
@ -14,6 +10,7 @@
|
|||
</style>
|
||||
|
||||
{% 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 @@
|
|||
<form action="{{ url_for('wallets_endpoint.send_new',wallet_alias=wallet_alias) }}" id="send-form" method="POST" style="width: 100%;">
|
||||
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="hidden" name="rbf_tx_id" value="{{ rbf_tx_id }}"/>
|
||||
<input type="hidden" name="recipient_dicts" id="recipient_dicts" value=""/>
|
||||
<h1 class="padded">{{ _("Create Transaction") }}</h1>
|
||||
<div style="display: flex; justify-content: center; margin-bottom: 15px">
|
||||
<div>{{ _("Available funds:") }} {{wallet.amount_available | btcunitamount}}
|
||||
|
|
@ -62,12 +60,13 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card" style="margin: auto;">
|
||||
<div id="recipients" {% if ui_option != 'ui' %}class="hidden"{% endif %}></div>
|
||||
<div class="card" style="margin: auto;">
|
||||
<div id="recipients" {% if ui_option != 'ui' %}class="hidden"{% else %} class="recipient_wrapper" {% endif %}></div>
|
||||
<img src="{{ url_for('static', filename='img/add-person.svg') }}" id="add-recipient" title="Add a recipient" class="recipient_button recipient_add" style="float:right" onclick="addRecipient('', '', 'btc', '')">
|
||||
<div id="recipients-txt-container" {% if ui_option == 'ui' %}class="hidden"{% endif %}>
|
||||
{{ _("Unit:") }}
|
||||
<label><input type="radio" class="inline" style="margin: 0 5px;" name="amount_unit_text" value="sat" onchange="toggleUnit(this, 'text')" {% if specter.unit == 'sat' %}checked{% endif %}>sat</label>
|
||||
<label><input type="radio" class="inline" style="margin: 0 5px;" name="amount_unit_text" value="btc" onchange="toggleUnit(this, 'text')" {% if specter.unit != 'sat' %}checked{% endif %}>BTC</label><br>
|
||||
<label><input type="radio" class="inline" style="margin: 0 5px;" name="amount_unit_text" value="sat" onchange="toggleTextUnit(this)" {% if specter.unit == 'sat' %}checked{% endif %}>sat</label>
|
||||
<label><input type="radio" class="inline" style="margin: 0 5px;" name="amount_unit_text" value="btc" onchange="toggleTextUnit(this)" {% if specter.unit != 'sat' %}checked{% endif %}>BTC</label><br>
|
||||
|
||||
<textarea id="recipients-txt" name="recipients" placeholder="{{ _('Enter recipient address, amount') }}" style="margin-top: 5px; font-size: 0.95em;" onblur="validateForm()">{{ recipients_txt }}</textarea>
|
||||
<p class="note" style="line-height: 2; background-color: #16202d; padding: 10px; border-radius: 7px;">
|
||||
|
|
@ -77,10 +76,9 @@
|
|||
<{{ _("ADDRESS") }}3>, <{{ _("AMOUNT") }}3><br>
|
||||
</p>
|
||||
</div>
|
||||
<button id="remove-recipient" style="width: 200px; height: 38px;" type="button" class="btn hidden" onclick="removeRecipient()"><b>−</b> {{ _("Remove recipient") }}</button>
|
||||
<p><span id="calculated_tx_fee_label" class="note" style="margin-top: 17px;" onclick="calculateEstimatedFee()">{{ _("Calculate estimated fee") }}</span><span class="note" id="calculated_tx_fee"></span><p>
|
||||
<span id="toggle_advanced" style="cursor: pointer;">{{ _("Advanced") }} {% if show_advanced_settings %}▼{% else %}▶{% endif %}</span>
|
||||
<br><br>
|
||||
<br>
|
||||
<div id="advanced_settings" style="margin: auto; max-width: 90%; display: {% if show_advanced_settings %}block{% else %}none{% endif %};">
|
||||
<div>{{ _("Transaction editor:") }}
|
||||
<label><input type="radio" class="inline" style="margin: 0 10px 0 20px;" name="ui_option" value="ui" onclick="toggleSendUIType(this)" {% if ui_option == 'ui' %}checked{% endif %} id="ui-radio-btn">UI</label>
|
||||
|
|
@ -91,9 +89,7 @@
|
|||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<div class="row break-row-mobile" id="coin-selection-row" style="margin-top: 30px;">
|
||||
<button id="add-recipient" style="width: 200px; height: 38px;" type="button" class="btn" onclick="addRecipient('', 0, 'btc', '')"><svg width="20" height="20" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg> {{ _("Add recipient") }} </button>
|
||||
</div><br>
|
||||
<div class="row break-row-mobile" id="vertical-space-for-subtract-fees-from-amount-checkbox" style="margin-top: 0px;"></div>
|
||||
</div>
|
||||
{% include "includes/tx-row.html" %}
|
||||
{% include "includes/tx-data.html" %}
|
||||
|
|
@ -112,10 +108,8 @@
|
|||
</form>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
|
||||
{% if specter.is_liquid %}
|
||||
const MIN_FEE_RATE = 0.1;
|
||||
const NETWORK = "Liquid"
|
||||
|
|
@ -124,9 +118,52 @@
|
|||
const NETWORK = "Bitcoin"
|
||||
{% endif %}
|
||||
|
||||
function getRecipientIds() {
|
||||
var ids = [];
|
||||
var children = document.getElementById(`recipients`).children;
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
ids.push(children[i].recipientId)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// combines all html recipients to a list
|
||||
function getRecipients() {
|
||||
var data = [];
|
||||
|
||||
var recipientIds = getRecipientIds();
|
||||
for (var i in recipientIds) {
|
||||
var recipientId = recipientIds[i];
|
||||
let recipient = document.getElementById(`recipient_${recipientId}`);
|
||||
data.push(recipient);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
// combines all recipient.value's to a list
|
||||
function getRecipientDicts() {
|
||||
var data = [];
|
||||
|
||||
var recipients = getRecipients();
|
||||
for (var i in recipients) {
|
||||
data.push(recipients[i].value);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function setVisibilityRemoveRecipientButton(){
|
||||
var recipientList = getRecipients();
|
||||
var hidden = recipientList.length == 1
|
||||
for (var i in recipientList) {
|
||||
recipientList[i].hiddenRemoveButton = hidden;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Amounts and units
|
||||
var units = [];
|
||||
var amounts = [];
|
||||
var textUnit = '{{ specter.unit }}';
|
||||
// TODO: use total balance including unconfirmed
|
||||
var assetBalances = {
|
||||
|
|
@ -176,7 +213,7 @@
|
|||
|
||||
// Main part of displaying subtractFrom, the rest is with addRecipient, deleteRecipient and toggleSendUIType
|
||||
FeeSelectionComponent.addEventListener("subtractClick", (event) => {
|
||||
if ((amounts.length > 1 || !document.getElementById('ui-radio-btn').checked) && document.getElementById('subtract').checked) {
|
||||
if ((getRecipientIds().length > 1 || !document.getElementById('ui-radio-btn').checked) && document.getElementById('subtract').checked) {
|
||||
FeeSelectionComponent.showSubtractFrom(true)
|
||||
if (!document.getElementById('ui-radio-btn').checked) {
|
||||
FeeSelectionComponent.addLineBreaks(2)
|
||||
|
|
@ -190,181 +227,107 @@
|
|||
}
|
||||
})
|
||||
|
||||
function addRecipient(addr, amount, amount_unit, label) {
|
||||
let i = amounts.length;
|
||||
amounts.push(amount);
|
||||
units.push(amount_unit);
|
||||
let valueOrPlaceholder = amount == 0 ? 'placeholder="0"' : `value=${amount}`
|
||||
if (amount_unit == 'sat') {
|
||||
amount = parseFloat(Number.parseFloat(amount * 1e8).toFixed(0));
|
||||
if (amount == 'NaN') {
|
||||
amount = '-'
|
||||
}
|
||||
}
|
||||
let step = 1;
|
||||
if (amount_unit == 'btc') {
|
||||
step = 1e-8;
|
||||
}
|
||||
let satChecked = amount_unit == 'btc' ? '' : 'checked';
|
||||
let btcChecked = amount_unit == 'btc' ? 'checked' : '';
|
||||
let convertedUnit = amount_unit == 'btc' ? 'sat' : 'BTC';
|
||||
{% if specter.is_liquid and wallet.balance.get("assets", {}) %}
|
||||
let assetSelector = `
|
||||
<select name="amount_unit_${i}" style="width: 100px;" onchange="toggleUnit(this, ${i})">
|
||||
<option value="btc">LBTC</option>
|
||||
<option value="sat">L-sat (10⁻⁸ LBTC)</option>
|
||||
{% for asset in wallet.balance.get("assets",{}).keys() | sort %}
|
||||
<option value="{{asset}}">{{asset | assetlabel}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
`;
|
||||
{% else %}
|
||||
let assetSelector = `
|
||||
<label><input type="radio" class="inline" style="margin: 0 5px;" name="amount_unit_${i}" value="sat" onchange="toggleUnit(this, ${i})" ${satChecked}>sat</label>
|
||||
<label><input type="radio" class="inline" style="margin: 0 5px;" name="amount_unit_${i}" value="btc" onchange="toggleUnit(this, ${i})" ${btcChecked}>BTC</label>
|
||||
`;
|
||||
{% endif %}
|
||||
let recipientForm = `
|
||||
<div id="recipient_${i}" style="border-bottom: 1px solid var(--cmap-border); padding-bottom: 10px; margin-bottom: 10px;">
|
||||
{{ _("Recipient address:") }}<br>
|
||||
<div class="row">
|
||||
<input type="text" id="address_${i}" name="address_${i}" oninput="validateForm()" value="${addr}">
|
||||
<qr-scanner id="address-scan-${i}" style="margin-top: 3px;">
|
||||
<a slot="button" class="btn" style="height: 35px;">
|
||||
<img src="{{ url_for('static', filename='img/qr-code.svg') }}" style="width: 26px; margin: 0px;" class="svg-white"> {{ _("Scan") }}</a>
|
||||
</qr-scanner>
|
||||
</div>
|
||||
<br>
|
||||
{{ _("Address label (optional):") }}<br>
|
||||
<input type="text" id="label_${i}" name="label_${i}" value="${label}">
|
||||
<br><br>
|
||||
{{ _("Amount:") }}<br>
|
||||
<input style="width: 200px" type="number" name="amount_${i}" oninput="calculateConvertedUnit(${i})" id="amount_${i}" min=0 step="${step}" autocomplete="off" ${valueOrPlaceholder}>
|
||||
<input type="hidden" name="btc_amount_${i}" id="btc_amount_${i}">
|
||||
<div class="mobile-only" style="margin-top: 10px;"></div>
|
||||
${assetSelector}
|
||||
<span class="note max-btn" style="margin-left: 5px;" id="send_max_${i}" onclick="setMaxAmount(${i})">({{ _("send max") }})</span>
|
||||
<div>
|
||||
<span class="note" id="converted_unit_amount_${i}">-</span> <span class="note" id="converted_unit_label_${i}">${convertedUnit}</span> <span class="note" id="converted_unit_alt_${i}"></span>
|
||||
</div>
|
||||
<div>
|
||||
`
|
||||
|
||||
let recipients = document.getElementById('recipients');
|
||||
let newRecipient = document.createElement('div');
|
||||
newRecipient.innerHTML = recipientForm;
|
||||
function addRecipient(address, amount, amount_unit, label) {
|
||||
let recipientIds = getRecipientIds();
|
||||
recipientId = 0;
|
||||
if (recipientIds.length > 0) {
|
||||
recipientId = Math.max(...recipientIds)+1
|
||||
};
|
||||
|
||||
// This adds the recipient
|
||||
let recipients = document.getElementById('recipients');
|
||||
let newRecipient = new RecipientBox();
|
||||
newRecipient.id = `recipient_${recipientId}`;
|
||||
newRecipient.recipientId = recipientId;
|
||||
newRecipient.title = `Recipient ${recipientId+1}`;
|
||||
newRecipient.address = address;
|
||||
newRecipient.amount = amount;
|
||||
newRecipient.unit = amount_unit;
|
||||
newRecipient.label = label;
|
||||
newRecipient.addEventListener('remove', (event) => {
|
||||
removeRecipient(newRecipient.recipientId);
|
||||
})
|
||||
newRecipient.addEventListener('address-input', (event) => {
|
||||
validateForm();
|
||||
})
|
||||
newRecipient.addEventListener('unit-change', (event) => {
|
||||
validateForm();
|
||||
})
|
||||
newRecipient.addEventListener('send-max', (event) => {
|
||||
setMaxAmount(newRecipient.recipientId);
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
recipients.appendChild(newRecipient);
|
||||
document.getElementById('address-scan-' + i).addEventListener('scan', e=>{
|
||||
let addr = e.detail.result;
|
||||
if(addr == null){
|
||||
return;
|
||||
}
|
||||
// remove bitcoin: stuff
|
||||
if(addr.indexOf("bitcoin:") >= 0){
|
||||
addr = addr.substr(addr.indexOf("bitcoin:")+8);
|
||||
}
|
||||
let arr = addr.split("?");
|
||||
addr = arr[0];
|
||||
document.getElementById("address_" + i).value = addr;
|
||||
let evt = new Event('input');
|
||||
document.getElementById("address_" + i).dispatchEvent(evt);
|
||||
// parse metadata like amount and message
|
||||
if(arr.length > 1){
|
||||
arr = arr[1].split("&");
|
||||
arr.forEach((e)=>{
|
||||
if(e.startsWith("amount=")){
|
||||
let val = parseFloat(e.substr(7));
|
||||
if(units[i] == 'sat'){
|
||||
val = Math.round(val*1e8);
|
||||
}
|
||||
document.getElementById("amount_" + i).value = val;
|
||||
let evt = new Event('input');
|
||||
document.getElementById("amount_" + i).dispatchEvent(evt);
|
||||
}
|
||||
if(e.startsWith("message=") || e.startsWith("label=")){
|
||||
document.getElementById("label_" + i).value = e.split("=")[1];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if (amounts.length == 1) {
|
||||
document.getElementById('remove-recipient').style.display = 'none';
|
||||
document.getElementById('coin-selection-row').style['margin-top'] = '30px';
|
||||
|
||||
// This adds the id's to the subtract_from_recipient_id_select
|
||||
var feeSelectionComponent = document.getElementById('fee-selection-component');
|
||||
var subtractFromRecipientIdSelect = feeSelectionComponent.subtractFrom.children.subtract_from_recipient_id_select;
|
||||
var opt = document.createElement('option');
|
||||
opt.value = recipientId;
|
||||
opt.innerHTML = newRecipient.title;
|
||||
subtractFromRecipientIdSelect.appendChild(opt);
|
||||
|
||||
|
||||
// Make the remove button visible again for all recipient components
|
||||
setVisibilityRemoveRecipientButton();
|
||||
|
||||
// control visibility and vertical spaces
|
||||
if (getRecipientIds().length == 1) {
|
||||
FeeSelectionComponent.showSubtractFrom(false);
|
||||
document.getElementById('vertical-space-for-subtract-fees-from-amount-checkbox').style['margin-top'] = '0px';
|
||||
} else {
|
||||
document.getElementById('remove-recipient').style.display = 'block';
|
||||
document.getElementById('coin-selection-row').style['margin-top'] = '90px';
|
||||
document.getElementById('vertical-space-for-subtract-fees-from-amount-checkbox').style['margin-top'] = '60px';
|
||||
if (document.getElementById('subtract').checked) {
|
||||
FeeSelectionComponent.showSubtractFrom(true)
|
||||
}
|
||||
}
|
||||
calculateConvertedUnit(i);
|
||||
|
||||
}
|
||||
|
||||
function removeRecipient() {
|
||||
let i = amounts.length - 1;
|
||||
let recipient = document.getElementById('recipient_' + i);
|
||||
recipient.parentNode.removeChild(recipient);
|
||||
amounts.splice(i, 1);
|
||||
units.splice(i, 1);
|
||||
if (amounts.length == 1) {
|
||||
document.getElementById('remove-recipient').style.display = 'none';
|
||||
function removeRecipient(recipientId=-1) {
|
||||
if (recipientId==-1) {
|
||||
recipientId = getRecipientIds().length - 1
|
||||
};
|
||||
let recipients = document.getElementById('recipients');
|
||||
|
||||
// This removes the recipient div
|
||||
for (var i = recipients.children.length -1; i >= 0; i--) {
|
||||
let child = recipients.children[i];
|
||||
if (child.recipientId == recipientId) {
|
||||
recipients.removeChild(child)
|
||||
}
|
||||
}
|
||||
|
||||
// This removes the recipientId from the subtract_from_recipient_id_select
|
||||
var subtractFromRecipientIdSelect = FeeSelectionComponent.subtractFrom.children.subtract_from_recipient_id_select;
|
||||
for (var i = subtractFromRecipientIdSelect.children.length -1; i >= 0; i--) {
|
||||
let child = subtractFromRecipientIdSelect.children[i];
|
||||
if (child.value == recipientId) {
|
||||
subtractFromRecipientIdSelect.removeChild(child)
|
||||
}
|
||||
}
|
||||
|
||||
// Not strictly necessary but better to have it to avoid any divergence
|
||||
FeeSelectionComponent.subtractUpdated();
|
||||
|
||||
// Hide the remove button if we only have one recipient
|
||||
setVisibilityRemoveRecipientButton();
|
||||
|
||||
// control visibility and vertical spaces
|
||||
if (getRecipientIds().length == 1) {
|
||||
FeeSelectionComponent.showSubtractFrom(false);
|
||||
document.getElementById('coin-selection-row').style['margin-top'] = '30px';
|
||||
document.getElementById('vertical-space-for-subtract-fees-from-amount-checkbox').style['margin-top'] = '0px';
|
||||
} else {
|
||||
document.getElementById('remove-recipient').style.display = 'block';
|
||||
if (document.getElementById('subtract').checked) {
|
||||
document.getElementById('subtract_from').style.display = 'block';
|
||||
}
|
||||
document.getElementById('coin-selection-row').style['margin-top'] = '90px';
|
||||
document.getElementById('vertical-space-for-subtract-fees-from-amount-checkbox').style['margin-top'] = '60px';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleUnit(unitSelected, i) {
|
||||
if (i == 'text') {
|
||||
textUnit = unitSelected.value
|
||||
} else {
|
||||
units[i] = unitSelected.value;
|
||||
let unitLabelEl = document.getElementById('converted_unit_label_' + i);
|
||||
if(units[i] == 'sat'){
|
||||
unitLabelEl.innerHTML = 'BTC';
|
||||
}else if(units[i] == 'btc'){
|
||||
unitLabelEl.innerHTML = 'sat';
|
||||
}else{
|
||||
unitLabelEl.innerHTML = '';
|
||||
}
|
||||
document.getElementById('amount_' + i).setAttribute('step', units[i] == 'sat' ? '1' : '1e-8');
|
||||
calculateConvertedUnit(i);
|
||||
}
|
||||
}
|
||||
|
||||
function calculateConvertedUnit(i) {
|
||||
let amountInput = document.getElementById('amount_' + i);
|
||||
amounts[i] = parseFloat(amountInput.value);
|
||||
|
||||
let convertedAmount = parseFloat(Number.parseFloat(amounts[i] / (units[i] == 'sat' ? 1e8 : 1e-8)).toFixed((units[i] == 'sat' ? 8 : 0)));
|
||||
if (convertedAmount == 'NaN') {
|
||||
convertedAmount = '-'
|
||||
}
|
||||
document.getElementById('btc_amount_' + i).value = (units[i] == 'sat' ? amounts[i] / 1e8 : amounts[i]);
|
||||
|
||||
if(units[i] == 'sat' || units[i] == 'btc'){
|
||||
document.getElementById('converted_unit_amount_' + i).innerHTML = (isNaN(convertedAmount) ? '-' : convertedAmount.toFixed(8).replace(/(\.0+|0+)$/, ''));
|
||||
{% if specter.price_check %}
|
||||
let altRate = parseFloat('{{ specter.alt_rate }}');
|
||||
let altSymbol = '{{ specter.alt_symbol }}';
|
||||
let altAmount = parseFloat((altRate * parseFloat(document.getElementById('btc_amount_' + i).value)).toFixed(2))
|
||||
if (!isNaN(altAmount) && (altSymbol && altRate)) {
|
||||
document.getElementById('converted_unit_alt_' + i).innerHTML = ' (' + altAmount + altSymbol + ')';
|
||||
} else {
|
||||
document.getElementById('converted_unit_alt_' + i).innerHTML = '';
|
||||
}
|
||||
{% endif %}
|
||||
}else{
|
||||
document.getElementById('converted_unit_amount_' + i).innerHTML = ' ';
|
||||
document.getElementById('converted_unit_alt_' + i).innerHTML = ' ';
|
||||
}
|
||||
|
||||
validateForm();
|
||||
function toggleTextUnit(unitSelected) {
|
||||
textUnit = unitSelected.value
|
||||
}
|
||||
|
||||
function isAboveWalletBalance(unit, amount) {
|
||||
|
|
@ -386,22 +349,25 @@
|
|||
return (unit == 'sat' ? amount / 1e8 : amount) < 1e-8;
|
||||
}
|
||||
|
||||
async function setMaxAmount(i) {
|
||||
async function setMaxAmount(recipientId) {
|
||||
const recipients = getRecipients();
|
||||
const recipient = document.getElementById(`recipient_${recipientId}`);
|
||||
FeeSelectionComponent.showSubtractFrom(true)
|
||||
FeeSelectionComponent.setSubtractFrom(i + 1)
|
||||
let amountInput = document.getElementById('amount_' + i);
|
||||
if (!validAddress(i)) {
|
||||
FeeSelectionComponent.setSubtractFrom(recipientId)
|
||||
if (!validAddress(recipientId)) {
|
||||
return;
|
||||
}
|
||||
FeeSelectionComponent.setSubtract(units[i] == 'sat' || units[i] == 'btc' || units[i] == 'lbtc');
|
||||
FeeSelectionComponent.setSubtract(recipient.unit == 'sat' || recipient.unit == 'btc' || recipient.unit == 'lbtc');
|
||||
|
||||
let othersAmount = 0;
|
||||
for(let j in amounts) {
|
||||
if (j != i) {
|
||||
let unit = units[j];
|
||||
let amount = amounts[j];
|
||||
for(let j in recipients) {
|
||||
other_recipient = recipients[j] ;
|
||||
if (other_recipient.recipientId != recipientId) {
|
||||
let unit = other_recipient.unit;
|
||||
let amount = other_recipient.amount;
|
||||
// assets
|
||||
if(units[i] != 'sat' || units[i] != 'btc'){
|
||||
if(unit == units[i]){
|
||||
if(other_recipient.unit != 'sat' || other_recipient.unit != 'btc'){
|
||||
if(unit == recipient.unit){
|
||||
othersAmount += amount;
|
||||
}
|
||||
// btc
|
||||
|
|
@ -412,8 +378,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
let maxAmount = (coinselectionWebcomponent.getSpendableAmount(units[i]) - (units[i] == 'sat' ? othersAmount * 1e8 : othersAmount));
|
||||
if (units[i] == 'sat') {
|
||||
|
||||
let maxAmount = (coinselectionWebcomponent.getSpendableAmount(recipient.unit) - (recipient.unit == 'sat' ? othersAmount * 1e8 : othersAmount));
|
||||
if (recipient.unit == 'sat') {
|
||||
maxAmount = Math.round(maxAmount);
|
||||
} else {
|
||||
maxAmount = parseFloat(maxAmount.toFixed(8));
|
||||
|
|
@ -421,23 +388,20 @@
|
|||
if (maxAmount < 0) {
|
||||
maxAmount = 0;
|
||||
}
|
||||
amountInput.value = maxAmount;
|
||||
|
||||
calculateConvertedUnit(i);
|
||||
document.getElementById(`recipient_${recipientId}`).amount = maxAmount;
|
||||
}
|
||||
|
||||
// Form validation
|
||||
function validateAmount(unit, amount, i, allowZero=false) {
|
||||
function validateAmount(unit, amount, recipientId, allowZero=false) {
|
||||
if (isNaN(amount)) {
|
||||
showError(`{{ _("Amount entered is invalid!") }}`, 5000);
|
||||
return false;
|
||||
}
|
||||
if (i) {
|
||||
let amountInput = document.getElementById('amount_' + i);
|
||||
if (recipientId) {
|
||||
if (coinselectionWebcomponent != null) {
|
||||
amountInput.max = coinselectionWebcomponent.getSpendableAmount(unit);
|
||||
document.getElementById(`recipient_${recipientId}`).max = coinselectionWebcomponent.getSpendableAmount(unit);
|
||||
} else {
|
||||
amountInput.max = spendableAmount
|
||||
document.getElementById(`recipient_${recipientId}`).max = spendableAmount
|
||||
}
|
||||
}
|
||||
if (!amount && !allowZero) {
|
||||
|
|
@ -463,19 +427,19 @@
|
|||
}
|
||||
|
||||
// Returns true if the address is valid
|
||||
function validAddress(i) {
|
||||
function validAddress(recipientId) {
|
||||
let reWhite = /\s/;
|
||||
let addressInput = document.getElementById('address_' + i);
|
||||
if (!addressInput.value) {
|
||||
let address = document.getElementById(`recipient_${recipientId}`).address;
|
||||
if (!address) {
|
||||
showError(`{{ _("You provided no address.") }}`, 5000);
|
||||
return false
|
||||
}
|
||||
// Segwit addresses are always between 14 and 74 characters long.
|
||||
else if (addressInput.value.length < 14) {
|
||||
else if (address.length < 14) {
|
||||
showError(`{{ _("Please provide a valid address!") }}`, 5000);
|
||||
return false
|
||||
}
|
||||
else if (reWhite.test(addressInput.value)) {
|
||||
else if (reWhite.test(address)) {
|
||||
showError(`{{ _("Looks like there are whitespaces in the address field.") }}`, 5000);
|
||||
return false
|
||||
}
|
||||
|
|
@ -501,6 +465,9 @@
|
|||
}
|
||||
|
||||
function validateForm(submitted=false) {
|
||||
const recipients = getRecipients();
|
||||
// this assures that the 'recipient_dicts' input has the newest recipientDicts
|
||||
document.getElementById('recipient_dicts').value = JSON.stringify(getRecipientDicts());
|
||||
console.log("validateForm is called")
|
||||
let createPSBTButton = document.getElementById('create_psbt_btn');
|
||||
// Disables submit but button still clickable before every validation
|
||||
|
|
@ -508,28 +475,26 @@
|
|||
let totalAmount = 0.0;
|
||||
let assetAmounts = {};
|
||||
if (document.getElementById('ui-radio-btn').checked) {
|
||||
for(let i in amounts) {
|
||||
let unit = units[i]
|
||||
let amount = amounts[i]
|
||||
if(unit == 'btc' || unit == 'sat'){
|
||||
totalAmount += (unit == 'sat' ? amount / 1e8 : amount);
|
||||
for(let i in recipients) {
|
||||
let recipient = recipients[i];
|
||||
if(recipient.unit == 'btc' || recipient.unit == 'sat'){
|
||||
totalAmount += (recipient.unit == 'sat' ? recipient.amount / 1e8 : recipient.amount);
|
||||
}else{
|
||||
if(unit in assetAmounts){
|
||||
assetAmounts[unit] += amount;
|
||||
if(recipient.unit in assetAmounts){
|
||||
assetAmounts[recipient.unit] += recipient.amount;
|
||||
}else{
|
||||
assetAmounts[unit] = amount;
|
||||
assetAmounts[recipient.unit] = recipient.amount;
|
||||
}
|
||||
}
|
||||
if (document.getElementById("amount_" + i).value == '') {
|
||||
amount = 0;
|
||||
}
|
||||
|
||||
|
||||
if (submitted) {
|
||||
// Check address
|
||||
if (!validAddress(i)) {
|
||||
if (!validAddress(recipient.recipientId)) {
|
||||
return;
|
||||
}
|
||||
// Check amount
|
||||
if (!validateAmount(unit, amount, i)) {
|
||||
if (!validateAmount(recipient.unit, recipient.amount, recipient.recipientId)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -558,7 +523,10 @@
|
|||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Necessary since recipients can be removed which leades to
|
||||
// mismatches between the index in the get_recipient_list and the selected recipientId
|
||||
indexSubtractFromRecipient = getRecipientIds().indexOf(parseInt(FeeSelectionComponent.subtractFromRecipientIdSelect.value, 10));
|
||||
FeeSelectionComponent.setSubtractFrom(indexSubtractFromRecipient)
|
||||
createPSBTButton.setAttribute('type', 'submit');
|
||||
return true;
|
||||
}
|
||||
|
|
@ -587,16 +555,12 @@
|
|||
setVisibility('add-recipient', 'flex');
|
||||
setVisibility('recipients-txt-container', 'none');
|
||||
setVisibility('subtract', 'block');
|
||||
if (amounts.length > 1) {
|
||||
setVisibility('remove-recipient', 'block');
|
||||
}
|
||||
else {
|
||||
if (getRecipientIds().length <= 1) {
|
||||
FeeSelectionComponent.showSubtractFrom(false)
|
||||
}
|
||||
} else {
|
||||
setVisibility('recipients', 'none');
|
||||
setVisibility('add-recipient', 'none');
|
||||
setVisibility('remove-recipient', 'none');
|
||||
setVisibility('recipients-txt-container', 'block');
|
||||
// Assumes that text is always used for multiple recipients
|
||||
if (document.getElementById('subtract').checked) {
|
||||
|
|
@ -612,23 +576,12 @@
|
|||
if (await validateForm(true) !== true) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
try {
|
||||
var formData = new FormData(document.getElementById('send-form'));
|
||||
let url="{{ url_for('wallets_endpoint_api.estimate_fee', wallet_alias=wallet.alias) }}"
|
||||
formData.append("estimate_fee", true)
|
||||
const response = await fetch(
|
||||
url,
|
||||
{
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}
|
||||
);
|
||||
if(response.status != 200){
|
||||
showError(await response.text());
|
||||
console.log("Error while fetching fees")
|
||||
return
|
||||
}
|
||||
let result = await response.json();
|
||||
|
||||
let result = await send_request(url, 'POST', "{{ csrf_token() }}", formData);
|
||||
console.log(result);
|
||||
if (result.success) {
|
||||
let psbt = result.psbt;
|
||||
|
|
@ -653,18 +606,20 @@
|
|||
return -1;
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function(){
|
||||
{% if fillform %}
|
||||
{% for addr, amount, amount_unit, label in recipients %}
|
||||
addRecipient("{{ addr }}", {{ amount }}, "{{ amount_unit }}", "{{ label }}");
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
addRecipient("", 0, "btc", "");
|
||||
{% endif %}
|
||||
adjustForLiquid()
|
||||
document.getElementById('toggle_advanced').addEventListener('click', (event) => {
|
||||
toggleAdvanced();
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
{% if fillform %}
|
||||
{% for addr, amount, amount_unit, label in recipients %}
|
||||
addRecipient("{{ addr }}", {{ amount }}, "{{ amount_unit }}", "{{ label }}");
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
addRecipient("", "", "btc", "");
|
||||
{% endif %}
|
||||
adjustForLiquid()
|
||||
document.getElementById('toggle_advanced').addEventListener('click', (event) => {
|
||||
toggleAdvanced();
|
||||
});
|
||||
// Hide the remove button if the page is loaded with only once recipient
|
||||
setVisibilityRemoveRecipientButton();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -28,10 +28,9 @@ It's designed to be used in a form and will "drop" what the user has chosen in f
|
|||
<input type="hidden" id="fee_rate_dynamic" name="fee_rate_dynamic" value="4" class="hidden">
|
||||
<input type="hidden" class="rbf-checkbox inline" name="rbf" id="rbf" value="true">
|
||||
<input type="hidden" class="inline" name="subtract" id="subtract" value="false">
|
||||
<input id="subtract_from_input" name="subtract_from" type="hidden" min="1" value="1" step="1" style="width: 80px; min-width: 80px;">
|
||||
<span id="subtract_from" style="display: none">
|
||||
<br>Subtract from recipient number:
|
||||
<input id="subtract_from_input" name="subtract_from" type="number" min="1" value="1" step="1" style="width: 80px; min-width: 80px;"><br>
|
||||
<br>{{ _("Subtract from ") }}
|
||||
<select id="subtract_from_recipient_id_select" name="subtract_from_stale" style="width: 140px; min-width: 140px;" ></select><br>
|
||||
</span>
|
||||
|
||||
</textarea>
|
||||
|
|
|
|||
|
|
@ -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":"<script>console.log(\'I escaped\')</script>","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",
|
||||
"<script>console.log('I escaped')</script>",
|
||||
]
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue