Chore: Fix psbt creator api to work with liquid assets when using json (#1831)

* fix psbt creator api to work with liquid assets when using json
* make cypress tests less flaky

Co-authored-by: moneymanolis <moneymanolis@protonmail.com>
This commit is contained in:
Stepan Snigirev 2022-08-03 18:50:08 +02:00 committed by GitHub
parent a82eddfe7f
commit 3ace290482
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 184 additions and 21 deletions

View file

@ -5,6 +5,7 @@ describe('Operating with an Elements multisig wallet', () => {
cy.visit('/')
cy.get('#node-switch-icon').click()
cy.get('#elements_node-select-node-form > .item > div').click()
cy.get('[value="save"]').click()
// Delete wallets if existing
cy.deleteWallet("Elm Multi Segwit Wallet")

View file

@ -184,7 +184,10 @@ describe('Test sending transactions', () => {
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 })
// Simulating pasting the address, this also reduces the amount of fetch API calls to just one
cy.get('#recipient_0').find('#address').invoke('val', "bcrt1qvtdx75y4554ngrq6aff3xdqnvjhmct5wck95qs").trigger('input')
cy.get('#recipient_0').find('#label').type('To my own wallet', { force: true })
cy.get('#recipient_0').find('#amount').type(10, { 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)')
})

View file

@ -87,7 +87,7 @@ Cypress.Commands.add("deleteDevice", (name) => {
cy.get('body').then(($body) => {
if ($body.text().includes(name)) {
cy.get('#toggle_devices_list').click()
cy.contains(name).click()
cy.contains(name).click( {force: true} )
cy.get('#forget_device').click()
cy.reload()
// For hot wallets only
@ -251,9 +251,9 @@ Cypress.Commands.add("mine2wallet", (chain) => {
Cypress.Commands.add("createPsbt", (address, label="a_label", amount=0.01) => {
cy.get('#btn_send').click()
// 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('#recipient_0').find('#address', { includeShadowDom: true }).type(address, { force: true })
cy.get('#recipient_0').find('#label', { includeShadowDom: true }).type(label, { force: true })
//cy.get('#send_max_0').click()
cy.get('#recipient_0').find('#amount', { includeShadowDom: true }).type(amount)
cy.get('#recipient_0').find('#amount', { includeShadowDom: true }).type(amount, { force: true })
cy.get('#create_psbt_btn').click()
})

View file

@ -7,7 +7,7 @@ import requests
from cryptoadvance.specter.specter_error import SpecterError
from cryptoadvance.specter.util.common import str2bool
from ..helpers import is_testnet
from ..helpers import is_testnet, normalize_address
from ..util.descriptor import AddChecksum, Descriptor
logger = logging.getLogger(__name__)
@ -86,12 +86,7 @@ class PsbtCreator:
f"Unknown ui_option: {ui_option}. Valid ones are ui|text|json"
)
# normalizing
self.addresses = [
address.lower()
if address.startswith(("BC1", "TB1", "BCRT1", "EL1", "ERT1", "EX1", "LQ1"))
else address
for address in self.addresses
]
self.addresses = [normalize_address(addr) for addr in self.addresses]
# get kwargs
if ui_option == "ui" or ui_option == "text":
self.kwargs = PsbtCreator.kwargs_from_request_form(request_form)
@ -186,7 +181,7 @@ class PsbtCreator:
continue
addresses.append(output.split(",")[0].strip())
if recipients_amount_unit == "sat":
amounts.append(float(output.split(",")[1].strip()) / 1e8)
amounts.append(round(float(output.split(",")[1].strip()) / 1e8, 8))
elif recipients_amount_unit == "btc":
amounts.append(float(output.split(",")[1].strip()))
else:
@ -243,18 +238,28 @@ class PsbtCreator:
try:
amount = float(recipient["amount"])
if recipient["unit"] == "sat":
amounts.append(float(amount / 1e8))
amounts.append(round(amount / 1e8, 8))
elif recipient["unit"] == "btc":
amounts.append(amount)
else:
raise SpecterError(
f"Non-compliant json: Unknown unit {recipient['unit']}"
)
if specter.is_liquid:
if len(recipient["unit"]) == 64:
amounts.append(amount)
else:
raise SpecterError(
f"Non-compliant json: Unknown unit {recipient['unit']}. Accepted: sat, btc or 64-char hex asset id."
)
else:
raise SpecterError(
f"Non-compliant json: Unknown unit {recipient['unit']}"
)
except ValueError as e:
raise SpecterError(
f"Could not parse amount {recipient.get('amount')} because {e}"
)
unit = recipient["unit"]
if specter.is_liquid and unit in ["sat", "btc"]:
unit = specter.default_asset
amount_units.append(unit)
label = recipient.get("label", "")

View file

@ -16,6 +16,7 @@ from embit.psbt import PSBT
from embit.transaction import Transaction
from embit.liquid.pset import PSET
from embit.liquid.transaction import LTransaction
from embit.liquid.networks import NETWORKS
from .persistence import read_json_file, write_json_file
from .util.bcur import bcur_decode
import threading
@ -67,6 +68,23 @@ def is_liquid(chain):
return chain not in ["main", "regtest", "test", "signet", "None", "none", None, ""]
def normalize_address(addr: str) -> str:
"""
Normalized address to canonical form.
Upper bech32 -> lower bech32, but doesn't touch base58 addresses.
"""
# bitcoin hrps
hrps = {net["bech32"] + "1" for net in NETWORKS.values()}
# liquid hrps
# Blech32 addresses are intended for confidential assets
hrps = hrps.union(
{net["blech32"] + "1" for net in NETWORKS.values() if "blech32" in net}
)
if addr.lower().startswith(tuple(hrps)):
return addr.lower()
return addr
def locked(customlock=defaultlock):
"""
@locked(lock) decorator.

View file

@ -240,7 +240,7 @@
markAdddressGreenIfMine(){
markAddressGreenIfMine(){
// mark own addresses green
this.isAddressMyOwn().then((isMine) => {
this.markRecipient(isMine);
@ -248,8 +248,13 @@
}
dispatchAddressOnInput(result) {
this.markAdddressGreenIfMine();
this.dispatchEvent(new CustomEvent('address-input'));
let address = this.addressElement.value;
// To reduce the fetch API calls if the address is typed
// TODO: If we have a better JS address validation, this can be useful here, too
if (address.length >= 14) {
this.markAddressGreenIfMine();
this.dispatchEvent(new CustomEvent('address-input'));
}
}
dispatchRemove(result) {
@ -314,7 +319,7 @@
// address
set address(newValue){
this.addressElement.value = newValue;
this.markAdddressGreenIfMine();
this.markAddressGreenIfMine();
}
get address(){
return this.addressElement.value;

View file

@ -168,3 +168,134 @@ def test_PsbtCreator_json(caplog):
}
psbt_creator.create_psbt(wallet_mock)
def test_PsbtCreator_ui_liquid(caplog):
caplog.set_level(logging.DEBUG)
specter_mock = MagicMock()
# non liquid and default asset btc (for unit-calculation)
specter_mock.is_liquid = True
specter_mock.default_asset = "1" * 64
wallet_mock = MagicMock()
specter_mock.chain = "liquid-regtest"
# Let's mock the request.form which behaves like a dict but also needs getlist()
request_form_data = {
"rbf_tx_id": "",
"amount_unit_text": "btc",
"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":"el1qqgxqu7f05rm7e245nvdje28wa6fl3s0cv5lga3yahfqddgr4u8zg75nxwjmrrswvue8x2glk0rhvngz30du8gs5m3lgtfvjrx","label":"someLabel","btc_amount":"0.1"},'
'{"unit":"sat","amount":111211,"recipient_id":1,"address":"EL1QQWHNYT8NSPG0PFXDZJ98ER2K66VE07D297VQ50TRGXZ2P28RJJSS69W3ZY3AUYLF60SKRL0G72V8ZTCCXZYR5WM2AQNAMNQMZ","label":"someOtherLabel","btc_amount":"0.00111211"},'
'{"unit":"0b411a802e536a4217f46a47644c9e207db975f81e697753db2a1915d17dda79","amount":0.003,"recipient_id":2,"address":"AzpxZsPXCk3ct5wx9QfynivkEjHwzKELtHqWtwZ7RZKPncPtYBYn6tktDxCeMtNghMUBMw9MtD7kDcjC","label":"asset test","btc_amount":"0.003"}]',
}
psbt_creator: PsbtCreator = PsbtCreator(
specter_mock, wallet_mock, "ui", request_form=request_form_data
)
assert psbt_creator.addresses == [
"el1qqgxqu7f05rm7e245nvdje28wa6fl3s0cv5lga3yahfqddgr4u8zg75nxwjmrrswvue8x2glk0rhvngz30du8gs5m3lgtfvjrx",
"el1qqwhnyt8nspg0pfxdzj98er2k66ve07d297vq50trgxz2p28rjjss69w3zy3auylf60skrl0g72v8ztccxzyr5wm2aqnamnqmz",
"AzpxZsPXCk3ct5wx9QfynivkEjHwzKELtHqWtwZ7RZKPncPtYBYn6tktDxCeMtNghMUBMw9MtD7kDcjC",
]
assert psbt_creator.amounts == [0.1, 0.00111211, 0.003]
assert psbt_creator.labels == ["someLabel", "someOtherLabel", "asset test"]
assert psbt_creator.amount_units == [
specter_mock.default_asset,
specter_mock.default_asset,
"0b411a802e536a4217f46a47644c9e207db975f81e697753db2a1915d17dda79",
]
assert psbt_creator.kwargs == {
"fee_rate": 64.0,
"rbf": True,
"rbf_edit_mode": False,
"readonly": False,
"selected_coins": [],
"subtract": False,
"subtract_from": 0,
"assets": [
specter_mock.default_asset,
specter_mock.default_asset,
"0b411a802e536a4217f46a47644c9e207db975f81e697753db2a1915d17dda79",
],
}
psbt_creator.create_psbt(wallet_mock)
def test_PsbtCreator_json(caplog):
caplog.set_level(logging.DEBUG)
specter_mock = MagicMock()
# set to liquid and provide default 64-char hex asset (111....)
specter_mock.is_liquid = True
specter_mock.default_asset = "1" * 64
wallet_mock = MagicMock()
specter_mock.chain = "liquid-regtest"
# Let's mock the request.form which behaves like a dict but also needs getlist()
request_json = """
{
"recipients" : [
{
"address": "el1qqgxqu7f05rm7e245nvdje28wa6fl3s0cv5lga3yahfqddgr4u8zg75nxwjmrrswvue8x2glk0rhvngz30du8gs5m3lgtfvjrx",
"amount": 0.1,
"unit": "1111111111111111111111111111111111111111111111111111111111111111",
"label": "someLabel"
},
{
"address": "EL1QQWHNYT8NSPG0PFXDZJ98ER2K66VE07D297VQ50TRGXZ2P28RJJSS69W3ZY3AUYLF60SKRL0G72V8ZTCCXZYR5WM2AQNAMNQMZ",
"amount": 111211,
"unit": "sat",
"label": "someOtherLabel"
},
{
"address": "AzpxZsPXCk3ct5wx9QfynivkEjHwzKELtHqWtwZ7RZKPncPtYBYn6tktDxCeMtNghMUBMw9MtD7kDcjC",
"amount": 0.1,
"unit": "0b411a802e536a4217f46a47644c9e207db975f81e697753db2a1915d17dda79",
"label": "asset test"
}
],
"rbf_tx_id": "",
"subtract_from": "0",
"fee_rate": "64",
"rbf": true
}
"""
psbt_creator: PsbtCreator = PsbtCreator(
specter_mock, wallet_mock, "json", request_json=request_json
)
assert psbt_creator.addresses == [
"el1qqgxqu7f05rm7e245nvdje28wa6fl3s0cv5lga3yahfqddgr4u8zg75nxwjmrrswvue8x2glk0rhvngz30du8gs5m3lgtfvjrx",
"el1qqwhnyt8nspg0pfxdzj98er2k66ve07d297vq50trgxz2p28rjjss69w3zy3auylf60skrl0g72v8ztccxzyr5wm2aqnamnqmz",
"AzpxZsPXCk3ct5wx9QfynivkEjHwzKELtHqWtwZ7RZKPncPtYBYn6tktDxCeMtNghMUBMw9MtD7kDcjC",
]
assert psbt_creator.amounts == [0.1, 0.00111211, 0.1]
assert psbt_creator.labels == ["someLabel", "someOtherLabel", "asset test"]
assert psbt_creator.amount_units == [
specter_mock.default_asset,
specter_mock.default_asset,
"0b411a802e536a4217f46a47644c9e207db975f81e697753db2a1915d17dda79",
]
assert psbt_creator.kwargs == {
"fee_rate": 64.0,
"rbf": True,
"rbf_edit_mode": False,
"readonly": False,
"selected_coins": [],
"subtract": False,
"subtract_from": 0,
"assets": [
specter_mock.default_asset,
specter_mock.default_asset,
"0b411a802e536a4217f46a47644c9e207db975f81e697753db2a1915d17dda79",
],
}
psbt_creator.create_psbt(wallet_mock)