#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Author: Bertrand256 # Created on: 2018-03 import sys import re from typing import Optional, Callable, Dict, List import simplejson import logging from PyQt5 import QtWidgets from PyQt5.QtCore import pyqtSlot from PyQt5.QtGui import QTextDocument, QDesktopServices from PyQt5.QtWidgets import QDialog, QMessageBox, QProxyStyle, QStyle from decimal import Decimal from bitcoinrpc.authproxy import JSONRPCException import app_cache import app_utils from app_config import AppConfig from app_defs import HWType from dashd_intf import DashdInterface from hw_common import HwSessionInfo from ui.ui_transaction_dlg import Ui_TransactionDlg from wallet_common import UtxoType, TxOutputType, Bip44AddressType from wnd_utils import WndUtils, ProxyStyleNoFocusRect CACHE_ITEM_DETAILS_WORD_WRAP = 'TransactionDlg_DetailsWordWrap' log = logging.getLogger('dmt.transaction_dlg') class TransactionDlg(QDialog, Ui_TransactionDlg, WndUtils): def __init__(self, parent: QDialog, app_config: AppConfig, dashd_intf: DashdInterface, raw_transaction: str, use_instant_send: bool, tx_inputs: List[UtxoType], tx_outputs: List[TxOutputType], cur_hd_tree_id: int, hw_session: HwSessionInfo, after_send_tx_callback: Callable[[dict], None], decoded_transaction: Optional[dict] = None, dependent_transactions: Optional[dict] = None, fn_show_address_on_hw: Callable[[Bip44AddressType], None] = None): QDialog.__init__(self, parent=parent) Ui_TransactionDlg.__init__(self) WndUtils.__init__(self, app_config) self.app_config = app_config self.parent = parent self.dashd_intf = dashd_intf self.transaction_sent = False self.raw_transaction = raw_transaction self.use_instant_send = use_instant_send self.tx_inputs = tx_inputs self.tx_outputs = tx_outputs self.tx_id = None # will be decoded from rawtransaction self.tx_size = None # as above self.cur_hd_tree_id = cur_hd_tree_id self.hw_session = hw_session self.decoded_transaction: Optional[dict] = decoded_transaction self.dependent_transactions = dependent_transactions # key: txid, value: transaction dict self.after_send_tx_callback: Callable[[Dict], None] = after_send_tx_callback self.fn_show_address_on_hw = fn_show_address_on_hw self.setupUi() def setupUi(self): Ui_TransactionDlg.setupUi(self, self) self.setWindowTitle('Transaction') self.chb_word_wrap.setChecked(app_cache.get_value(CACHE_ITEM_DETAILS_WORD_WRAP, False, bool)) self.apply_word_wrap(self.chb_word_wrap.isChecked()) self.edt_recipients.viewport().setAutoFillBackground(False) if sys.platform == 'win32': self.base_font_size = '11' self.title_font_size = '15' elif sys.platform == 'linux': self.base_font_size = '11' self.title_font_size = '17' else: # mac self.base_font_size = '13' self.title_font_size = '20' self.edt_raw_transaction.setStyleSheet(f'font: {self.base_font_size}pt "Courier New";') doc = QTextDocument(self) doc.setDocumentMargin(0) doc.setHtml(f'<span style=" font-size:{self.title_font_size}pt;white-space:nowrap">AAAAAAAAAAAAAAAAAA') self.edt_recipients.setStyle(ProxyStyleNoFocusRect()) default_width = int(doc.size().width()) * 3 default_height = int(default_width / 2) app_cache.restore_window_size(self, default_width=default_width, default_height=default_height) self.prepare_tx_view() def closeEvent(self, event): app_cache.save_window_size(self) def on_chb_word_wrap_toggled(self, checked): app_cache.set_value(CACHE_ITEM_DETAILS_WORD_WRAP, checked) self.apply_word_wrap(checked) def apply_word_wrap(self, checked): self.edt_raw_transaction.setWordWrapMode(0 if not checked else 1) def prepare_tx_view(self): def get_vout_value(vout: dict): val = vout.get('value') if not isinstance(val, (float, Decimal)): val = vout.get('valueSat') if val is not None: val = round(val / 1e8, 8) return float(val) try: if self.hw_session and self.hw_session.hw_type: hw_type_desc = HWType.get_desc(self.hw_session.hw_type) else: hw_type_desc = 'device' self.edt_recipients.clear() if not self.decoded_transaction: try: self.decoded_transaction = self.dashd_intf.decoderawtransaction(self.raw_transaction) self.decoded_transaction['hex'] = self.raw_transaction # fill up the missing fields for this new (not yet unpublished) transaction which will # be needed when registering pending transaction in cache vins = self.decoded_transaction.get('vin') if vins: for idx, vin in enumerate(vins): if idx < len(self.tx_inputs): inp = self.tx_inputs[idx] if not vin.get('valueSat'): vin['valueSat'] = inp.satoshis if not vin.get('value'): vin['value'] = round(inp.satoshis / 1e8, 8) if not vin.get('address'): vin['address'] = inp.address else: log.warning('Input index of the decoded transaction does not exist in the input list') except JSONRPCException as e: if re.match('.*400 Bad Request', str(e)) and len(self.raw_transaction): raise Exception('Error while decoding raw transaction: ' + str(e) + '.' + '\n\nProbable cause: size of the transation exceeded the RPC node limit.' '\n\nDecrease the number of inputs.') else: raise Exception('Error while decoding raw transaction: ' + str(e) + '.') except Exception as e: raise Exception('Error while decoding raw transaction: ' + str(e) + '.') if isinstance(self.decoded_transaction, dict): self.edt_raw_transaction.setPlainText(simplejson.dumps(self.decoded_transaction, indent=2)) vout_list = self.decoded_transaction.get('vout') self.tx_size = self.decoded_transaction.get('size') self.tx_id = self.decoded_transaction.get('txid') if vout_list and isinstance(vout_list, list): vin_list = self.decoded_transaction.get('vin') if vin_list and isinstance(vin_list, list): inputs_total = 0.0 for vin in vin_list: txid = vin.get('txid') txindex = vin.get('vout') rawtx = None if isinstance(self.dependent_transactions, dict): rawtx = self.dependent_transactions.get(txid) if not rawtx: rawtx = self.dashd_intf.getrawtransaction(txid, 1) if rawtx: vlist = rawtx.get('vout') val = None for v in vlist: if v.get('n') == txindex: val = get_vout_value(v) break if val is None: log.error(f'Couldn\'t find output {txindex} in source transaction {txid}') else: inputs_total += val if self.tx_size is not None: if self.tx_size > 1024: tx_size_str = f'{round(self.tx_size/1024, 2)} kB' else: tx_size_str = f'{self.tx_size} bytes' # prepare the list of recipients outputs_total = 0.0 recipients = '' for row_idx, vout in enumerate(vout_list): val = get_vout_value(vout) outputs_total += val spk = vout.get('scriptPubKey') address = '' if spk: ads = spk.get('addresses') if isinstance(ads, list) and len(ads) == 1: address = ads[0] else: address = str(ads) address_info = '' if row_idx < len(self.tx_outputs): tx_out = self.tx_outputs[row_idx] if tx_out.address_ref: if tx_out.address_ref.is_change: address_info = f'the change address, path: {tx_out.address_ref.bip32_path}' if self.fn_show_address_on_hw: address_info += f' (<a href="{row_idx}">show on {hw_type_desc}</a>)' elif tx_out.address_ref.tree_id: if self.cur_hd_tree_id == tx_out.address_ref.tree_id: address_info = f'your address in this wallet' if tx_out.address_ref.bip32_path and self.fn_show_address_on_hw: address_info += f' (<a href="{row_idx}">show on {hw_type_desc}</a>)' else: address_info = f'your address in this wallet, under a different identity' else: address_info = 'external address' else: address_info = 'external address' if address_info: address_info = f'<br><span style="color:gray">{address_info}</span><hr>' if row_idx == 0: lbl = f'<p class="lbl">Recipients:</p>' else: lbl = '' recipients += f'<tr><td class="lbl">{lbl}</td><td>{address}{address_info}</td><td><p class="val">{app_utils.to_string(val)} Dash</p></td></tr>' fee = round(inputs_total - outputs_total, 8) send_tx_row = '' if self.transaction_sent: url = self.app_config.get_block_explorer_tx() if url: url = url.replace('%TXID%', self.tx_id) send_tx_row = f'<tr><td class="lbl"><p class="lbl">Transaction ID:</p></td><td colspan="2"><a href="{url}">{self.tx_id}</a></td></tr>' if self.transaction_sent: title = 'Transaction summary - sent' subtitle = '<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; ' \ 'margin-right:0px; -qt-block-indent:0; text-indent:0px; ' \ 'background-color:#2eb82e;color:white; padding: 1px 3px 1px 3px; ' \ f'border-radius: 3px;"><span style=" font-size:{self.base_font_size}pt;">' \ 'Transaction successfully sent...</span></p>' else: title = 'Transaction summary - ready to send' subtitle = '<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; ' \ 'margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=' \ f'"font-size:{self.base_font_size}pt;">Click the <b><Send transaction></b> button to ' \ 'broadcast the transaction.</span></p>' summary = f"""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> td.lbl{{text-align: right;vertical-align: top;}} p.lbl{{margin: 0 5px 0 0; font-weight: bold;}} p.val{{margin: 0 0 0 8px; color: navy;}} </style></head><body style="font-size:{self.base_font_size}pt; font-weight:400; font-style:normal; margin-left:10px;margin-right:10px;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:{self.title_font_size}pt; font-weight:600;">{title}</span></p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:{self.base_font_size}pt;"><br /></p> {subtitle} <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"> <table> {send_tx_row} <tr><td class="lbl"><p class="lbl">Total amount:</p></td><td>{app_utils.to_string(inputs_total)} Dash</td></tr> <tr><td class="lbl"><p class="lbl">Fee:</p></td><td>{app_utils.to_string(fee)} Dash</td></tr> <tr><td class="lbl"><p class="lbl">Transaction size:</p></td><td>{tx_size_str}</td></tr> <tr><td class="lbl"><p class="lbl">InstantSend:</p></td><td>{'YES' if self.use_instant_send else 'NO'}</td></tr> {recipients} </table></p></body></html>""" self.edt_recipients.setText(summary) else: raise Exception('Empty \'vin\' list in the decoded transaction.') else: raise Exception('Empty \'vout\' list in the decoded transaction.') else: raise Exception('Error: could\'t parse tha raw transaction.') except Exception as e: log.exception("Unhandled exception occurred.") raise def on_edt_recipients_anchorClicked(self, link): if self.fn_show_address_on_hw: url = link.url() if re.match('^\d+$', url): row_idx = int(url) if 0 <= row_idx < len(self.tx_outputs): self.fn_show_address_on_hw(self.tx_outputs[row_idx].address_ref) else: QDesktopServices.openUrl(link) @pyqtSlot(bool) def on_btn_details_clicked(self, enabled): idx = (self.stacket_widget.currentIndex() + 1) % 2 self.stacket_widget.setCurrentIndex(idx) self.btn_details.setText({0: 'Show Details', 1: 'Hide Details'}.get(idx)) @pyqtSlot(bool) def on_btn_broadcast_clicked(self): try: log.debug('Broadcasting raw transaction: ' + self.raw_transaction) txid = self.dashd_intf.sendrawtransaction(self.raw_transaction, self.use_instant_send) if txid != self.tx_id: log.warning('TXID returned by sendrawtransaction differs from the original txid') self.tx_id = txid log.info('Transaction sent, txid: ' + txid) self.transaction_sent = True self.btn_broadcast.setEnabled(False) self.prepare_tx_view() if self.after_send_tx_callback: self.after_send_tx_callback(self.decoded_transaction) except Exception as e: log.exception(f'Exception occurred while broadcasting transaction. ' f'Transaction size: {self.tx_size} bytes.') self.errorMsg('An error occurred while sending transation: '+ str(e)) @pyqtSlot(bool) def on_btn_close_clicked(self, enabled): if self.transaction_sent: self.accept() else: self.reject() self.closeEvent(None)