(this somehow escaped attention before, as most objects usually don't have multiple instances, unless multiple wallets are open at the same time.) Also, move all signal declarations, class constants and variables to the top of class definitions.
720 lines
28 KiB
Python
720 lines
28 KiB
Python
import asyncio
|
|
import queue
|
|
import threading
|
|
import time
|
|
from typing import Optional, TYPE_CHECKING
|
|
from functools import partial
|
|
|
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer
|
|
|
|
from electrum import bitcoin
|
|
from electrum.i18n import _
|
|
from electrum.invoices import InvoiceError, PR_DEFAULT_EXPIRATION_WHEN_CREATING, PR_PAID
|
|
from electrum.logging import get_logger
|
|
from electrum.network import TxBroadcastError, BestEffortRequestFailed
|
|
from electrum.transaction import PartialTxOutput
|
|
from electrum.util import (parse_max_spend, InvalidPassword, event_listener)
|
|
from electrum.plugin import run_hook
|
|
from electrum.wallet import Multisig_Wallet
|
|
from electrum.crypto import pw_decode_with_version_and_mac
|
|
|
|
from .auth import AuthMixin, auth_protect
|
|
from .qeaddresslistmodel import QEAddressListModel
|
|
from .qechannellistmodel import QEChannelListModel
|
|
from .qeinvoicelistmodel import QEInvoiceListModel, QERequestListModel
|
|
from .qetransactionlistmodel import QETransactionListModel
|
|
from .qetypes import QEAmount
|
|
from .util import QtEventListener, qt_event_listener
|
|
|
|
if TYPE_CHECKING:
|
|
from electrum.wallet import Abstract_Wallet
|
|
|
|
|
|
class QEWallet(AuthMixin, QObject, QtEventListener):
|
|
__instances = []
|
|
|
|
# this factory method should be used to instantiate QEWallet
|
|
# so we have only one QEWallet for each electrum.wallet
|
|
@classmethod
|
|
def getInstanceFor(cls, wallet):
|
|
for i in cls.__instances:
|
|
if i.wallet == wallet:
|
|
return i
|
|
i = QEWallet(wallet)
|
|
cls.__instances.append(i)
|
|
return i
|
|
|
|
_logger = get_logger(__name__)
|
|
|
|
# emitted when wallet wants to display a user notification
|
|
# actual presentation should be handled on app or window level
|
|
userNotify = pyqtSignal(object, object)
|
|
|
|
# shared signal for many static wallet properties
|
|
dataChanged = pyqtSignal()
|
|
|
|
isUptodateChanged = pyqtSignal()
|
|
requestStatusChanged = pyqtSignal([str,int], arguments=['key','status'])
|
|
requestCreateSuccess = pyqtSignal([str], arguments=['key'])
|
|
requestCreateError = pyqtSignal([str,str], arguments=['code','error'])
|
|
invoiceStatusChanged = pyqtSignal([str,int], arguments=['key','status'])
|
|
invoiceCreateSuccess = pyqtSignal()
|
|
invoiceCreateError = pyqtSignal([str,str], arguments=['code','error'])
|
|
paymentSucceeded = pyqtSignal([str], arguments=['key'])
|
|
paymentFailed = pyqtSignal([str,str], arguments=['key','reason'])
|
|
requestNewPassword = pyqtSignal()
|
|
transactionSigned = pyqtSignal([str], arguments=['txid'])
|
|
broadcastSucceeded = pyqtSignal([str], arguments=['txid'])
|
|
broadcastFailed = pyqtSignal([str,str,str], arguments=['txid','code','reason'])
|
|
importChannelBackupFailed = pyqtSignal([str], arguments=['message'])
|
|
labelsUpdated = pyqtSignal()
|
|
otpRequested = pyqtSignal()
|
|
otpSuccess = pyqtSignal()
|
|
otpFailed = pyqtSignal([str,str], arguments=['code','message'])
|
|
|
|
_network_signal = pyqtSignal(str, object)
|
|
|
|
def __init__(self, wallet: 'Abstract_Wallet', parent=None):
|
|
super().__init__(parent)
|
|
self.wallet = wallet
|
|
|
|
self._isUpToDate = False
|
|
self._synchronizing = False
|
|
self._synchronizing_progress = ''
|
|
|
|
self._historyModel = None
|
|
self._addressModel = None
|
|
self._requestModel = None
|
|
self._invoiceModel = None
|
|
self._channelModel = None
|
|
|
|
self._lightningbalance = QEAmount()
|
|
self._confirmedbalance = QEAmount()
|
|
self._unconfirmedbalance = QEAmount()
|
|
self._frozenbalance = QEAmount()
|
|
self._totalbalance = QEAmount()
|
|
self._lightningcanreceive = QEAmount()
|
|
self._lightningcansend = QEAmount()
|
|
|
|
|
|
self.tx_notification_queue = queue.Queue()
|
|
self.tx_notification_last_time = 0
|
|
|
|
self.notification_timer = QTimer(self)
|
|
self.notification_timer.setSingleShot(False)
|
|
self.notification_timer.setInterval(500) # msec
|
|
self.notification_timer.timeout.connect(self.notify_transactions)
|
|
|
|
# To avoid leaking references to "self" that prevent the
|
|
# window from being GC-ed when closed, callbacks should be
|
|
# methods of this class only, and specifically not be
|
|
# partials, lambdas or methods of subobjects. Hence...
|
|
|
|
self.register_callbacks()
|
|
self.destroyed.connect(lambda: self.on_destroy())
|
|
|
|
@pyqtProperty(bool, notify=isUptodateChanged)
|
|
def isUptodate(self):
|
|
return self._isUpToDate
|
|
|
|
synchronizingChanged = pyqtSignal()
|
|
@pyqtProperty(bool, notify=synchronizingChanged)
|
|
def synchronizing(self):
|
|
return self._synchronizing
|
|
|
|
@synchronizing.setter
|
|
def synchronizing(self, synchronizing):
|
|
if self._synchronizing != synchronizing:
|
|
self._synchronizing = synchronizing
|
|
self.synchronizingChanged.emit()
|
|
|
|
synchronizingProgressChanged = pyqtSignal()
|
|
@pyqtProperty(str, notify=synchronizingProgressChanged)
|
|
def synchronizing_progress(self):
|
|
return self._synchronizing_progress
|
|
|
|
@synchronizing_progress.setter
|
|
def synchronizing_progress(self, progress):
|
|
if self._synchronizing_progress != progress:
|
|
self._synchronizing_progress = progress
|
|
self.synchronizingProgressChanged.emit()
|
|
|
|
@event_listener
|
|
def on_event_status(self):
|
|
self._logger.debug('status')
|
|
uptodate = self.wallet.is_up_to_date()
|
|
if self._isUpToDate != uptodate:
|
|
self._isUpToDate = uptodate
|
|
self.isUptodateChanged.emit()
|
|
|
|
if self.wallet.network.is_connected():
|
|
server_height = self.wallet.network.get_server_height()
|
|
server_lag = self.wallet.network.get_local_height() - server_height
|
|
# Server height can be 0 after switching to a new server
|
|
# until we get a headers subscription request response.
|
|
# Display the synchronizing message in that case.
|
|
if not self._isUpToDate or server_height == 0:
|
|
num_sent, num_answered = self.wallet.adb.get_history_sync_state_details()
|
|
self.synchronizing_progress = ("{} ({}/{})"
|
|
.format(_("Synchronizing..."), num_answered, num_sent))
|
|
self.synchronizing = True
|
|
else:
|
|
self.synchronizing_progress = ''
|
|
self.synchronizing = False
|
|
|
|
@qt_event_listener
|
|
def on_event_request_status(self, wallet, key, status):
|
|
if wallet == self.wallet:
|
|
self._logger.debug('request status %d for key %s' % (status, key))
|
|
self.requestStatusChanged.emit(key, status)
|
|
if status == PR_PAID:
|
|
# might be new incoming LN payment, update history
|
|
# TODO: only update if it was paid over lightning,
|
|
# and even then, we can probably just add the payment instead
|
|
# of recreating the whole history (expensive)
|
|
self.historyModel.init_model()
|
|
|
|
@event_listener
|
|
def on_event_invoice_status(self, wallet, key, status):
|
|
if wallet == self.wallet:
|
|
self._logger.debug(f'invoice status update for key {key} to {status}')
|
|
self.invoiceStatusChanged.emit(key, status)
|
|
|
|
@qt_event_listener
|
|
def on_event_new_transaction(self, wallet, tx):
|
|
if wallet == self.wallet:
|
|
self._logger.info(f'new transaction {tx.txid()}')
|
|
self.add_tx_notification(tx)
|
|
self.addressModel.setDirty()
|
|
self.historyModel.init_model() # TODO: be less dramatic
|
|
|
|
@event_listener
|
|
def on_event_wallet_updated(self, wallet):
|
|
if wallet == self.wallet:
|
|
self._logger.debug('wallet %s updated' % str(wallet))
|
|
self.balanceChanged.emit()
|
|
|
|
@event_listener
|
|
def on_event_channel(self, wallet, channel):
|
|
if wallet == self.wallet:
|
|
self.balanceChanged.emit()
|
|
|
|
@event_listener
|
|
def on_event_channels_updated(self, wallet):
|
|
if wallet == self.wallet:
|
|
self.balanceChanged.emit()
|
|
|
|
@qt_event_listener
|
|
def on_event_payment_succeeded(self, wallet, key):
|
|
if wallet == self.wallet:
|
|
self.paymentSucceeded.emit(key)
|
|
self.historyModel.init_model() # TODO: be less dramatic
|
|
|
|
@event_listener
|
|
def on_event_payment_failed(self, wallet, key, reason):
|
|
if wallet == self.wallet:
|
|
self.paymentFailed.emit(key, reason)
|
|
|
|
def on_destroy(self):
|
|
self.unregister_callbacks()
|
|
|
|
def add_tx_notification(self, tx):
|
|
self._logger.debug('new transaction event')
|
|
self.tx_notification_queue.put(tx)
|
|
if not self.notification_timer.isActive():
|
|
self._logger.debug('starting wallet notification timer')
|
|
self.notification_timer.start()
|
|
|
|
def notify_transactions(self):
|
|
if self.tx_notification_queue.qsize() == 0:
|
|
self._logger.debug('queue empty, stopping wallet notification timer')
|
|
self.notification_timer.stop()
|
|
return
|
|
if not self.wallet.is_up_to_date():
|
|
return # no notifications while syncing
|
|
now = time.time()
|
|
rate_limit = 20 # seconds
|
|
if self.tx_notification_last_time + rate_limit > now:
|
|
return
|
|
self.tx_notification_last_time = now
|
|
self._logger.info("Notifying app about new transactions")
|
|
txns = []
|
|
while True:
|
|
try:
|
|
txns.append(self.tx_notification_queue.get_nowait())
|
|
except queue.Empty:
|
|
break
|
|
|
|
config = self.wallet.config
|
|
# Combine the transactions if there are at least three
|
|
if len(txns) >= 3:
|
|
total_amount = 0
|
|
for tx in txns:
|
|
tx_wallet_delta = self.wallet.get_wallet_delta(tx)
|
|
if not tx_wallet_delta.is_relevant:
|
|
continue
|
|
total_amount += tx_wallet_delta.delta
|
|
self.userNotify.emit(self.wallet, _("{} new transactions: Total amount received in the new transactions {}").format(len(txns), config.format_amount_and_units(total_amount)))
|
|
else:
|
|
for tx in txns:
|
|
tx_wallet_delta = self.wallet.get_wallet_delta(tx)
|
|
if not tx_wallet_delta.is_relevant:
|
|
continue
|
|
self.userNotify.emit(self.wallet,
|
|
_("New transaction: {}").format(config.format_amount_and_units(tx_wallet_delta.delta)))
|
|
|
|
historyModelChanged = pyqtSignal()
|
|
@pyqtProperty(QETransactionListModel, notify=historyModelChanged)
|
|
def historyModel(self):
|
|
if self._historyModel is None:
|
|
self._historyModel = QETransactionListModel(self.wallet)
|
|
return self._historyModel
|
|
|
|
addressModelChanged = pyqtSignal()
|
|
@pyqtProperty(QEAddressListModel, notify=addressModelChanged)
|
|
def addressModel(self):
|
|
if self._addressModel is None:
|
|
self._addressModel = QEAddressListModel(self.wallet)
|
|
return self._addressModel
|
|
|
|
requestModelChanged = pyqtSignal()
|
|
@pyqtProperty(QERequestListModel, notify=requestModelChanged)
|
|
def requestModel(self):
|
|
if self._requestModel is None:
|
|
self._requestModel = QERequestListModel(self.wallet)
|
|
return self._requestModel
|
|
|
|
invoiceModelChanged = pyqtSignal()
|
|
@pyqtProperty(QEInvoiceListModel, notify=invoiceModelChanged)
|
|
def invoiceModel(self):
|
|
if self._invoiceModel is None:
|
|
self._invoiceModel = QEInvoiceListModel(self.wallet)
|
|
return self._invoiceModel
|
|
|
|
channelModelChanged = pyqtSignal()
|
|
@pyqtProperty(QEChannelListModel, notify=channelModelChanged)
|
|
def channelModel(self):
|
|
if self._channelModel is None:
|
|
self._channelModel = QEChannelListModel(self.wallet)
|
|
return self._channelModel
|
|
|
|
nameChanged = pyqtSignal()
|
|
@pyqtProperty(str, notify=nameChanged)
|
|
def name(self):
|
|
return self.wallet.basename()
|
|
|
|
isLightningChanged = pyqtSignal()
|
|
@pyqtProperty(bool, notify=isLightningChanged)
|
|
def isLightning(self):
|
|
return bool(self.wallet.lnworker)
|
|
|
|
billingInfoChanged = pyqtSignal()
|
|
@pyqtProperty('QVariantMap', notify=billingInfoChanged)
|
|
def billingInfo(self):
|
|
return {} if self.wallet.wallet_type != '2fa' else self.wallet.billing_info
|
|
|
|
@pyqtProperty(bool, notify=dataChanged)
|
|
def canHaveLightning(self):
|
|
return self.wallet.can_have_lightning()
|
|
|
|
@pyqtProperty(str, notify=dataChanged)
|
|
def walletType(self):
|
|
return self.wallet.wallet_type
|
|
|
|
@pyqtProperty(bool, notify=dataChanged)
|
|
def hasSeed(self):
|
|
return self.wallet.has_seed()
|
|
|
|
@pyqtProperty(str, notify=dataChanged)
|
|
def seed(self):
|
|
try:
|
|
return self.wallet.get_seed(self.password)
|
|
except:
|
|
return ''
|
|
|
|
@pyqtProperty(str, notify=dataChanged)
|
|
def txinType(self):
|
|
if self.wallet.wallet_type == 'imported':
|
|
return self.wallet.txin_type
|
|
return self.wallet.get_txin_type(self.wallet.dummy_address())
|
|
|
|
@pyqtProperty(bool, notify=dataChanged)
|
|
def isWatchOnly(self):
|
|
return self.wallet.is_watching_only()
|
|
|
|
@pyqtProperty(bool, notify=dataChanged)
|
|
def isDeterministic(self):
|
|
return self.wallet.is_deterministic()
|
|
|
|
@pyqtProperty(bool, notify=dataChanged)
|
|
def isEncrypted(self):
|
|
return self.wallet.storage.is_encrypted()
|
|
|
|
@pyqtProperty(bool, notify=dataChanged)
|
|
def isHardware(self):
|
|
return self.wallet.storage.is_encrypted_with_hw_device()
|
|
|
|
@pyqtProperty('QVariantList', notify=dataChanged)
|
|
def keystores(self):
|
|
result = []
|
|
for k in self.wallet.get_keystores():
|
|
result.append({
|
|
'derivation_prefix': k.get_derivation_prefix() or '',
|
|
'master_pubkey': k.get_master_public_key() or '',
|
|
'fingerprint': k.get_root_fingerprint() or '',
|
|
'watch_only': k.is_watching_only()
|
|
})
|
|
return result
|
|
|
|
@pyqtProperty(str, notify=dataChanged)
|
|
def lightningNodePubkey(self):
|
|
return self.wallet.lnworker.node_keypair.pubkey.hex() if self.wallet.lnworker else ''
|
|
|
|
@pyqtProperty(str, notify=dataChanged)
|
|
def derivationPrefix(self):
|
|
keystores = self.wallet.get_keystores()
|
|
if len(keystores) > 1:
|
|
self._logger.debug('multiple keystores not supported yet')
|
|
if len(keystores) == 0:
|
|
self._logger.debug('no keystore')
|
|
return ''
|
|
if not self.isDeterministic:
|
|
return ''
|
|
return keystores[0].get_derivation_prefix()
|
|
|
|
@pyqtProperty(str, notify=dataChanged)
|
|
def masterPubkey(self):
|
|
return self.wallet.get_master_public_key()
|
|
|
|
@pyqtProperty(bool, notify=dataChanged)
|
|
def canSignWithoutServer(self):
|
|
return self.wallet.can_sign_without_server() if self.wallet.wallet_type == '2fa' else True
|
|
|
|
@pyqtProperty(bool, notify=dataChanged)
|
|
def canSignWithoutCosigner(self):
|
|
if isinstance(self.wallet, Multisig_Wallet):
|
|
if self.wallet.wallet_type == '2fa': # 2fa is multisig, but it handles cosigning itself
|
|
return True
|
|
return self.wallet.m == 1
|
|
return True
|
|
|
|
balanceChanged = pyqtSignal()
|
|
|
|
@pyqtProperty(QEAmount, notify=balanceChanged)
|
|
def frozenBalance(self):
|
|
c, u, x = self.wallet.get_frozen_balance()
|
|
self._frozenbalance.satsInt = c+x
|
|
return self._frozenbalance
|
|
|
|
@pyqtProperty(QEAmount, notify=balanceChanged)
|
|
def unconfirmedBalance(self):
|
|
self._unconfirmedbalance.satsInt = self.wallet.get_balance()[1]
|
|
return self._unconfirmedbalance
|
|
|
|
@pyqtProperty(QEAmount, notify=balanceChanged)
|
|
def confirmedBalance(self):
|
|
c, u, x = self.wallet.get_balance()
|
|
self._confirmedbalance.satsInt = c+x
|
|
return self._confirmedbalance
|
|
|
|
@pyqtProperty(QEAmount, notify=balanceChanged)
|
|
def lightningBalance(self):
|
|
if self.isLightning:
|
|
self._lightningbalance.satsInt = int(self.wallet.lnworker.get_balance())
|
|
# else:
|
|
# self._lightningbalance.satsInt = 0
|
|
return self._lightningbalance
|
|
|
|
@pyqtProperty(QEAmount, notify=balanceChanged)
|
|
def totalBalance(self):
|
|
total = self.confirmedBalance.satsInt + self.lightningBalance.satsInt
|
|
self._totalbalance.satsInt = total
|
|
return self._totalbalance
|
|
|
|
@pyqtProperty(QEAmount, notify=balanceChanged)
|
|
def lightningCanSend(self):
|
|
if self.isLightning:
|
|
self._lightningcansend.satsInt = int(self.wallet.lnworker.num_sats_can_send())
|
|
return self._lightningcansend
|
|
|
|
@pyqtProperty(QEAmount, notify=balanceChanged)
|
|
def lightningCanReceive(self):
|
|
if self.isLightning:
|
|
self._lightningcanreceive.satsInt = int(self.wallet.lnworker.num_sats_can_receive())
|
|
return self._lightningcanreceive
|
|
|
|
@pyqtSlot()
|
|
def enableLightning(self):
|
|
self.wallet.init_lightning(password=None) # TODO pass password if needed
|
|
self.isLightningChanged.emit()
|
|
|
|
@pyqtSlot(str, int, int, bool)
|
|
def send_onchain(self, address, amount, fee=None, rbf=False):
|
|
self._logger.info('send_onchain: %s %d' % (address,amount))
|
|
coins = self.wallet.get_spendable_coins(None)
|
|
if not bitcoin.is_address(address):
|
|
self._logger.warning('Invalid Bitcoin Address: ' + address)
|
|
return False
|
|
|
|
outputs = [PartialTxOutput.from_address_and_value(address, amount)]
|
|
self._logger.info(str(outputs))
|
|
output_values = [x.value for x in outputs]
|
|
if any(parse_max_spend(outval) for outval in output_values):
|
|
output_value = '!'
|
|
else:
|
|
output_value = sum(output_values)
|
|
self._logger.info(str(output_value))
|
|
# see qt/confirm_tx_dialog qt/main_window
|
|
tx = self.wallet.make_unsigned_transaction(coins=coins,outputs=outputs, fee=None)
|
|
self._logger.info(str(tx.to_json()))
|
|
tx.set_rbf(True)
|
|
self.sign(tx, broadcast=True)
|
|
|
|
@auth_protect
|
|
def sign(self, tx, *, broadcast: bool = False):
|
|
sign_hook = run_hook('tc_sign_wrapper', self.wallet, tx, partial(self.on_sign_complete, broadcast),
|
|
self.on_sign_failed)
|
|
if sign_hook:
|
|
self.do_sign(tx, False)
|
|
self._logger.debug('plugin needs to sign tx too')
|
|
sign_hook(tx)
|
|
return
|
|
|
|
self.do_sign(tx, broadcast)
|
|
|
|
def do_sign(self, tx, broadcast):
|
|
tx = self.wallet.sign_transaction(tx, self.password)
|
|
|
|
if tx is None:
|
|
self._logger.info('did not sign')
|
|
return
|
|
|
|
txid = tx.txid()
|
|
self._logger.debug(f'do_sign(), txid={txid}')
|
|
|
|
self.transactionSigned.emit(txid)
|
|
|
|
if not tx.is_complete():
|
|
self._logger.debug('tx not complete')
|
|
return
|
|
|
|
if broadcast:
|
|
self.broadcast(tx)
|
|
|
|
# this assumes a 2fa wallet, but there are no other tc_sign_wrapper hooks, so that's ok
|
|
def on_sign_complete(self, broadcast, tx):
|
|
self.otpSuccess.emit()
|
|
if broadcast:
|
|
self.broadcast(tx)
|
|
|
|
def on_sign_failed(self, error):
|
|
self.otpFailed.emit('error', error)
|
|
|
|
def request_otp(self, on_submit):
|
|
self._otp_on_submit = on_submit
|
|
self.otpRequested.emit()
|
|
|
|
@pyqtSlot(str)
|
|
def submitOtp(self, otp):
|
|
self._otp_on_submit(otp)
|
|
|
|
def broadcast(self, tx):
|
|
assert tx.is_complete()
|
|
|
|
def broadcast_thread():
|
|
try:
|
|
self._logger.info('running broadcast in thread')
|
|
self.wallet.network.run_from_another_thread(self.wallet.network.broadcast_transaction(tx))
|
|
except TxBroadcastError as e:
|
|
self._logger.error(repr(e))
|
|
self.broadcastFailed.emit(tx.txid(),'',repr(e))
|
|
except BestEffortRequestFailed as e:
|
|
self._logger.error(repr(e))
|
|
self.broadcastFailed.emit(tx.txid(),'',repr(e))
|
|
else:
|
|
self._logger.info('broadcast success')
|
|
self.broadcastSucceeded.emit(tx.txid())
|
|
self.historyModel.requestRefresh.emit() # via qt thread
|
|
|
|
threading.Thread(target=broadcast_thread).start()
|
|
|
|
#TODO: properly catch server side errors, e.g. bad-txns-inputs-missingorspent
|
|
|
|
paymentAuthRejected = pyqtSignal()
|
|
def ln_auth_rejected(self):
|
|
self.paymentAuthRejected.emit()
|
|
|
|
@pyqtSlot(str)
|
|
@auth_protect(reject='ln_auth_rejected')
|
|
def pay_lightning_invoice(self, invoice_key):
|
|
self._logger.debug('about to pay LN')
|
|
invoice = self.wallet.get_invoice(invoice_key)
|
|
assert(invoice)
|
|
assert(invoice.lightning_invoice)
|
|
|
|
amount_msat = invoice.get_amount_msat()
|
|
|
|
def pay_thread():
|
|
try:
|
|
coro = self.wallet.lnworker.pay_invoice(invoice.lightning_invoice, amount_msat=amount_msat)
|
|
fut = asyncio.run_coroutine_threadsafe(coro, self.wallet.network.asyncio_loop)
|
|
fut.result()
|
|
except Exception as e:
|
|
self.paymentFailed.emit(invoice.get_id(), repr(e))
|
|
|
|
threading.Thread(target=pay_thread).start()
|
|
|
|
def create_bitcoin_request(self, amount: int, message: str, expiration: int, ignore_gap: bool) -> Optional[str]:
|
|
addr = self.wallet.get_unused_address()
|
|
if addr is None:
|
|
if not self.wallet.is_deterministic(): # imported wallet
|
|
# TODO implement
|
|
return
|
|
#msg = [
|
|
#_('No more addresses in your wallet.'), ' ',
|
|
#_('You are using a non-deterministic wallet, which cannot create new addresses.'), ' ',
|
|
#_('If you want to create new addresses, use a deterministic wallet instead.'), '\n\n',
|
|
#_('Creating a new payment request will reuse one of your addresses and overwrite an existing request. Continue anyway?'),
|
|
#]
|
|
#if not self.question(''.join(msg)):
|
|
#return
|
|
#addr = self.wallet.get_receiving_address()
|
|
else: # deterministic wallet
|
|
if not ignore_gap:
|
|
self.requestCreateError.emit('gaplimit',_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?"))
|
|
return
|
|
addr = self.wallet.create_new_address(False)
|
|
|
|
req_key = self.wallet.create_request(amount, message, expiration, addr)
|
|
#try:
|
|
#self.wallet.add_payment_request(req)
|
|
#except Exception as e:
|
|
#self.logger.exception('Error adding payment request')
|
|
#self.requestCreateError.emit('fatal',_('Error adding payment request') + ':\n' + repr(e))
|
|
#else:
|
|
## TODO: check this flow. Only if alias is defined in config. OpenAlias?
|
|
#pass
|
|
##self.sign_payment_request(addr)
|
|
|
|
return req_key, addr
|
|
|
|
@pyqtSlot(QEAmount, str, int)
|
|
@pyqtSlot(QEAmount, str, int, bool)
|
|
@pyqtSlot(QEAmount, str, int, bool, bool)
|
|
def createRequest(self, amount: QEAmount, message: str, expiration: int, is_lightning: bool = False, ignore_gap: bool = False):
|
|
# TODO: unify this method and create_bitcoin_request
|
|
try:
|
|
if is_lightning:
|
|
if not self.wallet.lnworker.channels:
|
|
self.requestCreateError.emit('fatal',_("You need to open a Lightning channel first."))
|
|
return
|
|
# TODO maybe show a warning if amount exceeds lnworker.num_sats_can_receive (as in kivy)
|
|
# TODO fallback address robustness
|
|
addr = self.wallet.get_unused_address()
|
|
key = self.wallet.create_request(amount.satsInt, message, expiration, addr)
|
|
else:
|
|
key, addr = self.create_bitcoin_request(amount.satsInt, message, expiration, ignore_gap)
|
|
if not key:
|
|
return
|
|
self.addressModel.init_model()
|
|
except InvoiceError as e:
|
|
self.requestCreateError.emit('fatal',_('Error creating payment request') + ':\n' + str(e))
|
|
return
|
|
|
|
assert key is not None
|
|
self.requestModel.add_invoice(self.wallet.get_request(key))
|
|
self.requestCreateSuccess.emit(key)
|
|
|
|
@pyqtSlot()
|
|
@pyqtSlot(bool)
|
|
def createDefaultRequest(self, ignore_gap: bool = False):
|
|
try:
|
|
default_expiry = self.wallet.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
|
|
if self.wallet.lnworker and self.wallet.lnworker.channels:
|
|
addr = self.wallet.get_unused_address()
|
|
# if addr is None, we ran out of addresses
|
|
if addr is None:
|
|
# TODO: remove oldest unpaid request having a fallback address and try again
|
|
pass
|
|
key = self.wallet.create_request(None, None, default_expiry, addr)
|
|
else:
|
|
key, addr = self.create_bitcoin_request(None, None, default_expiry, ignore_gap)
|
|
if not key:
|
|
return
|
|
except InvoiceError as e:
|
|
self.requestCreateError.emit('fatal',_('Error creating payment request') + ':\n' + str(e))
|
|
return
|
|
|
|
assert key is not None
|
|
self.requestModel.add_invoice(self.wallet.get_request(key))
|
|
self.requestCreateSuccess.emit(key)
|
|
|
|
@pyqtSlot(str)
|
|
def delete_request(self, key: str):
|
|
self._logger.debug('delete req %s' % key)
|
|
self.wallet.delete_request(key)
|
|
self.requestModel.delete_invoice(key)
|
|
|
|
@pyqtSlot(str, result='QVariant')
|
|
def get_request(self, key: str):
|
|
return self.requestModel.get_model_invoice(key)
|
|
|
|
@pyqtSlot(str)
|
|
def delete_invoice(self, key: str):
|
|
self._logger.debug('delete inv %s' % key)
|
|
self.wallet.delete_invoice(key)
|
|
self.invoiceModel.delete_invoice(key)
|
|
|
|
@pyqtSlot(str, result='QVariant')
|
|
def get_invoice(self, key: str):
|
|
return self.invoiceModel.get_model_invoice(key)
|
|
|
|
@pyqtSlot(str, result=bool)
|
|
def verify_password(self, password):
|
|
try:
|
|
self.wallet.storage.check_password(password)
|
|
return True
|
|
except InvalidPassword as e:
|
|
return False
|
|
|
|
@pyqtSlot(str)
|
|
def set_password(self, password):
|
|
storage = self.wallet.storage
|
|
|
|
# HW wallet not supported yet
|
|
if storage.is_encrypted_with_hw_device():
|
|
return
|
|
|
|
try:
|
|
self.wallet.update_password(self.password, password, encrypt_storage=True)
|
|
self.password = password
|
|
except InvalidPassword as e:
|
|
self._logger.exception(repr(e))
|
|
|
|
@pyqtSlot(str)
|
|
def importAddresses(self, addresslist):
|
|
self.wallet.import_addresses(addresslist.split())
|
|
|
|
@pyqtSlot(str)
|
|
def importPrivateKeys(self, keyslist):
|
|
self.wallet.import_private_keys(keyslist.split(), self.password)
|
|
|
|
@pyqtSlot(str)
|
|
def importChannelBackup(self, backup_str):
|
|
try:
|
|
self.wallet.lnworker.import_channel_backup(backup_str)
|
|
except Exception as e:
|
|
self._logger.debug(f'could not import channel backup: {repr(e)}')
|
|
self.importChannelBackupFailed.emit(f'Failed to import backup:\n\n{str(e)}')
|
|
|
|
@pyqtSlot(str, result=bool)
|
|
def isValidChannelBackup(self, backup_str):
|
|
try:
|
|
assert backup_str.startswith('channel_backup:')
|
|
encrypted = backup_str[15:]
|
|
xpub = self.wallet.get_fingerprint()
|
|
decrypted = pw_decode_with_version_and_mac(encrypted, xpub)
|
|
return True
|
|
except Exception as e:
|
|
return False
|