tiagotrs 6 years ago
parent
commit
576c2718c8
  1. 13
      MANIFEST.in
  2. 6
      contrib/build-osx/osx.spec
  3. 4
      contrib/build-wine/deterministic.spec
  4. 2
      contrib/build-wine/docker/Dockerfile
  5. 3
      contrib/deterministic-build/requirements-hw.txt
  6. 1
      contrib/requirements/requirements-hw.txt
  7. 137
      electrum/address_synchronizer.py
  8. 18
      electrum/base_wizard.py
  9. 94
      electrum/blockchain.py
  10. 4
      electrum/coinchooser.py
  11. 7
      electrum/commands.py
  12. 3
      electrum/ecc_fast.py
  13. 13
      electrum/gui/kivy/main_window.py
  14. 6
      electrum/gui/kivy/uix/dialogs/__init__.py
  15. 8
      electrum/gui/kivy/uix/dialogs/addresses.py
  16. 7
      electrum/gui/kivy/uix/dialogs/installwizard.py
  17. 10
      electrum/gui/kivy/uix/screens.py
  18. 2
      electrum/gui/kivy/uix/ui_screens/network.kv
  19. 10
      electrum/gui/qt/address_list.py
  20. 23
      electrum/gui/qt/history_list.py
  21. 12
      electrum/gui/qt/main_window.py
  22. 8
      electrum/gui/qt/network_dialog.py
  23. 13
      electrum/gui/qt/paytoedit.py
  24. 10
      electrum/gui/stdio.py
  25. 10
      electrum/gui/text.py
  26. 25
      electrum/network.py
  27. 7
      electrum/paymentrequest.py
  28. 1
      electrum/plugin.py
  29. 23
      electrum/plugins/cosigner_pool/qt.py
  30. 6
      electrum/plugins/digitalbitbox/digitalbitbox.py
  31. 16
      electrum/plugins/hw_wallet/plugin.py
  32. 7
      electrum/plugins/keepkey/keepkey.py
  33. 10
      electrum/plugins/ledger/ledger.py
  34. 8
      electrum/plugins/safe_t/__init__.py
  35. 11
      electrum/plugins/safe_t/client.py
  36. 252
      electrum/plugins/safe_t/clientbase.py
  37. 14
      electrum/plugins/safe_t/cmdline.py
  38. 492
      electrum/plugins/safe_t/qt.py
  39. 509
      electrum/plugins/safe_t/safe_t.py
  40. 95
      electrum/plugins/safe_t/transport.py
  41. 7
      electrum/plugins/trezor/trezor.py
  42. 9
      electrum/plugins/trustedcoin/trustedcoin.py
  43. 20
      electrum/simple_config.py
  44. 16
      electrum/storage.py
  45. 45
      electrum/tests/test_wallet_vertical.py
  46. 28
      electrum/transaction.py
  47. 43
      electrum/util.py
  48. 9
      electrum/verifier.py
  49. 90
      electrum/wallet.py
  50. 2
      icons.qrc
  51. BIN
      icons/safe-t.png
  52. BIN
      icons/safe-t_unpaired.png
  53. 21
      setup.py

13
MANIFEST.in

@ -3,15 +3,16 @@ include README.rst
include electrum.conf.sample include electrum.conf.sample
include electrum.desktop include electrum.desktop
include *.py include *.py
include electrum include run_electrum
include contrib/requirements/requirements.txt include contrib/requirements/requirements.txt
include contrib/requirements/requirements-hw.txt include contrib/requirements/requirements-hw.txt
recursive-include lib *.py
recursive-include gui *.py
recursive-include plugins *.py
recursive-include packages *.py recursive-include packages *.py
recursive-include packages cacert.pem recursive-include packages cacert.pem
include icons.qrc include icons.qrc
recursive-include icons * graft icons
recursive-include scripts *
graft electrum
prune electrum/tests
global-exclude __pycache__
global-exclude *.py[co]

6
contrib/build-osx/osx.spec

@ -23,6 +23,7 @@ block_cipher = None
# see https://github.com/pyinstaller/pyinstaller/issues/2005 # see https://github.com/pyinstaller/pyinstaller/issues/2005
hiddenimports = [] hiddenimports = []
hiddenimports += collect_submodules('trezorlib') hiddenimports += collect_submodules('trezorlib')
hiddenimports += collect_submodules('safetlib')
hiddenimports += collect_submodules('btchip') hiddenimports += collect_submodules('btchip')
hiddenimports += collect_submodules('keepkeylib') hiddenimports += collect_submodules('keepkeylib')
hiddenimports += collect_submodules('websocket') hiddenimports += collect_submodules('websocket')
@ -33,10 +34,11 @@ datas = [
(electrum+'electrum/locale', PYPKG + '/locale') (electrum+'electrum/locale', PYPKG + '/locale')
] ]
datas += collect_data_files('trezorlib') datas += collect_data_files('trezorlib')
datas += collect_data_files('safetlib')
datas += collect_data_files('btchip') datas += collect_data_files('btchip')
datas += collect_data_files('keepkeylib') datas += collect_data_files('keepkeylib')
# Add libusb so Trezor will work # Add libusb so Trezor and Safe-T mini will work
binaries = [(electrum + "contrib/build-osx/libusb-1.0.dylib", ".")] binaries = [(electrum + "contrib/build-osx/libusb-1.0.dylib", ".")]
binaries += [(electrum + "contrib/build-osx/libsecp256k1.0.dylib", ".")] binaries += [(electrum + "contrib/build-osx/libsecp256k1.0.dylib", ".")]
@ -57,6 +59,8 @@ a = Analysis([electrum+ MAIN_SCRIPT,
electrum+'electrum/plugins/email_requests/qt.py', electrum+'electrum/plugins/email_requests/qt.py',
electrum+'electrum/plugins/trezor/client.py', electrum+'electrum/plugins/trezor/client.py',
electrum+'electrum/plugins/trezor/qt.py', electrum+'electrum/plugins/trezor/qt.py',
electrum+'electrum/plugins/safe_t/client.py',
electrum+'electrum/plugins/safe_t/qt.py',
electrum+'electrum/plugins/keepkey/qt.py', electrum+'electrum/plugins/keepkey/qt.py',
electrum+'electrum/plugins/ledger/qt.py', electrum+'electrum/plugins/ledger/qt.py',
], ],

4
contrib/build-wine/deterministic.spec

@ -18,6 +18,7 @@ home = 'C:\\electrum\\'
# see https://github.com/pyinstaller/pyinstaller/issues/2005 # see https://github.com/pyinstaller/pyinstaller/issues/2005
hiddenimports = [] hiddenimports = []
hiddenimports += collect_submodules('trezorlib') hiddenimports += collect_submodules('trezorlib')
hiddenimports += collect_submodules('safetlib')
hiddenimports += collect_submodules('btchip') hiddenimports += collect_submodules('btchip')
hiddenimports += collect_submodules('keepkeylib') hiddenimports += collect_submodules('keepkeylib')
hiddenimports += collect_submodules('websocket') hiddenimports += collect_submodules('websocket')
@ -37,6 +38,7 @@ datas = [
('C:\\Program Files (x86)\\ZBar\\bin\\', '.') ('C:\\Program Files (x86)\\ZBar\\bin\\', '.')
] ]
datas += collect_data_files('trezorlib') datas += collect_data_files('trezorlib')
datas += collect_data_files('safetlib')
datas += collect_data_files('btchip') datas += collect_data_files('btchip')
datas += collect_data_files('keepkeylib') datas += collect_data_files('keepkeylib')
@ -54,6 +56,8 @@ a = Analysis([home+'run_electrum',
home+'electrum/plugins/email_requests/qt.py', home+'electrum/plugins/email_requests/qt.py',
home+'electrum/plugins/trezor/client.py', home+'electrum/plugins/trezor/client.py',
home+'electrum/plugins/trezor/qt.py', home+'electrum/plugins/trezor/qt.py',
home+'electrum/plugins/safe_t/client.py',
home+'electrum/plugins/safe_t/qt.py',
home+'electrum/plugins/keepkey/qt.py', home+'electrum/plugins/keepkey/qt.py',
home+'electrum/plugins/ledger/qt.py', home+'electrum/plugins/ledger/qt.py',
#home+'packages/requests/utils.py' #home+'packages/requests/utils.py'

2
contrib/build-wine/docker/Dockerfile

@ -8,7 +8,7 @@ RUN dpkg --add-architecture i386 && \
wget=1.19.4-1ubuntu2.1 \ wget=1.19.4-1ubuntu2.1 \
gnupg2=2.2.4-1ubuntu1.1 \ gnupg2=2.2.4-1ubuntu1.1 \
dirmngr=2.2.4-1ubuntu1.1 \ dirmngr=2.2.4-1ubuntu1.1 \
software-properties-common=0.96.24.32.3 \ software-properties-common=0.96.24.32.4 \
&& \ && \
wget -nc https://dl.winehq.org/wine-builds/Release.key && \ wget -nc https://dl.winehq.org/wine-builds/Release.key && \
apt-key add Release.key && \ apt-key add Release.key && \

3
contrib/deterministic-build/requirements-hw.txt

@ -94,6 +94,9 @@ pyblake2==1.1.2 \
requests==2.19.1 \ requests==2.19.1 \
--hash=sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1 \ --hash=sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1 \
--hash=sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a --hash=sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a
safet==0.1.3 \
--hash=sha256:ba80fe9f6ba317ab9514a8726cd3792e68eb46dd419f380d48ae4a0ccae646dc \
--hash=sha256:e5d8e6a87c8bdf1cefd07004181b93fd7631557fdab09d143ba8d1b29291d6dc
setuptools==40.0.0 \ setuptools==40.0.0 \
--hash=sha256:012adb8e25fbfd64c652e99e7bab58799a3aaf05d39ab38561f69190a909015f \ --hash=sha256:012adb8e25fbfd64c652e99e7bab58799a3aaf05d39ab38561f69190a909015f \
--hash=sha256:d68abee4eed409fbe8c302ac4d8429a1ffef912cd047a903b5701c024048dd49 --hash=sha256:d68abee4eed409fbe8c302ac4d8429a1ffef912cd047a903b5701c024048dd49

1
contrib/requirements/requirements-hw.txt

@ -1,5 +1,6 @@
Cython>=0.27 Cython>=0.27
trezor[hidapi]>=0.9.0 trezor[hidapi]>=0.9.0
safet[hidapi]>=0.1.0
keepkey keepkey
btchip-python btchip-python
websocket-client websocket-client

137
electrum/address_synchronizer.py

@ -27,10 +27,11 @@ from collections import defaultdict
from . import bitcoin from . import bitcoin
from .bitcoin import COINBASE_MATURITY, TYPE_ADDRESS, TYPE_PUBKEY from .bitcoin import COINBASE_MATURITY, TYPE_ADDRESS, TYPE_PUBKEY
from .util import PrintError, profiler, bfh from .util import PrintError, profiler, bfh, VerifiedTxInfo, TxMinedStatus
from .transaction import Transaction from .transaction import Transaction, TxOutput
from .synchronizer import Synchronizer from .synchronizer import Synchronizer
from .verifier import SPV from .verifier import SPV
from .blockchain import hash_header
from .i18n import _ from .i18n import _
TX_HEIGHT_LOCAL = -2 TX_HEIGHT_LOCAL = -2
@ -45,6 +46,7 @@ class UnrelatedTransactionException(AddTransactionException):
def __str__(self): def __str__(self):
return _("Transaction is unrelated to this wallet.") return _("Transaction is unrelated to this wallet.")
class AddressSynchronizer(PrintError): class AddressSynchronizer(PrintError):
""" """
inherited by wallet inherited by wallet
@ -61,12 +63,17 @@ class AddressSynchronizer(PrintError):
self.transaction_lock = threading.RLock() self.transaction_lock = threading.RLock()
# address -> list(txid, height) # address -> list(txid, height)
self.history = storage.get('addr_history',{}) self.history = storage.get('addr_history',{})
# Verified transactions. txid -> (height, timestamp, block_pos). Access with self.lock. # Verified transactions. txid -> VerifiedTxInfo. Access with self.lock.
self.verified_tx = storage.get('verified_tx3', {}) verified_tx = storage.get('verified_tx3', {})
self.verified_tx = {}
for txid, (height, timestamp, txpos, header_hash) in verified_tx.items():
self.verified_tx[txid] = VerifiedTxInfo(height, timestamp, txpos, header_hash)
# Transactions pending verification. txid -> tx_height. Access with self.lock. # Transactions pending verification. txid -> tx_height. Access with self.lock.
self.unverified_tx = defaultdict(int) self.unverified_tx = defaultdict(int)
# true when synchronized # true when synchronized
self.up_to_date = False self.up_to_date = False
# thread local storage for caching stuff
self.threadlocal_cache = threading.local()
self.load_and_cleanup() self.load_and_cleanup()
@ -90,10 +97,13 @@ class AddressSynchronizer(PrintError):
with self.lock, self.transaction_lock: with self.lock, self.transaction_lock:
related_txns = self._history_local.get(addr, set()) related_txns = self._history_local.get(addr, set())
for tx_hash in related_txns: for tx_hash in related_txns:
tx_height = self.get_tx_height(tx_hash)[0] tx_height = self.get_tx_height(tx_hash).height
h.append((tx_hash, tx_height)) h.append((tx_hash, tx_height))
return h return h
def get_address_history_len(self, addr: str) -> int:
return len(self._history_local.get(addr, ()))
def get_txin_address(self, txi): def get_txin_address(self, txi):
addr = txi.get('address') addr = txi.get('address')
if addr and addr != "(pubkey)": if addr and addr != "(pubkey)":
@ -107,12 +117,11 @@ class AddressSynchronizer(PrintError):
return addr return addr
return None return None
def get_txout_address(self, txo): def get_txout_address(self, txo: TxOutput):
_type, x, v = txo if txo.type == TYPE_ADDRESS:
if _type == TYPE_ADDRESS: addr = txo.address
addr = x elif txo.type == TYPE_PUBKEY:
elif _type == TYPE_PUBKEY: addr = bitcoin.public_key_to_p2pkh(bfh(txo.address))
addr = bitcoin.public_key_to_p2pkh(bfh(x))
else: else:
addr = None addr = None
return addr return addr
@ -193,7 +202,7 @@ class AddressSynchronizer(PrintError):
# of add_transaction tx, we might learn of more-and-more inputs of # of add_transaction tx, we might learn of more-and-more inputs of
# being is_mine, as we roll the gap_limit forward # being is_mine, as we roll the gap_limit forward
is_coinbase = tx.inputs()[0]['type'] == 'coinbase' is_coinbase = tx.inputs()[0]['type'] == 'coinbase'
tx_height = self.get_tx_height(tx_hash)[0] tx_height = self.get_tx_height(tx_hash).height
if not allow_unrelated: if not allow_unrelated:
# note that during sync, if the transactions are not properly sorted, # note that during sync, if the transactions are not properly sorted,
# it could happen that we think tx is unrelated but actually one of the inputs is is_mine. # it could happen that we think tx is unrelated but actually one of the inputs is is_mine.
@ -212,10 +221,10 @@ class AddressSynchronizer(PrintError):
conflicting_txns = self.get_conflicting_transactions(tx) conflicting_txns = self.get_conflicting_transactions(tx)
if conflicting_txns: if conflicting_txns:
existing_mempool_txn = any( existing_mempool_txn = any(
self.get_tx_height(tx_hash2)[0] in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) self.get_tx_height(tx_hash2).height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT)
for tx_hash2 in conflicting_txns) for tx_hash2 in conflicting_txns)
existing_confirmed_txn = any( existing_confirmed_txn = any(
self.get_tx_height(tx_hash2)[0] > 0 self.get_tx_height(tx_hash2).height > 0
for tx_hash2 in conflicting_txns) for tx_hash2 in conflicting_txns)
if existing_confirmed_txn and tx_height <= 0: if existing_confirmed_txn and tx_height <= 0:
# this is a non-confirmed tx that conflicts with confirmed txns; drop. # this is a non-confirmed tx that conflicts with confirmed txns; drop.
@ -393,7 +402,7 @@ class AddressSynchronizer(PrintError):
def remove_local_transactions_we_dont_have(self): def remove_local_transactions_we_dont_have(self):
txid_set = set(self.txi) | set(self.txo) txid_set = set(self.txi) | set(self.txo)
for txid in txid_set: for txid in txid_set:
tx_height = self.get_tx_height(txid)[0] tx_height = self.get_tx_height(txid).height
if tx_height == TX_HEIGHT_LOCAL and txid not in self.transactions: if tx_height == TX_HEIGHT_LOCAL and txid not in self.transactions:
self.remove_transaction(txid) self.remove_transaction(txid)
@ -431,17 +440,30 @@ class AddressSynchronizer(PrintError):
self.save_transactions() self.save_transactions()
def get_txpos(self, tx_hash): def get_txpos(self, tx_hash):
"return position, even if the tx is unverified" """Returns (height, txpos) tuple, even if the tx is unverified."""
with self.lock: with self.lock:
if tx_hash in self.verified_tx: if tx_hash in self.verified_tx:
height, timestamp, pos = self.verified_tx[tx_hash] info = self.verified_tx[tx_hash]
return height, pos return info.height, info.txpos
elif tx_hash in self.unverified_tx: elif tx_hash in self.unverified_tx:
height = self.unverified_tx[tx_hash] height = self.unverified_tx[tx_hash]
return (height, 0) if height > 0 else ((1e9 - height), 0) return (height, 0) if height > 0 else ((1e9 - height), 0)
else: else:
return (1e9+1, 0) return (1e9+1, 0)
def with_local_height_cached(func):
# get local height only once, as it's relatively expensive.
# take care that nested calls work as expected
def f(self, *args, **kwargs):
orig_val = getattr(self.threadlocal_cache, 'local_height', None)
self.threadlocal_cache.local_height = orig_val or self.get_local_height()
try:
return func(self, *args, **kwargs)
finally:
self.threadlocal_cache.local_height = orig_val
return f
@with_local_height_cached
def get_history(self, domain=None): def get_history(self, domain=None):
# get domain # get domain
if domain is None: if domain is None:
@ -462,16 +484,16 @@ class AddressSynchronizer(PrintError):
history = [] history = []
for tx_hash in tx_deltas: for tx_hash in tx_deltas:
delta = tx_deltas[tx_hash] delta = tx_deltas[tx_hash]
height, conf, timestamp = self.get_tx_height(tx_hash) tx_mined_status = self.get_tx_height(tx_hash)
history.append((tx_hash, height, conf, timestamp, delta)) history.append((tx_hash, tx_mined_status, delta))
history.sort(key = lambda x: self.get_txpos(x[0])) history.sort(key = lambda x: self.get_txpos(x[0]))
history.reverse() history.reverse()
# 3. add balance # 3. add balance
c, u, x = self.get_balance(domain) c, u, x = self.get_balance(domain)
balance = c + u + x balance = c + u + x
h2 = [] h2 = []
for tx_hash, height, conf, timestamp, delta in history: for tx_hash, tx_mined_status, delta in history:
h2.append((tx_hash, height, conf, timestamp, delta, balance)) h2.append((tx_hash, tx_mined_status, delta, balance))
if balance is None or delta is None: if balance is None or delta is None:
balance = None balance = None
else: else:
@ -503,25 +525,27 @@ class AddressSynchronizer(PrintError):
self._history_local[addr] = cur_hist self._history_local[addr] = cur_hist
def add_unverified_tx(self, tx_hash, tx_height): def add_unverified_tx(self, tx_hash, tx_height):
if tx_height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) \ if tx_hash in self.verified_tx:
and tx_hash in self.verified_tx: if tx_height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT):
with self.lock:
self.verified_tx.pop(tx_hash)
if self.verifier:
self.verifier.remove_spv_proof_for_tx(tx_hash)
else:
with self.lock: with self.lock:
self.verified_tx.pop(tx_hash) # tx will be verified only if height > 0
self.unverified_tx[tx_hash] = tx_height
# to remove pending proof requests:
if self.verifier: if self.verifier:
self.verifier.remove_spv_proof_for_tx(tx_hash) self.verifier.remove_spv_proof_for_tx(tx_hash)
# tx will be verified only if height > 0 def add_verified_tx(self, tx_hash: str, info: VerifiedTxInfo):
if tx_hash not in self.verified_tx:
with self.lock:
self.unverified_tx[tx_hash] = tx_height
def add_verified_tx(self, tx_hash, info):
# Remove from the unverified map and add to the verified map # Remove from the unverified map and add to the verified map
with self.lock: with self.lock:
self.unverified_tx.pop(tx_hash, None) self.unverified_tx.pop(tx_hash, None)
self.verified_tx[tx_hash] = info # (tx_height, timestamp, pos) self.verified_tx[tx_hash] = info
height, conf, timestamp = self.get_tx_height(tx_hash) tx_mined_status = self.get_tx_height(tx_hash)
self.network.trigger_callback('verified', tx_hash, height, conf, timestamp) self.network.trigger_callback('verified', tx_hash, tx_mined_status)
def get_unverified_txs(self): def get_unverified_txs(self):
'''Returns a map from tx hash to transaction height''' '''Returns a map from tx hash to transaction height'''
@ -532,33 +556,45 @@ class AddressSynchronizer(PrintError):
'''Used by the verifier when a reorg has happened''' '''Used by the verifier when a reorg has happened'''
txs = set() txs = set()
with self.lock: with self.lock:
for tx_hash, item in list(self.verified_tx.items()): for tx_hash, info in list(self.verified_tx.items()):
tx_height, timestamp, pos = item tx_height = info.height
if tx_height >= height: if tx_height >= height:
header = blockchain.read_header(tx_height) header = blockchain.read_header(tx_height)
# fixme: use block hash, not timestamp if not header or hash_header(header) != info.header_hash:
if not header or header.get('timestamp') != timestamp:
self.verified_tx.pop(tx_hash, None) self.verified_tx.pop(tx_hash, None)
# NOTE: we should add these txns to self.unverified_tx,
# but with what height?
# If on the new fork after the reorg, the txn is at the
# same height, we will not get a status update for the
# address. If the txn is not mined or at a diff height,
# we should get a status update. Unless we put tx into
# unverified_tx, it will turn into local. So we put it
# into unverified_tx with the old height, and if we get
# a status update, that will overwrite it.
self.unverified_tx[tx_hash] = tx_height
txs.add(tx_hash) txs.add(tx_hash)
return txs return txs
def get_local_height(self): def get_local_height(self):
""" return last known height if we are offline """ """ return last known height if we are offline """
cached_local_height = getattr(self.threadlocal_cache, 'local_height', None)
if cached_local_height is not None:
return cached_local_height
return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0) return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0)
def get_tx_height(self, tx_hash): def get_tx_height(self, tx_hash: str) -> TxMinedStatus:
""" Given a transaction, returns (height, conf, timestamp) """ """ Given a transaction, returns (height, conf, timestamp, header_hash) """
with self.lock: with self.lock:
if tx_hash in self.verified_tx: if tx_hash in self.verified_tx:
height, timestamp, pos = self.verified_tx[tx_hash] info = self.verified_tx[tx_hash]
conf = max(self.get_local_height() - height + 1, 0) conf = max(self.get_local_height() - info.height + 1, 0)
return height, conf, timestamp return TxMinedStatus(info.height, conf, info.timestamp, info.header_hash)
elif tx_hash in self.unverified_tx: elif tx_hash in self.unverified_tx:
height = self.unverified_tx[tx_hash] height = self.unverified_tx[tx_hash]
return height, 0, None return TxMinedStatus(height, 0, None, None)
else: else:
# local transaction # local transaction
return TX_HEIGHT_LOCAL, 0, None return TxMinedStatus(TX_HEIGHT_LOCAL, 0, None, None)
def set_up_to_date(self, up_to_date): def set_up_to_date(self, up_to_date):
with self.lock: with self.lock:
@ -691,8 +727,11 @@ class AddressSynchronizer(PrintError):
received, sent = self.get_addr_io(address) received, sent = self.get_addr_io(address)
return sum([v for height, v, is_cb in received.values()]) return sum([v for height, v, is_cb in received.values()])
# return the balance of a bitcoin address: confirmed and matured, unconfirmed, unmatured @with_local_height_cached
def get_addr_balance(self, address): def get_addr_balance(self, address):
"""Return the balance of a bitcoin address:
confirmed and matured, unconfirmed, unmatured
"""
received, sent = self.get_addr_io(address) received, sent = self.get_addr_io(address)
c = u = x = 0 c = u = x = 0
local_height = self.get_local_height() local_height = self.get_local_height()
@ -710,6 +749,7 @@ class AddressSynchronizer(PrintError):
u -= v u -= v
return c, u, x return c, u, x
@with_local_height_cached
def get_utxos(self, domain=None, excluded=None, mature=False, confirmed_only=False): def get_utxos(self, domain=None, excluded=None, mature=False, confirmed_only=False):
coins = [] coins = []
if domain is None: if domain is None:
@ -742,10 +782,7 @@ class AddressSynchronizer(PrintError):
def is_used(self, address): def is_used(self, address):
h = self.history.get(address,[]) h = self.history.get(address,[])
if len(h) == 0: return len(h) != 0
return False
c, u, x = self.get_addr_balance(address)
return c + u + x == 0
def is_empty(self, address): def is_empty(self, address):
c, u, x = self.get_addr_balance(address) c, u, x = self.get_addr_balance(address)

18
electrum/base_wizard.py

@ -34,7 +34,7 @@ from .keystore import bip44_derivation, purpose48_derivation
from .wallet import Imported_Wallet, Standard_Wallet, Multisig_Wallet, wallet_types, Wallet from .wallet import Imported_Wallet, Standard_Wallet, Multisig_Wallet, wallet_types, Wallet
from .storage import STO_EV_USER_PW, STO_EV_XPUB_PW, get_derivation_used_for_hw_device_encryption from .storage import STO_EV_USER_PW, STO_EV_XPUB_PW, get_derivation_used_for_hw_device_encryption
from .i18n import _ from .i18n import _
from .util import UserCancelled, InvalidPassword from .util import UserCancelled, InvalidPassword, WalletFileException
# hardware device setup purpose # hardware device setup purpose
HWD_SETUP_NEW_WALLET, HWD_SETUP_DECRYPT_WALLET = range(0, 2) HWD_SETUP_NEW_WALLET, HWD_SETUP_DECRYPT_WALLET = range(0, 2)
@ -106,10 +106,20 @@ class BaseWizard(object):
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.on_wallet_type) self.choice_dialog(title=title, message=message, choices=choices, run_next=self.on_wallet_type)
def upgrade_storage(self): def upgrade_storage(self):
exc = None
def on_finished(): def on_finished():
self.wallet = Wallet(self.storage) if exc is None:
self.terminate() self.wallet = Wallet(self.storage)
self.waiting_dialog(partial(self.storage.upgrade), _('Upgrading wallet format...'), on_finished=on_finished) self.terminate()
else:
raise exc
def do_upgrade():
nonlocal exc
try:
self.storage.upgrade()
except Exception as e:
exc = e
self.waiting_dialog(do_upgrade, _('Upgrading wallet format...'), on_finished=on_finished)
def load_2fa(self): def load_2fa(self):
self.storage.put('wallet_type', '2fa') self.storage.put('wallet_type', '2fa')

94
electrum/blockchain.py

@ -79,12 +79,12 @@ def read_blockchains(config):
l = filter(lambda x: x.startswith('fork_'), os.listdir(fdir)) l = filter(lambda x: x.startswith('fork_'), os.listdir(fdir))
l = sorted(l, key = lambda x: int(x.split('_')[1])) l = sorted(l, key = lambda x: int(x.split('_')[1]))
for filename in l: for filename in l:
checkpoint = int(filename.split('_')[2]) forkpoint = int(filename.split('_')[2])
parent_id = int(filename.split('_')[1]) parent_id = int(filename.split('_')[1])
b = Blockchain(config, checkpoint, parent_id) b = Blockchain(config, forkpoint, parent_id)
h = b.read_header(b.checkpoint) h = b.read_header(b.forkpoint)
if b.parent().can_connect(h, check_height=False): if b.parent().can_connect(h, check_height=False):
blockchains[b.checkpoint] = b blockchains[b.forkpoint] = b
else: else:
util.print_error("cannot connect", filename) util.print_error("cannot connect", filename)
return blockchains return blockchains
@ -109,32 +109,39 @@ class Blockchain(util.PrintError):
Manages blockchain headers and their verification Manages blockchain headers and their verification
""" """
def __init__(self, config, checkpoint, parent_id): def __init__(self, config, forkpoint, parent_id):
self.config = config self.config = config
self.catch_up = None # interface catching up self.catch_up = None # interface catching up
self.checkpoint = checkpoint self.forkpoint = forkpoint
self.checkpoints = constants.net.CHECKPOINTS self.checkpoints = constants.net.CHECKPOINTS
self.parent_id = parent_id self.parent_id = parent_id
self.lock = threading.Lock() assert parent_id != forkpoint
self.lock = threading.RLock()
with self.lock: with self.lock:
self.update_size() self.update_size()
def with_lock(func):
def func_wrapper(self, *args, **kwargs):
with self.lock:
return func(self, *args, **kwargs)
return func_wrapper
def parent(self): def parent(self):
return blockchains[self.parent_id] return blockchains[self.parent_id]
def get_max_child(self): def get_max_child(self):
children = list(filter(lambda y: y.parent_id==self.checkpoint, blockchains.values())) children = list(filter(lambda y: y.parent_id==self.forkpoint, blockchains.values()))
return max([x.checkpoint for x in children]) if children else None return max([x.forkpoint for x in children]) if children else None
def get_checkpoint(self): def get_forkpoint(self):
mc = self.get_max_child() mc = self.get_max_child()
return mc if mc is not None else self.checkpoint return mc if mc is not None else self.forkpoint
def get_branch_size(self): def get_branch_size(self):
return self.height() - self.get_checkpoint() + 1 return self.height() - self.get_forkpoint() + 1
def get_name(self): def get_name(self):
return self.get_hash(self.get_checkpoint()).lstrip('00')[0:10] return self.get_hash(self.get_forkpoint()).lstrip('00')[0:10]
def check_header(self, header): def check_header(self, header):
header_hash = hash_header(header) header_hash = hash_header(header)
@ -142,14 +149,14 @@ class Blockchain(util.PrintError):
return header_hash == self.get_hash(height) return header_hash == self.get_hash(height)
def fork(parent, header): def fork(parent, header):
checkpoint = header.get('block_height') forkpoint = header.get('block_height')
self = Blockchain(parent.config, checkpoint, parent.checkpoint) self = Blockchain(parent.config, forkpoint, parent.forkpoint)
open(self.path(), 'w+').close() open(self.path(), 'w+').close()
self.save_header(header) self.save_header(header)
return self return self
def height(self): def height(self):
return self.checkpoint + self.size() - 1 return self.forkpoint + self.size() - 1
def size(self): def size(self):
with self.lock: with self.lock:
@ -183,44 +190,55 @@ class Blockchain(util.PrintError):
def path(self): def path(self):
d = util.get_headers_dir(self.config) d = util.get_headers_dir(self.config)
filename = 'blockchain_headers' if self.parent_id is None else os.path.join('forks', 'fork_%d_%d'%(self.parent_id, self.checkpoint)) filename = 'blockchain_headers' if self.parent_id is None else os.path.join('forks', 'fork_%d_%d'%(self.parent_id, self.forkpoint))
return os.path.join(d, filename) return os.path.join(d, filename)
@with_lock
def save_chunk(self, index, chunk): def save_chunk(self, index, chunk):
filename = self.path() chunk_within_checkpoint_region = index < len(self.checkpoints)
d = (index * 2016 - self.checkpoint) * 80 # chunks in checkpoint region are the responsibility of the 'main chain'
if d < 0: if chunk_within_checkpoint_region and self.parent_id is not None:
chunk = chunk[-d:] main_chain = blockchains[0]
d = 0 main_chain.save_chunk(index, chunk)
truncate = index >= len(self.checkpoints) return
self.write(chunk, d, truncate)
delta_height = (index * 2016 - self.forkpoint)
delta_bytes = delta_height * 80
# if this chunk contains our forkpoint, only save the part after forkpoint
# (the part before is the responsibility of the parent)
if delta_bytes < 0:
chunk = chunk[-delta_bytes:]
delta_bytes = 0
truncate = not chunk_within_checkpoint_region
self.write(chunk, delta_bytes, truncate)
self.swap_with_parent() self.swap_with_parent()
@with_lock
def swap_with_parent(self): def swap_with_parent(self):
if self.parent_id is None: if self.parent_id is None:
return return
parent_branch_size = self.parent().height() - self.checkpoint + 1 parent_branch_size = self.parent().height() - self.forkpoint + 1
if parent_branch_size >= self.size(): if parent_branch_size >= self.size():
return return
self.print_error("swap", self.checkpoint, self.parent_id) self.print_error("swap", self.forkpoint, self.parent_id)
parent_id = self.parent_id parent_id = self.parent_id
checkpoint = self.checkpoint forkpoint = self.forkpoint
parent = self.parent() parent = self.parent()
self.assert_headers_file_available(self.path()) self.assert_headers_file_available(self.path())
with open(self.path(), 'rb') as f: with open(self.path(), 'rb') as f:
my_data = f.read() my_data = f.read()
self.assert_headers_file_available(parent.path()) self.assert_headers_file_available(parent.path())
with open(parent.path(), 'rb') as f: with open(parent.path(), 'rb') as f:
f.seek((checkpoint - parent.checkpoint)*80) f.seek((forkpoint - parent.forkpoint)*80)
parent_data = f.read(parent_branch_size*80) parent_data = f.read(parent_branch_size*80)
self.write(parent_data, 0) self.write(parent_data, 0)
parent.write(my_data, (checkpoint - parent.checkpoint)*80) parent.write(my_data, (forkpoint - parent.forkpoint)*80)
# store file path # store file path
for b in blockchains.values(): for b in blockchains.values():
b.old_path = b.path() b.old_path = b.path()
# swap parameters # swap parameters
self.parent_id = parent.parent_id; parent.parent_id = parent_id self.parent_id = parent.parent_id; parent.parent_id = parent_id
self.checkpoint = parent.checkpoint; parent.checkpoint = checkpoint self.forkpoint = parent.forkpoint; parent.forkpoint = forkpoint
self._size = parent._size; parent._size = parent_branch_size self._size = parent._size; parent._size = parent_branch_size
# move files # move files
for b in blockchains.values(): for b in blockchains.values():
@ -229,8 +247,8 @@ class Blockchain(util.PrintError):
self.print_error("renaming", b.old_path, b.path()) self.print_error("renaming", b.old_path, b.path())
os.rename(b.old_path, b.path()) os.rename(b.old_path, b.path())
# update pointers # update pointers
blockchains[self.checkpoint] = self blockchains[self.forkpoint] = self
blockchains[parent.checkpoint] = parent blockchains[parent.forkpoint] = parent
def assert_headers_file_available(self, path): def assert_headers_file_available(self, path):
if os.path.exists(path): if os.path.exists(path):
@ -254,23 +272,25 @@ class Blockchain(util.PrintError):
os.fsync(f.fileno()) os.fsync(f.fileno())
self.update_size() self.update_size()
@with_lock
def save_header(self, header): def save_header(self, header):
delta = header.get('block_height') - self.checkpoint delta = header.get('block_height') - self.forkpoint
data = bfh(serialize_header(header)) data = bfh(serialize_header(header))
# headers are only _appended_ to the end:
assert delta == self.size() assert delta == self.size()
assert len(data) == 80 assert len(data) == 80
self.write(data, delta*80) self.write(data, delta*80)
self.swap_with_parent() self.swap_with_parent()
def read_header(self, height): def read_header(self, height):
assert self.parent_id != self.checkpoint assert self.parent_id != self.forkpoint
if height < 0: if height < 0:
return return
if height < self.checkpoint: if height < self.forkpoint:
return self.parent().read_header(height) return self.parent().read_header(height)
if height > self.height(): if height > self.height():
return return
delta = height - self.checkpoint delta = height - self.forkpoint
name = self.path() name = self.path()
self.assert_headers_file_available(name) self.assert_headers_file_available(name)
with open(name, 'rb') as f: with open(name, 'rb') as f:

4
electrum/coinchooser.py

@ -26,7 +26,7 @@ from collections import defaultdict, namedtuple
from math import floor, log10 from math import floor, log10
from .bitcoin import sha256, COIN, TYPE_ADDRESS, is_address from .bitcoin import sha256, COIN, TYPE_ADDRESS, is_address
from .transaction import Transaction from .transaction import Transaction, TxOutput
from .util import NotEnoughFunds, PrintError from .util import NotEnoughFunds, PrintError
@ -178,7 +178,7 @@ class CoinChooserBase(PrintError):
# size of the change output, add it to the transaction. # size of the change output, add it to the transaction.
dust = sum(amount for amount in amounts if amount < dust_threshold) dust = sum(amount for amount in amounts if amount < dust_threshold)
amounts = [amount for amount in amounts if amount >= dust_threshold] amounts = [amount for amount in amounts if amount >= dust_threshold]
change = [(TYPE_ADDRESS, addr, amount) change = [TxOutput(TYPE_ADDRESS, addr, amount)
for addr, amount in zip(change_addrs, amounts)] for addr, amount in zip(change_addrs, amounts)]
self.print_error('change:', change) self.print_error('change:', change)
if dust: if dust:

7
electrum/commands.py

@ -38,7 +38,7 @@ from .util import bfh, bh2u, format_satoshis, json_decode, print_error, json_enc
from . import bitcoin from . import bitcoin
from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS
from .i18n import _ from .i18n import _
from .transaction import Transaction, multisig_script from .transaction import Transaction, multisig_script, TxOutput
from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
from .plugin import run_hook from .plugin import run_hook
@ -226,7 +226,7 @@ class Commands:
txin['signatures'] = [None] txin['signatures'] = [None]
txin['num_sig'] = 1 txin['num_sig'] = 1
outputs = [(TYPE_ADDRESS, x['address'], int(x['value'])) for x in outputs] outputs = [TxOutput(TYPE_ADDRESS, x['address'], int(x['value'])) for x in outputs]
tx = Transaction.from_io(inputs, outputs, locktime=locktime) tx = Transaction.from_io(inputs, outputs, locktime=locktime)
tx.sign(keypairs) tx.sign(keypairs)
return tx.as_dict() return tx.as_dict()
@ -415,7 +415,7 @@ class Commands:
for address, amount in outputs: for address, amount in outputs:
address = self._resolver(address) address = self._resolver(address)
amount = satoshis(amount) amount = satoshis(amount)
final_outputs.append((TYPE_ADDRESS, address, amount)) final_outputs.append(TxOutput(TYPE_ADDRESS, address, amount))
coins = self.wallet.get_spendable_coins(domain, self.config) coins = self.wallet.get_spendable_coins(domain, self.config)
tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr) tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr)
@ -826,6 +826,7 @@ def add_network_options(parser):
parser.add_argument("-1", "--oneserver", action="store_true", dest="oneserver", default=None, help="connect to one server only") parser.add_argument("-1", "--oneserver", action="store_true", dest="oneserver", default=None, help="connect to one server only")
parser.add_argument("-s", "--server", dest="server", default=None, help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)") parser.add_argument("-s", "--server", dest="server", default=None, help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)")
parser.add_argument("-p", "--proxy", dest="proxy", default=None, help="set proxy [type:]host[:port], where type is socks4,socks5 or http") parser.add_argument("-p", "--proxy", dest="proxy", default=None, help="set proxy [type:]host[:port], where type is socks4,socks5 or http")
parser.add_argument("--noonion", action="store_true", dest="noonion", default=None, help="do not try to connect to onion servers")
def add_global_options(parser): def add_global_options(parser):
group = parser.add_argument_group('global options') group = parser.add_argument_group('global options')

3
electrum/ecc_fast.py

@ -72,6 +72,9 @@ def load_library():
secp256k1.secp256k1_ecdsa_signature_parse_compact.argtypes = [c_void_p, c_char_p, c_char_p] secp256k1.secp256k1_ecdsa_signature_parse_compact.argtypes = [c_void_p, c_char_p, c_char_p]
secp256k1.secp256k1_ecdsa_signature_parse_compact.restype = c_int secp256k1.secp256k1_ecdsa_signature_parse_compact.restype = c_int
secp256k1.secp256k1_ecdsa_signature_normalize.argtypes = [c_void_p, c_char_p, c_char_p]
secp256k1.secp256k1_ecdsa_signature_normalize.restype = c_int
secp256k1.secp256k1_ecdsa_signature_serialize_compact.argtypes = [c_void_p, c_char_p, c_char_p] secp256k1.secp256k1_ecdsa_signature_serialize_compact.argtypes = [c_void_p, c_char_p, c_char_p]
secp256k1.secp256k1_ecdsa_signature_serialize_compact.restype = c_int secp256k1.secp256k1_ecdsa_signature_serialize_compact.restype = c_int

13
electrum/gui/kivy/main_window.py

@ -88,7 +88,7 @@ class ElectrumWindow(App):
balance = StringProperty('') balance = StringProperty('')
fiat_balance = StringProperty('') fiat_balance = StringProperty('')
is_fiat = BooleanProperty(False) is_fiat = BooleanProperty(False)
blockchain_checkpoint = NumericProperty(0) blockchain_forkpoint = NumericProperty(0)
auto_connect = BooleanProperty(False) auto_connect = BooleanProperty(False)
def on_auto_connect(self, instance, x): def on_auto_connect(self, instance, x):
@ -159,6 +159,9 @@ class ElectrumWindow(App):
Logger.info("on_history") Logger.info("on_history")
self._trigger_update_history() self._trigger_update_history()
def on_fee_histogram(self, *args):
self._trigger_update_history()
def _get_bu(self): def _get_bu(self):
decimal_point = self.electrum_config.get('decimal_point', 5) decimal_point = self.electrum_config.get('decimal_point', 5)
return decimal_point_to_base_unit_name(decimal_point) return decimal_point_to_base_unit_name(decimal_point)
@ -483,7 +486,7 @@ class ElectrumWindow(App):
interests = ['updated', 'status', 'new_transaction', 'verified', 'interfaces'] interests = ['updated', 'status', 'new_transaction', 'verified', 'interfaces']
self.network.register_callback(self.on_network_event, interests) self.network.register_callback(self.on_network_event, interests)
self.network.register_callback(self.on_fee, ['fee']) self.network.register_callback(self.on_fee, ['fee'])
self.network.register_callback(self.on_history, ['fee_histogram']) self.network.register_callback(self.on_fee_histogram, ['fee_histogram'])
self.network.register_callback(self.on_quotes, ['on_quotes']) self.network.register_callback(self.on_quotes, ['on_quotes'])
self.network.register_callback(self.on_history, ['on_history']) self.network.register_callback(self.on_history, ['on_history'])
# load wallet # load wallet
@ -651,7 +654,7 @@ class ElectrumWindow(App):
self.num_nodes = len(self.network.get_interfaces()) self.num_nodes = len(self.network.get_interfaces())
self.num_chains = len(self.network.get_blockchains()) self.num_chains = len(self.network.get_blockchains())
chain = self.network.blockchain() chain = self.network.blockchain()
self.blockchain_checkpoint = chain.get_checkpoint() self.blockchain_forkpoint = chain.get_forkpoint()
self.blockchain_name = chain.get_name() self.blockchain_name = chain.get_name()
interface = self.network.interface interface = self.network.interface
if interface: if interface:
@ -710,13 +713,14 @@ class ElectrumWindow(App):
self.fiat_balance = self.fx.format_amount(c+u+x) + ' [size=22dp]%s[/size]'% self.fx.ccy self.fiat_balance = self.fx.format_amount(c+u+x) + ' [size=22dp]%s[/size]'% self.fx.ccy
def get_max_amount(self): def get_max_amount(self):
from electrum.transaction import TxOutput
if run_hook('abort_send', self): if run_hook('abort_send', self):
return '' return ''
inputs = self.wallet.get_spendable_coins(None, self.electrum_config) inputs = self.wallet.get_spendable_coins(None, self.electrum_config)
if not inputs: if not inputs:
return '' return ''
addr = str(self.send_screen.screen.address) or self.wallet.dummy_address() addr = str(self.send_screen.screen.address) or self.wallet.dummy_address()
outputs = [(TYPE_ADDRESS, addr, '!')] outputs = [TxOutput(TYPE_ADDRESS, addr, '!')]
try: try:
tx = self.wallet.make_unsigned_transaction(inputs, outputs, self.electrum_config) tx = self.wallet.make_unsigned_transaction(inputs, outputs, self.electrum_config)
except NoDynamicFeeEstimates as e: except NoDynamicFeeEstimates as e:
@ -883,6 +887,7 @@ class ElectrumWindow(App):
self.wallet.invoices.save() self.wallet.invoices.save()
self.update_tab('invoices') self.update_tab('invoices')
else: else:
msg = msg[:500] if msg else _('There was an error broadcasting the transaction.')
self.show_error(msg) self.show_error(msg)
if self.network and self.network.is_connected(): if self.network and self.network.is_connected():

6
electrum/gui/kivy/uix/dialogs/__init__.py

@ -206,9 +206,9 @@ class OutputList(RecycleView):
def update(self, outputs): def update(self, outputs):
res = [] res = []
for (type, address, amount) in outputs: for o in outputs:
value = self.app.format_amount_and_units(amount) value = self.app.format_amount_and_units(o.value)
res.append({'address': address, 'value': value}) res.append({'address': o.address, 'value': value})
self.data = res self.data = res

8
electrum/gui/kivy/uix/dialogs/addresses.py

@ -136,14 +136,14 @@ class AddressesDialog(Factory.Popup):
for address in _list: for address in _list:
label = wallet.labels.get(address, '') label = wallet.labels.get(address, '')
balance = sum(wallet.get_addr_balance(address)) balance = sum(wallet.get_addr_balance(address))
is_used = wallet.is_used(address) is_used_and_empty = wallet.is_used(address) and balance == 0
if self.show_used == 1 and (balance or is_used): if self.show_used == 1 and (balance or is_used_and_empty):
continue continue
if self.show_used == 2 and balance == 0: if self.show_used == 2 and balance == 0:
continue continue
if self.show_used == 3 and not is_used: if self.show_used == 3 and not is_used_and_empty:
continue continue
card = self.get_card(address, balance, is_used, label) card = self.get_card(address, balance, is_used_and_empty, label)
if search and not self.ext_search(card, search): if search and not self.ext_search(card, search):
continue continue
cards.append(card) cards.append(card)

7
electrum/gui/kivy/uix/dialogs/installwizard.py

@ -957,7 +957,12 @@ class InstallWizard(BaseWizard, Widget):
# on completion hide message # on completion hide message
Clock.schedule_once(lambda dt: app.info_bubble.hide(now=True), -1) Clock.schedule_once(lambda dt: app.info_bubble.hide(now=True), -1)
if on_finished: if on_finished:
Clock.schedule_once(lambda dt: on_finished(), -1) def protected_on_finished():
try:
on_finished()
except Exception as e:
self.show_error(str(e))
Clock.schedule_once(lambda dt: protected_on_finished(), -1)
app = App.get_running_app() app = App.get_running_app()
app.show_info_bubble( app.show_info_bubble(

10
electrum/gui/kivy/uix/screens.py

@ -20,6 +20,7 @@ from kivy.utils import platform
from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat
from electrum import bitcoin from electrum import bitcoin
from electrum.transaction import TxOutput
from electrum.util import timestamp_to_datetime from electrum.util import timestamp_to_datetime
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
from electrum.plugin import run_hook from electrum.plugin import run_hook
@ -131,8 +132,8 @@ class HistoryScreen(CScreen):
d = LabelDialog(_('Enter Transaction Label'), text, callback) d = LabelDialog(_('Enter Transaction Label'), text, callback)
d.open() d.open()
def get_card(self, tx_hash, height, conf, timestamp, value, balance): def get_card(self, tx_hash, tx_mined_status, value, balance):
status, status_str = self.app.wallet.get_tx_status(tx_hash, height, conf, timestamp) status, status_str = self.app.wallet.get_tx_status(tx_hash, tx_mined_status)
icon = "atlas://electrum/gui/kivy/theming/light/" + TX_ICONS[status] icon = "atlas://electrum/gui/kivy/theming/light/" + TX_ICONS[status]
label = self.app.wallet.get_label(tx_hash) if tx_hash else _('Pruned transaction outputs') label = self.app.wallet.get_label(tx_hash) if tx_hash else _('Pruned transaction outputs')
ri = {} ri = {}
@ -141,7 +142,7 @@ class HistoryScreen(CScreen):
ri['icon'] = icon ri['icon'] = icon
ri['date'] = status_str ri['date'] = status_str
ri['message'] = label ri['message'] = label
ri['confirmations'] = conf ri['confirmations'] = tx_mined_status.conf
if value is not None: if value is not None:
ri['is_mine'] = value < 0 ri['is_mine'] = value < 0
if value < 0: value = - value if value < 0: value = - value
@ -158,7 +159,6 @@ class HistoryScreen(CScreen):
return return
history = reversed(self.app.wallet.get_history()) history = reversed(self.app.wallet.get_history())
history_card = self.screen.ids.history_container history_card = self.screen.ids.history_container
count = 0
history_card.data = [self.get_card(*item) for item in history] history_card.data = [self.get_card(*item) for item in history]
@ -257,7 +257,7 @@ class SendScreen(CScreen):
except: except:
self.app.show_error(_('Invalid amount') + ':\n' + self.screen.amount) self.app.show_error(_('Invalid amount') + ':\n' + self.screen.amount)
return return
outputs = [(bitcoin.TYPE_ADDRESS, address, amount)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, address, amount)]
message = self.screen.message message = self.screen.message
amount = sum(map(lambda x:x[2], outputs)) amount = sum(map(lambda x:x[2], outputs))
if self.app.electrum_config.get('use_rbf'): if self.app.electrum_config.get('use_rbf'):

2
electrum/gui/kivy/uix/ui_screens/network.kv

@ -46,7 +46,7 @@ Popup:
CardSeparator CardSeparator
SettingsItem: SettingsItem:
title: _('Fork detected at block {}').format(app.blockchain_checkpoint) if app.num_chains>1 else _('No fork detected') title: _('Fork detected at block {}').format(app.blockchain_forkpoint) if app.num_chains>1 else _('No fork detected')
fork_description: (_('You are following branch') if app.auto_connect else _("Your server is on branch")) + ' ' + app.blockchain_name fork_description: (_('You are following branch') if app.auto_connect else _("Your server is on branch")) + ' ' + app.blockchain_name
description: self.fork_description if app.num_chains>1 else _('Connected nodes are on the same chain') description: self.fork_description if app.num_chains>1 else _('Connected nodes are on the same chain')
action: app.choose_blockchain_dialog action: app.choose_blockchain_dialog

10
electrum/gui/qt/address_list.py

@ -93,20 +93,20 @@ class AddressList(MyTreeWidget):
else: else:
addr_list = self.wallet.get_addresses() addr_list = self.wallet.get_addresses()
self.clear() self.clear()
fx = self.parent.fx
for address in addr_list: for address in addr_list:
num = len(self.wallet.get_address_history(address)) num = self.wallet.get_address_history_len(address)
is_used = self.wallet.is_used(address)
label = self.wallet.labels.get(address, '') label = self.wallet.labels.get(address, '')
c, u, x = self.wallet.get_addr_balance(address) c, u, x = self.wallet.get_addr_balance(address)
balance = c + u + x balance = c + u + x
if self.show_used == 1 and (balance or is_used): is_used_and_empty = self.wallet.is_used(address) and balance == 0
if self.show_used == 1 and (balance or is_used_and_empty):
continue continue
if self.show_used == 2 and balance == 0: if self.show_used == 2 and balance == 0:
continue continue
if self.show_used == 3 and not is_used: if self.show_used == 3 and not is_used_and_empty:
continue continue
balance_text = self.parent.format_amount(balance, whitespaces=True) balance_text = self.parent.format_amount(balance, whitespaces=True)
fx = self.parent.fx
# create item # create item
if fx and fx.get_fiat_address_config(): if fx and fx.get_fiat_address_config():
rate = fx.exchange_rate() rate = fx.exchange_rate()

23
electrum/gui/qt/history_list.py

@ -29,7 +29,7 @@ import datetime
from electrum.address_synchronizer import TX_HEIGHT_LOCAL from electrum.address_synchronizer import TX_HEIGHT_LOCAL
from .util import * from .util import *
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import block_explorer_URL, profiler, print_error from electrum.util import block_explorer_URL, profiler, print_error, TxMinedStatus
try: try:
from electrum.plot import plot_history, NothingToPlotException from electrum.plot import plot_history, NothingToPlotException
@ -229,6 +229,9 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
current_tx = item.data(0, Qt.UserRole) if item else None current_tx = item.data(0, Qt.UserRole) if item else None
self.clear() self.clear()
if fx: fx.history_used_spot = False if fx: fx.history_used_spot = False
blue_brush = QBrush(QColor("#1E1EFF"))
red_brush = QBrush(QColor("#BC1E1E"))
monospace_font = QFont(MONOSPACE_FONT)
for tx_item in self.transactions: for tx_item in self.transactions:
tx_hash = tx_item['txid'] tx_hash = tx_item['txid']
height = tx_item['height'] height = tx_item['height']
@ -237,7 +240,8 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
value = tx_item['value'].value value = tx_item['value'].value
balance = tx_item['balance'].value balance = tx_item['balance'].value
label = tx_item['label'] label = tx_item['label']
status, status_str = self.wallet.get_tx_status(tx_hash, height, conf, timestamp) tx_mined_status = TxMinedStatus(height, conf, timestamp, None)
status, status_str = self.wallet.get_tx_status(tx_hash, tx_mined_status)
has_invoice = self.wallet.invoices.paid.get(tx_hash) has_invoice = self.wallet.invoices.paid.get(tx_hash)
icon = self.icon_cache.get(":icons/" + TX_ICONS[status]) icon = self.icon_cache.get(":icons/" + TX_ICONS[status])
v_str = self.parent.format_amount(value, is_diff=True, whitespaces=True) v_str = self.parent.format_amount(value, is_diff=True, whitespaces=True)
@ -262,12 +266,12 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
if i>3: if i>3:
item.setTextAlignment(i, Qt.AlignRight | Qt.AlignVCenter) item.setTextAlignment(i, Qt.AlignRight | Qt.AlignVCenter)
if i!=2: if i!=2:
item.setFont(i, QFont(MONOSPACE_FONT)) item.setFont(i, monospace_font)
if value and value < 0: if value and value < 0:
item.setForeground(3, QBrush(QColor("#BC1E1E"))) item.setForeground(3, red_brush)
item.setForeground(4, QBrush(QColor("#BC1E1E"))) item.setForeground(4, red_brush)
if fiat_value and not tx_item['fiat_default']: if fiat_value and not tx_item['fiat_default']:
item.setForeground(6, QBrush(QColor("#1E1EFF"))) item.setForeground(6, blue_brush)
if tx_hash: if tx_hash:
item.setData(0, Qt.UserRole, tx_hash) item.setData(0, Qt.UserRole, tx_hash)
self.insertTopLevelItem(0, item) self.insertTopLevelItem(0, item)
@ -304,10 +308,11 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
label = self.wallet.get_label(txid) label = self.wallet.get_label(txid)
item.setText(3, label) item.setText(3, label)
def update_item(self, tx_hash, height, conf, timestamp): def update_item(self, tx_hash, tx_mined_status):
if self.wallet is None: if self.wallet is None:
return return
status, status_str = self.wallet.get_tx_status(tx_hash, height, conf, timestamp) conf = tx_mined_status.conf
status, status_str = self.wallet.get_tx_status(tx_hash, tx_mined_status)
icon = self.icon_cache.get(":icons/" + TX_ICONS[status]) icon = self.icon_cache.get(":icons/" + TX_ICONS[status])
items = self.findItems(tx_hash, Qt.UserRole|Qt.MatchContains|Qt.MatchRecursive, column=1) items = self.findItems(tx_hash, Qt.UserRole|Qt.MatchContains|Qt.MatchRecursive, column=1)
if items: if items:
@ -332,7 +337,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
column_title = self.headerItem().text(column) column_title = self.headerItem().text(column)
column_data = item.text(column) column_data = item.text(column)
tx_URL = block_explorer_URL(self.config, 'tx', tx_hash) tx_URL = block_explorer_URL(self.config, 'tx', tx_hash)
height, conf, timestamp = self.wallet.get_tx_height(tx_hash) height = self.wallet.get_tx_height(tx_hash).height
tx = self.wallet.transactions.get(tx_hash) tx = self.wallet.transactions.get(tx_hash)
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
is_unconfirmed = height <= 0 is_unconfirmed = height <= 0

12
electrum/gui/qt/main_window.py

@ -50,7 +50,7 @@ from electrum.util import (format_time, format_satoshis, format_fee_satoshis,
export_meta, import_meta, bh2u, bfh, InvalidPassword, export_meta, import_meta, bh2u, bfh, InvalidPassword,
base_units, base_units_list, base_unit_name_to_decimal_point, base_units, base_units_list, base_unit_name_to_decimal_point,
decimal_point_to_base_unit_name, quantize_feerate) decimal_point_to_base_unit_name, quantize_feerate)
from electrum.transaction import Transaction from electrum.transaction import Transaction, TxOutput
from electrum.address_synchronizer import AddTransactionException from electrum.address_synchronizer import AddTransactionException
from electrum.wallet import Multisig_Wallet, CannotBumpFee from electrum.wallet import Multisig_Wallet, CannotBumpFee
@ -1306,7 +1306,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
outputs = self.payto_e.get_outputs(self.is_max) outputs = self.payto_e.get_outputs(self.is_max)
if not outputs: if not outputs:
_type, addr = self.get_payto_or_dummy() _type, addr = self.get_payto_or_dummy()
outputs = [(_type, addr, amount)] outputs = [TxOutput(_type, addr, amount)]
is_sweep = bool(self.tx_external_keypairs) is_sweep = bool(self.tx_external_keypairs)
make_tx = lambda fee_est: \ make_tx = lambda fee_est: \
self.wallet.make_unsigned_transaction( self.wallet.make_unsigned_transaction(
@ -1485,14 +1485,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.show_error(_('No outputs')) self.show_error(_('No outputs'))
return return
for _type, addr, amount in outputs: for o in outputs:
if addr is None: if o.address is None:
self.show_error(_('Bitcoin Address is None')) self.show_error(_('Bitcoin Address is None'))
return return
if _type == TYPE_ADDRESS and not bitcoin.is_address(addr): if o.type == TYPE_ADDRESS and not bitcoin.is_address(o.address):
self.show_error(_('Invalid Bitcoin Address')) self.show_error(_('Invalid Bitcoin Address'))
return return
if amount is None: if o.value is None:
self.show_error(_('Invalid Amount')) self.show_error(_('Invalid Amount'))
return return

8
electrum/gui/qt/network_dialog.py

@ -106,9 +106,9 @@ class NodesListWidget(QTreeWidget):
b = network.blockchains[k] b = network.blockchains[k]
name = b.get_name() name = b.get_name()
if n_chains >1: if n_chains >1:
x = QTreeWidgetItem([name + '@%d'%b.get_checkpoint(), '%d'%b.height()]) x = QTreeWidgetItem([name + '@%d'%b.get_forkpoint(), '%d'%b.height()])
x.setData(0, Qt.UserRole, 1) x.setData(0, Qt.UserRole, 1)
x.setData(1, Qt.UserRole, b.checkpoint) x.setData(1, Qt.UserRole, b.forkpoint)
else: else:
x = self x = self
for i in items: for i in items:
@ -357,9 +357,9 @@ class NetworkChoiceLayout(object):
chains = self.network.get_blockchains() chains = self.network.get_blockchains()
if len(chains)>1: if len(chains)>1:
chain = self.network.blockchain() chain = self.network.blockchain()
checkpoint = chain.get_checkpoint() forkpoint = chain.get_forkpoint()
name = chain.get_name() name = chain.get_name()
msg = _('Chain split detected at block {0}').format(checkpoint) + '\n' msg = _('Chain split detected at block {0}').format(forkpoint) + '\n'
msg += (_('You are following branch') if auto_connect else _('Your server is on branch'))+ ' ' + name msg += (_('You are following branch') if auto_connect else _('Your server is on branch'))+ ' ' + name
msg += ' (%d %s)' % (chain.get_branch_size(), _('blocks')) msg += ' (%d %s)' % (chain.get_branch_size(), _('blocks'))
else: else:

13
electrum/gui/qt/paytoedit.py

@ -29,6 +29,7 @@ from decimal import Decimal
from electrum import bitcoin from electrum import bitcoin
from electrum.util import bfh from electrum.util import bfh
from electrum.transaction import TxOutput
from .qrtextedit import ScanQRTextEdit from .qrtextedit import ScanQRTextEdit
from .completion_text_edit import CompletionTextEdit from .completion_text_edit import CompletionTextEdit
@ -77,7 +78,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit):
x, y = line.split(',') x, y = line.split(',')
out_type, out = self.parse_output(x) out_type, out = self.parse_output(x)
amount = self.parse_amount(y) amount = self.parse_amount(y)
return out_type, out, amount return TxOutput(out_type, out, amount)
def parse_output(self, x): def parse_output(self, x):
try: try:
@ -139,16 +140,16 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit):
is_max = False is_max = False
for i, line in enumerate(lines): for i, line in enumerate(lines):
try: try:
_type, to_address, amount = self.parse_address_and_amount(line) output = self.parse_address_and_amount(line)
except: except:
self.errors.append((i, line.strip())) self.errors.append((i, line.strip()))
continue continue
outputs.append((_type, to_address, amount)) outputs.append(output)
if amount == '!': if output.value == '!':
is_max = True is_max = True
else: else:
total += amount total += output.value
self.win.is_max = is_max self.win.is_max = is_max
self.outputs = outputs self.outputs = outputs
@ -174,7 +175,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit):
amount = self.amount_edit.get_amount() amount = self.amount_edit.get_amount()
_type, addr = self.payto_address _type, addr = self.payto_address
self.outputs = [(_type, addr, amount)] self.outputs = [TxOutput(_type, addr, amount)]
return self.outputs[:] return self.outputs[:]

10
electrum/gui/stdio.py

@ -4,6 +4,7 @@ _ = lambda x:x
from electrum import WalletStorage, Wallet from electrum import WalletStorage, Wallet
from electrum.util import format_satoshis, set_verbosity from electrum.util import format_satoshis, set_verbosity
from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS
from electrum.transaction import TxOutput
import getpass, datetime import getpass, datetime
# minimal fdisk like gui for console usage # minimal fdisk like gui for console usage
@ -87,9 +88,9 @@ class ElectrumGui:
+ "%d"%(width[2]+delta)+"s"+"%"+"%d"%(width[3]+delta)+"s" + "%d"%(width[2]+delta)+"s"+"%"+"%d"%(width[3]+delta)+"s"
messages = [] messages = []
for item in self.wallet.get_history(): for tx_hash, tx_mined_status, delta, balance in self.wallet.get_history():
tx_hash, height, conf, timestamp, delta, balance = item if tx_mined_status.conf:
if conf: timestamp = tx_mined_status.timestamp
try: try:
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
except Exception: except Exception:
@ -189,7 +190,8 @@ class ElectrumGui:
if c == "n": return if c == "n": return
try: try:
tx = self.wallet.mktx([(TYPE_ADDRESS, self.str_recipient, amount)], password, self.config, fee) tx = self.wallet.mktx([TxOutput(TYPE_ADDRESS, self.str_recipient, amount)],
password, self.config, fee)
except Exception as e: except Exception as e:
print(str(e)) print(str(e))
return return

10
electrum/gui/text.py

@ -6,6 +6,7 @@ import getpass
import electrum import electrum
from electrum.util import format_satoshis, set_verbosity from electrum.util import format_satoshis, set_verbosity
from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS
from electrum.transaction import TxOutput
from .. import Wallet, WalletStorage from .. import Wallet, WalletStorage
_ = lambda x:x _ = lambda x:x
@ -109,9 +110,9 @@ class ElectrumGui:
b = 0 b = 0
self.history = [] self.history = []
for item in self.wallet.get_history(): for tx_hash, tx_mined_status, value, balance in self.wallet.get_history():
tx_hash, height, conf, timestamp, value, balance = item if tx_mined_status.conf:
if conf: timestamp = tx_mined_status.timestamp
try: try:
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
except Exception: except Exception:
@ -340,7 +341,8 @@ class ElectrumGui:
else: else:
password = None password = None
try: try:
tx = self.wallet.mktx([(TYPE_ADDRESS, self.str_recipient, amount)], password, self.config, fee) tx = self.wallet.mktx([TxOutput(TYPE_ADDRESS, self.str_recipient, amount)],
password, self.config, fee)
except Exception as e: except Exception as e:
self.show_message(str(e)) self.show_message(str(e))
return return

25
electrum/network.py

@ -89,6 +89,10 @@ def filter_version(servers):
return {k: v for k, v in servers.items() if is_recent(v.get('version'))} return {k: v for k, v in servers.items() if is_recent(v.get('version'))}
def filter_noonion(servers):
return {k: v for k, v in servers.items() if not k.endswith('.onion')}
def filter_protocol(hostmap, protocol='s'): def filter_protocol(hostmap, protocol='s'):
'''Filters the hostmap for those implementing protocol. '''Filters the hostmap for those implementing protocol.
The result is a list in serialized form.''' The result is a list in serialized form.'''
@ -409,6 +413,8 @@ class Network(util.DaemonThread):
continue continue
if host not in out: if host not in out:
out[host] = {protocol: port} out[host] = {protocol: port}
if self.config.get('noonion'):
out = filter_noonion(out)
return out return out
@with_interface_lock @with_interface_lock
@ -897,6 +903,7 @@ class Network(util.DaemonThread):
self.connection_down(interface.server) self.connection_down(interface.server)
return return
height = header.get('block_height') height = header.get('block_height')
#interface.print_error('got header', height, blockchain.hash_header(header))
if interface.request != height: if interface.request != height:
interface.print_error("unsolicited header",interface.request, height) interface.print_error("unsolicited header",interface.request, height)
self.connection_down(interface.server) self.connection_down(interface.server)
@ -911,6 +918,9 @@ class Network(util.DaemonThread):
next_height = height + 1 next_height = height + 1
interface.blockchain.catch_up = interface.server interface.blockchain.catch_up = interface.server
elif chain: elif chain:
# FIXME should await "initial chunk download".
# binary search will NOT do the correct thing if we don't yet
# have all headers up to the fork height
interface.print_error("binary search") interface.print_error("binary search")
interface.mode = 'binary' interface.mode = 'binary'
interface.blockchain = chain interface.blockchain = chain
@ -952,9 +962,9 @@ class Network(util.DaemonThread):
elif branch.parent().check_header(header): elif branch.parent().check_header(header):
interface.print_error('reorg', interface.bad, interface.tip) interface.print_error('reorg', interface.bad, interface.tip)
interface.blockchain = branch.parent() interface.blockchain = branch.parent()
next_height = None next_height = interface.bad
else: else:
interface.print_error('checkpoint conflicts with existing fork', branch.path()) interface.print_error('forkpoint conflicts with existing fork', branch.path())
branch.write(b'', 0) branch.write(b'', 0)
branch.save_header(interface.bad_header) branch.save_header(interface.bad_header)
interface.mode = 'catch_up' interface.mode = 'catch_up'
@ -970,10 +980,12 @@ class Network(util.DaemonThread):
with self.blockchains_lock: with self.blockchains_lock:
self.blockchains[interface.bad] = b self.blockchains[interface.bad] = b
interface.blockchain = b interface.blockchain = b
interface.print_error("new chain", b.checkpoint) interface.print_error("new chain", b.forkpoint)
interface.mode = 'catch_up' interface.mode = 'catch_up'
next_height = interface.bad + 1 maybe_next_height = interface.bad + 1
interface.blockchain.catch_up = interface.server if maybe_next_height <= interface.tip:
next_height = maybe_next_height
interface.blockchain.catch_up = interface.server
else: else:
assert bh == interface.good assert bh == interface.good
if interface.blockchain.catch_up is None and bh < interface.tip: if interface.blockchain.catch_up is None and bh < interface.tip:
@ -1086,6 +1098,7 @@ class Network(util.DaemonThread):
except InvalidHeader: except InvalidHeader:
self.connection_down(interface.server) self.connection_down(interface.server)
return return
#interface.print_error('notified of header', height, blockchain.hash_header(header))
if height < self.max_checkpoint(): if height < self.max_checkpoint():
self.connection_down(interface.server) self.connection_down(interface.server)
return return
@ -1130,7 +1143,7 @@ class Network(util.DaemonThread):
@with_interface_lock @with_interface_lock
def blockchain(self): def blockchain(self):
if self.interface and self.interface.blockchain is not None: if self.interface and self.interface.blockchain is not None:
self.blockchain_index = self.interface.blockchain.checkpoint self.blockchain_index = self.interface.blockchain.forkpoint
return self.blockchains[self.blockchain_index] return self.blockchains[self.blockchain_index]
@with_interface_lock @with_interface_lock

7
electrum/paymentrequest.py

@ -42,6 +42,7 @@ from .util import print_error, bh2u, bfh
from .util import export_meta, import_meta from .util import export_meta, import_meta
from .bitcoin import TYPE_ADDRESS from .bitcoin import TYPE_ADDRESS
from .transaction import TxOutput
REQUEST_HEADERS = {'Accept': 'application/bitcoin-paymentrequest', 'User-Agent': 'Electrum'} REQUEST_HEADERS = {'Accept': 'application/bitcoin-paymentrequest', 'User-Agent': 'Electrum'}
ACK_HEADERS = {'Content-Type':'application/bitcoin-payment','Accept':'application/bitcoin-paymentack','User-Agent':'Electrum'} ACK_HEADERS = {'Content-Type':'application/bitcoin-payment','Accept':'application/bitcoin-paymentack','User-Agent':'Electrum'}
@ -123,7 +124,7 @@ class PaymentRequest:
self.outputs = [] self.outputs = []
for o in self.details.outputs: for o in self.details.outputs:
addr = transaction.get_address_from_output_script(o.script)[1] addr = transaction.get_address_from_output_script(o.script)[1]
self.outputs.append((TYPE_ADDRESS, addr, o.amount)) self.outputs.append(TxOutput(TYPE_ADDRESS, addr, o.amount))
self.memo = self.details.memo self.memo = self.details.memo
self.payment_url = self.details.payment_url self.payment_url = self.details.payment_url
@ -225,8 +226,8 @@ class PaymentRequest:
def get_address(self): def get_address(self):
o = self.outputs[0] o = self.outputs[0]
assert o[0] == TYPE_ADDRESS assert o.type == TYPE_ADDRESS
return o[1] return o.address
def get_requestor(self): def get_requestor(self):
return self.requestor if self.requestor else self.get_address() return self.requestor if self.requestor else self.get_address()

1
electrum/plugin.py

@ -26,7 +26,6 @@ from collections import namedtuple
import traceback import traceback
import sys import sys
import os import os
import imp
import pkgutil import pkgutil
import time import time
import threading import threading

23
electrum/plugins/cosigner_pool/qt.py

@ -38,6 +38,7 @@ from electrum.wallet import Multisig_Wallet
from electrum.util import bh2u, bfh from electrum.util import bh2u, bfh
from electrum.gui.qt.transaction_dialog import show_transaction from electrum.gui.qt.transaction_dialog import show_transaction
from electrum.gui.qt.util import WaitingDialog
import sys import sys
import traceback import traceback
@ -170,20 +171,26 @@ class Plugin(BasePlugin):
return cosigner_xpub in xpub_set return cosigner_xpub in xpub_set
def do_send(self, tx): def do_send(self, tx):
def on_success(result):
window.show_message(_("Your transaction was sent to the cosigning pool.") + '\n' +
_("Open your cosigner wallet to retrieve it."))
def on_failure(exc_info):
e = exc_info[1]
try: traceback.print_exception(*exc_info)
except OSError: pass
window.show_error(_("Failed to send transaction to cosigning pool") + ':\n' + str(e))
for window, xpub, K, _hash in self.cosigner_list: for window, xpub, K, _hash in self.cosigner_list:
if not self.cosigner_can_sign(tx, xpub): if not self.cosigner_can_sign(tx, xpub):
continue continue
# construct message
raw_tx_bytes = bfh(str(tx)) raw_tx_bytes = bfh(str(tx))
public_key = ecc.ECPubkey(K) public_key = ecc.ECPubkey(K)
message = public_key.encrypt_message(raw_tx_bytes).decode('ascii') message = public_key.encrypt_message(raw_tx_bytes).decode('ascii')
try: # send message
server.put(_hash, message) task = lambda: server.put(_hash, message)
except Exception as e: msg = _('Sending transaction to cosigning pool...')
traceback.print_exc(file=sys.stdout) WaitingDialog(window, msg, task, on_success, on_failure)
window.show_error(_("Failed to send transaction to cosigning pool") + ':\n' + str(e))
return
window.show_message(_("Your transaction was sent to the cosigning pool.") + '\n' +
_("Open your cosigner wallet to retrieve it."))
def on_receive(self, keyhash, message): def on_receive(self, keyhash, message):
self.print_error("signal arrived for", keyhash) self.print_error("signal arrived for", keyhash)

6
electrum/plugins/digitalbitbox/digitalbitbox.py

@ -534,9 +534,9 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
self.give_error("No matching x_key for sign_transaction") # should never happen self.give_error("No matching x_key for sign_transaction") # should never happen
# Build pubkeyarray from outputs # Build pubkeyarray from outputs
for _type, address, amount in tx.outputs(): for o in tx.outputs():
assert _type == TYPE_ADDRESS assert o.type == TYPE_ADDRESS
info = tx.output_info.get(address) info = tx.output_info.get(o.address)
if info is not None: if info is not None:
index, xpubs, m = info index, xpubs, m = info
changePath = self.get_derivation() + "/%d/%d" % index changePath = self.get_derivation() + "/%d/%d" % index

16
electrum/plugins/hw_wallet/plugin.py

@ -26,7 +26,9 @@
from electrum.plugin import BasePlugin, hook from electrum.plugin import BasePlugin, hook
from electrum.i18n import _ from electrum.i18n import _
from electrum.bitcoin import is_address from electrum.bitcoin import is_address, TYPE_SCRIPT
from electrum.util import bfh
from electrum.transaction import opcodes, TxOutput
class HW_PluginBase(BasePlugin): class HW_PluginBase(BasePlugin):
@ -87,3 +89,15 @@ def is_any_tx_output_on_change_branch(tx):
if index[0] == 1: if index[0] == 1:
return True return True
return False return False
def trezor_validate_op_return_output_and_get_data(output: TxOutput) -> bytes:
if output.type != TYPE_SCRIPT:
raise Exception("Unexpected output type: {}".format(output.type))
script = bfh(output.address)
if not (script[0] == opcodes.OP_RETURN and
script[1] == len(script) - 2 and script[1] <= 75):
raise Exception(_("Only OP_RETURN scripts, with one constant push, are supported."))
if output.value != 0:
raise Exception(_("Amount for OP_RETURN output must be zero."))
return script[2:]

7
electrum/plugins/keepkey/keepkey.py

@ -15,7 +15,7 @@ from electrum.wallet import Standard_Wallet
from electrum.base_wizard import ScriptTypeNotSupported from electrum.base_wizard import ScriptTypeNotSupported
from ..hw_wallet import HW_PluginBase from ..hw_wallet import HW_PluginBase
from ..hw_wallet.plugin import is_any_tx_output_on_change_branch from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data
# TREZOR initialization methods # TREZOR initialization methods
@ -382,7 +382,7 @@ class KeepKeyPlugin(HW_PluginBase):
txoutputtype.amount = amount txoutputtype.amount = amount
if _type == TYPE_SCRIPT: if _type == TYPE_SCRIPT:
txoutputtype.script_type = self.types.PAYTOOPRETURN txoutputtype.script_type = self.types.PAYTOOPRETURN
txoutputtype.op_return_data = address[2:] txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(o)
elif _type == TYPE_ADDRESS: elif _type == TYPE_ADDRESS:
if is_segwit_address(address): if is_segwit_address(address):
txoutputtype.script_type = self.types.PAYTOWITNESS txoutputtype.script_type = self.types.PAYTOWITNESS
@ -401,7 +401,8 @@ class KeepKeyPlugin(HW_PluginBase):
has_change = False has_change = False
any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)
for _type, address, amount in tx.outputs(): for o in tx.outputs():
_type, address, amount = o.type, o.address, o.value
use_create_by_derivation = False use_create_by_derivation = False
info = tx.output_info.get(address) info = tx.output_info.get(address)

10
electrum/plugins/ledger/ledger.py

@ -394,9 +394,9 @@ class Ledger_KeyStore(Hardware_KeyStore):
self.give_error("Transaction with more than 2 outputs not supported") self.give_error("Transaction with more than 2 outputs not supported")
has_change = False has_change = False
any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)
for _type, address, amount in tx.outputs(): for o in tx.outputs():
assert _type == TYPE_ADDRESS assert o.type == TYPE_ADDRESS
info = tx.output_info.get(address) info = tx.output_info.get(o.address)
if (info is not None) and len(tx.outputs()) > 1 \ if (info is not None) and len(tx.outputs()) > 1 \
and not has_change: and not has_change:
index, xpubs, m = info index, xpubs, m = info
@ -407,9 +407,9 @@ class Ledger_KeyStore(Hardware_KeyStore):
changePath = self.get_derivation()[2:] + "/%d/%d"%index changePath = self.get_derivation()[2:] + "/%d/%d"%index
has_change = True has_change = True
else: else:
output = address output = o.address
else: else:
output = address output = o.address
self.handler.show_message(_("Confirm Transaction on your Ledger device...")) self.handler.show_message(_("Confirm Transaction on your Ledger device..."))
try: try:

8
electrum/plugins/safe_t/__init__.py

@ -0,0 +1,8 @@
from electrum.i18n import _
fullname = 'Safe-T mini Wallet'
description = _('Provides support for Safe-T mini hardware wallet')
requires = [('safetlib','github.com/archos-safe-t/python-safet')]
registers_keystore = ('hardware', 'safe_t', _("Safe-T mini wallet"))
available_for = ['qt', 'cmdline']

11
electrum/plugins/safe_t/client.py

@ -0,0 +1,11 @@
from safetlib.client import proto, BaseClient, ProtocolMixin
from .clientbase import SafeTClientBase
class SafeTClient(SafeTClientBase, ProtocolMixin, BaseClient):
def __init__(self, transport, handler, plugin):
BaseClient.__init__(self, transport=transport)
ProtocolMixin.__init__(self, transport=transport)
SafeTClientBase.__init__(self, handler, plugin, proto)
SafeTClientBase.wrap_methods(SafeTClient)

252
electrum/plugins/safe_t/clientbase.py

@ -0,0 +1,252 @@
import time
from struct import pack
from electrum.i18n import _
from electrum.util import PrintError, UserCancelled
from electrum.keystore import bip39_normalize_passphrase
from electrum.bitcoin import serialize_xpub
class GuiMixin(object):
# Requires: self.proto, self.device
# ref: https://github.com/trezor/trezor-common/blob/44dfb07cfaafffada4b2ce0d15ba1d90d17cf35e/protob/types.proto#L89
messages = {
3: _("Confirm the transaction output on your {} device"),
4: _("Confirm internal entropy on your {} device to begin"),
5: _("Write down the seed word shown on your {}"),
6: _("Confirm on your {} that you want to wipe it clean"),
7: _("Confirm on your {} device the message to sign"),
8: _("Confirm the total amount spent and the transaction fee on your "
"{} device"),
10: _("Confirm wallet address on your {} device"),
14: _("Choose on your {} device where to enter your passphrase"),
'default': _("Check your {} device to continue"),
}
def callback_Failure(self, msg):
# BaseClient's unfortunate call() implementation forces us to
# raise exceptions on failure in order to unwind the stack.
# However, making the user acknowledge they cancelled
# gets old very quickly, so we suppress those. The NotInitialized
# one is misnamed and indicates a passphrase request was cancelled.
if msg.code in (self.types.FailureType.PinCancelled,
self.types.FailureType.ActionCancelled,
self.types.FailureType.NotInitialized):
raise UserCancelled()
raise RuntimeError(msg.message)
def callback_ButtonRequest(self, msg):
message = self.msg
if not message:
message = self.messages.get(msg.code, self.messages['default'])
self.handler.show_message(message.format(self.device), self.cancel)
return self.proto.ButtonAck()
def callback_PinMatrixRequest(self, msg):
if msg.type == 2:
msg = _("Enter a new PIN for your {}:")
elif msg.type == 3:
msg = (_("Re-enter the new PIN for your {}.\n\n"
"NOTE: the positions of the numbers have changed!"))
else:
msg = _("Enter your current {} PIN:")
pin = self.handler.get_pin(msg.format(self.device))
if len(pin) > 9:
self.handler.show_error(_('The PIN cannot be longer than 9 characters.'))
pin = '' # to cancel below
if not pin:
return self.proto.Cancel()
return self.proto.PinMatrixAck(pin=pin)
def callback_PassphraseRequest(self, req):
if req and hasattr(req, 'on_device') and req.on_device is True:
return self.proto.PassphraseAck()
if self.creating_wallet:
msg = _("Enter a passphrase to generate this wallet. Each time "
"you use this wallet your {} will prompt you for the "
"passphrase. If you forget the passphrase you cannot "
"access the bitcoins in the wallet.").format(self.device)
else:
msg = _("Enter the passphrase to unlock this wallet:")
passphrase = self.handler.get_passphrase(msg, self.creating_wallet)
if passphrase is None:
return self.proto.Cancel()
passphrase = bip39_normalize_passphrase(passphrase)
ack = self.proto.PassphraseAck(passphrase=passphrase)
length = len(ack.passphrase)
if length > 50:
self.handler.show_error(_("Too long passphrase ({} > 50 chars).").format(length))
return self.proto.Cancel()
return ack
def callback_PassphraseStateRequest(self, msg):
return self.proto.PassphraseStateAck()
def callback_WordRequest(self, msg):
self.step += 1
msg = _("Step {}/24. Enter seed word as explained on "
"your {}:").format(self.step, self.device)
word = self.handler.get_word(msg)
# Unfortunately the device can't handle self.proto.Cancel()
return self.proto.WordAck(word=word)
class SafeTClientBase(GuiMixin, PrintError):
def __init__(self, handler, plugin, proto):
assert hasattr(self, 'tx_api') # ProtocolMixin already constructed?
self.proto = proto
self.device = plugin.device
self.handler = handler
self.tx_api = plugin
self.types = plugin.types
self.msg = None
self.creating_wallet = False
self.used()
def __str__(self):
return "%s/%s" % (self.label(), self.features.device_id)
def label(self):
'''The name given by the user to the device.'''
return self.features.label
def is_initialized(self):
'''True if initialized, False if wiped.'''
return self.features.initialized
def is_pairable(self):
return not self.features.bootloader_mode
def has_usable_connection_with_device(self):
try:
res = self.ping("electrum pinging device")
assert res == "electrum pinging device"
except BaseException:
return False
return True
def used(self):
self.last_operation = time.time()
def prevent_timeouts(self):
self.last_operation = float('inf')
def timeout(self, cutoff):
'''Time out the client if the last operation was before cutoff.'''
if self.last_operation < cutoff:
self.print_error("timed out")
self.clear_session()
@staticmethod
def expand_path(n):
'''Convert bip32 path to list of uint32 integers with prime flags
0/-1/1' -> [0, 0x80000001, 0x80000001]'''
# This code is similar to code in safetlib where it unfortunately
# is not declared as a staticmethod. Our n has an extra element.
PRIME_DERIVATION_FLAG = 0x80000000
path = []
for x in n.split('/')[1:]:
prime = 0
if x.endswith("'"):
x = x.replace('\'', '')
prime = PRIME_DERIVATION_FLAG
if x.startswith('-'):
prime = PRIME_DERIVATION_FLAG
path.append(abs(int(x)) | prime)
return path
def cancel(self):
'''Provided here as in keepkeylib but not safetlib.'''
self.transport.write(self.proto.Cancel())
def i4b(self, x):
return pack('>I', x)
def get_xpub(self, bip32_path, xtype):
address_n = self.expand_path(bip32_path)
creating = False
node = self.get_public_node(address_n, creating).node
return serialize_xpub(xtype, node.chain_code, node.public_key, node.depth, self.i4b(node.fingerprint), self.i4b(node.child_num))
def toggle_passphrase(self):
if self.features.passphrase_protection:
self.msg = _("Confirm on your {} device to disable passphrases")
else:
self.msg = _("Confirm on your {} device to enable passphrases")
enabled = not self.features.passphrase_protection
self.apply_settings(use_passphrase=enabled)
def change_label(self, label):
self.msg = _("Confirm the new label on your {} device")
self.apply_settings(label=label)
def change_homescreen(self, homescreen):
self.msg = _("Confirm on your {} device to change your home screen")
self.apply_settings(homescreen=homescreen)
def set_pin(self, remove):
if remove:
self.msg = _("Confirm on your {} device to disable PIN protection")
elif self.features.pin_protection:
self.msg = _("Confirm on your {} device to change your PIN")
else:
self.msg = _("Confirm on your {} device to set a PIN")
self.change_pin(remove)
def clear_session(self):
'''Clear the session to force pin (and passphrase if enabled)
re-entry. Does not leak exceptions.'''
self.print_error("clear session:", self)
self.prevent_timeouts()
try:
super(SafeTClientBase, self).clear_session()
except BaseException as e:
# If the device was removed it has the same effect...
self.print_error("clear_session: ignoring error", str(e))
def get_public_node(self, address_n, creating):
self.creating_wallet = creating
return super(SafeTClientBase, self).get_public_node(address_n)
def close(self):
'''Called when Our wallet was closed or the device removed.'''
self.print_error("closing client")
self.clear_session()
# Release the device
self.transport.close()
def firmware_version(self):
f = self.features
return (f.major_version, f.minor_version, f.patch_version)
def atleast_version(self, major, minor=0, patch=0):
return self.firmware_version() >= (major, minor, patch)
@staticmethod
def wrapper(func):
'''Wrap methods to clear any message box they opened.'''
def wrapped(self, *args, **kwargs):
try:
self.prevent_timeouts()
return func(self, *args, **kwargs)
finally:
self.used()
self.handler.finished()
self.creating_wallet = False
self.msg = None
return wrapped
@staticmethod
def wrap_methods(cls):
for method in ['apply_settings', 'change_pin',
'get_address', 'get_public_node',
'load_device_by_mnemonic', 'load_device_by_xprv',
'recovery_device', 'reset_device', 'sign_message',
'sign_tx', 'wipe_device']:
setattr(cls, method, cls.wrapper(getattr(cls, method)))

14
electrum/plugins/safe_t/cmdline.py

@ -0,0 +1,14 @@
from electrum.plugin import hook
from .safe_t import SafeTPlugin
from ..hw_wallet import CmdLineHandler
class Plugin(SafeTPlugin):
handler = CmdLineHandler()
@hook
def init_keystore(self, keystore):
if not isinstance(keystore, self.keystore_class):
return
keystore.handler = self.handler
def create_handler(self, window):
return self.handler

492
electrum/plugins/safe_t/qt.py

@ -0,0 +1,492 @@
from functools import partial
import threading
from PyQt5.Qt import Qt
from PyQt5.Qt import QGridLayout, QInputDialog, QPushButton
from PyQt5.Qt import QVBoxLayout, QLabel
from electrum.gui.qt.util import *
from electrum.i18n import _
from electrum.plugin import hook, DeviceMgr
from electrum.util import PrintError, UserCancelled, bh2u
from electrum.wallet import Wallet, Standard_Wallet
from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
from .safe_t import SafeTPlugin, TIM_NEW, TIM_RECOVER, TIM_MNEMONIC
PASSPHRASE_HELP_SHORT =_(
"Passphrases allow you to access new wallets, each "
"hidden behind a particular case-sensitive passphrase.")
PASSPHRASE_HELP = PASSPHRASE_HELP_SHORT + " " + _(
"You need to create a separate Electrum wallet for each passphrase "
"you use as they each generate different addresses. Changing "
"your passphrase does not lose other wallets, each is still "
"accessible behind its own passphrase.")
RECOMMEND_PIN = _(
"You should enable PIN protection. Your PIN is the only protection "
"for your bitcoins if your device is lost or stolen.")
PASSPHRASE_NOT_PIN = _(
"If you forget a passphrase you will be unable to access any "
"bitcoins in the wallet behind it. A passphrase is not a PIN. "
"Only change this if you are sure you understand it.")
class QtHandler(QtHandlerBase):
pin_signal = pyqtSignal(object)
def __init__(self, win, pin_matrix_widget_class, device):
super(QtHandler, self).__init__(win, device)
self.pin_signal.connect(self.pin_dialog)
self.pin_matrix_widget_class = pin_matrix_widget_class
def get_pin(self, msg):
self.done.clear()
self.pin_signal.emit(msg)
self.done.wait()
return self.response
def pin_dialog(self, msg):
# Needed e.g. when resetting a device
self.clear_dialog()
dialog = WindowModalDialog(self.top_level_window(), _("Enter PIN"))
matrix = self.pin_matrix_widget_class()
vbox = QVBoxLayout()
vbox.addWidget(QLabel(msg))
vbox.addWidget(matrix)
vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog)))
dialog.setLayout(vbox)
dialog.exec_()
self.response = str(matrix.get_value())
self.done.set()
class QtPlugin(QtPluginBase):
# Derived classes must provide the following class-static variables:
# icon_file
# pin_matrix_widget_class
def create_handler(self, window):
return QtHandler(window, self.pin_matrix_widget_class(), self.device)
@hook
def receive_menu(self, menu, addrs, wallet):
if len(addrs) != 1:
return
for keystore in wallet.get_keystores():
if type(keystore) == self.keystore_class:
def show_address():
keystore.thread.add(partial(self.show_address, wallet, addrs[0], keystore))
menu.addAction(_("Show on {}").format(self.device), show_address)
break
def show_settings_dialog(self, window, keystore):
device_id = self.choose_device(window, keystore)
if device_id:
SettingsDialog(window, self, keystore, device_id).exec_()
def request_safe_t_init_settings(self, wizard, method, device):
vbox = QVBoxLayout()
next_enabled = True
label = QLabel(_("Enter a label to name your device:"))
name = QLineEdit()
hl = QHBoxLayout()
hl.addWidget(label)
hl.addWidget(name)
hl.addStretch(1)
vbox.addLayout(hl)
def clean_text(widget):
text = widget.toPlainText().strip()
return ' '.join(text.split())
if method in [TIM_NEW, TIM_RECOVER]:
gb = QGroupBox()
hbox1 = QHBoxLayout()
gb.setLayout(hbox1)
vbox.addWidget(gb)
gb.setTitle(_("Select your seed length:"))
bg = QButtonGroup()
for i, count in enumerate([12, 18, 24]):
rb = QRadioButton(gb)
rb.setText(_("%d words") % count)
bg.addButton(rb)
bg.setId(rb, i)
hbox1.addWidget(rb)
rb.setChecked(True)
cb_pin = QCheckBox(_('Enable PIN protection'))
cb_pin.setChecked(True)
else:
text = QTextEdit()
text.setMaximumHeight(60)
if method == TIM_MNEMONIC:
msg = _("Enter your BIP39 mnemonic:")
else:
msg = _("Enter the master private key beginning with xprv:")
def set_enabled():
from electrum.keystore import is_xprv
wizard.next_button.setEnabled(is_xprv(clean_text(text)))
text.textChanged.connect(set_enabled)
next_enabled = False
vbox.addWidget(QLabel(msg))
vbox.addWidget(text)
pin = QLineEdit()
pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,9}')))
pin.setMaximumWidth(100)
hbox_pin = QHBoxLayout()
hbox_pin.addWidget(QLabel(_("Enter your PIN (digits 1-9):")))
hbox_pin.addWidget(pin)
hbox_pin.addStretch(1)
if method in [TIM_NEW, TIM_RECOVER]:
vbox.addWidget(WWLabel(RECOMMEND_PIN))
vbox.addWidget(cb_pin)
else:
vbox.addLayout(hbox_pin)
passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT)
passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN)
passphrase_warning.setStyleSheet("color: red")
cb_phrase = QCheckBox(_('Enable passphrases'))
cb_phrase.setChecked(False)
vbox.addWidget(passphrase_msg)
vbox.addWidget(passphrase_warning)
vbox.addWidget(cb_phrase)
wizard.exec_layout(vbox, next_enabled=next_enabled)
if method in [TIM_NEW, TIM_RECOVER]:
item = bg.checkedId()
pin = cb_pin.isChecked()
else:
item = ' '.join(str(clean_text(text)).split())
pin = str(pin.text())
return (item, name.text(), pin, cb_phrase.isChecked())
class Plugin(SafeTPlugin, QtPlugin):
icon_unpaired = ":icons/safe-t_unpaired.png"
icon_paired = ":icons/safe-t.png"
@classmethod
def pin_matrix_widget_class(self):
from safetlib.qt.pinmatrix import PinMatrixWidget
return PinMatrixWidget
class SettingsDialog(WindowModalDialog):
'''This dialog doesn't require a device be paired with a wallet.
We want users to be able to wipe a device even if they've forgotten
their PIN.'''
def __init__(self, window, plugin, keystore, device_id):
title = _("{} Settings").format(plugin.device)
super(SettingsDialog, self).__init__(window, title)
self.setMaximumWidth(540)
devmgr = plugin.device_manager()
config = devmgr.config
handler = keystore.handler
thread = keystore.thread
hs_rows, hs_cols = (64, 128)
def invoke_client(method, *args, **kw_args):
unpair_after = kw_args.pop('unpair_after', False)
def task():
client = devmgr.client_by_id(device_id)
if not client:
raise RuntimeError("Device not connected")
if method:
getattr(client, method)(*args, **kw_args)
if unpair_after:
devmgr.unpair_id(device_id)
return client.features
thread.add(task, on_success=update)
def update(features):
self.features = features
set_label_enabled()
if features.bootloader_hash:
bl_hash = bh2u(features.bootloader_hash)
bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]])
else:
bl_hash = "N/A"
noyes = [_("No"), _("Yes")]
endis = [_("Enable Passphrases"), _("Disable Passphrases")]
disen = [_("Disabled"), _("Enabled")]
setchange = [_("Set a PIN"), _("Change PIN")]
version = "%d.%d.%d" % (features.major_version,
features.minor_version,
features.patch_version)
device_label.setText(features.label)
pin_set_label.setText(noyes[features.pin_protection])
passphrases_label.setText(disen[features.passphrase_protection])
bl_hash_label.setText(bl_hash)
label_edit.setText(features.label)
device_id_label.setText(features.device_id)
initialized_label.setText(noyes[features.initialized])
version_label.setText(version)
clear_pin_button.setVisible(features.pin_protection)
clear_pin_warning.setVisible(features.pin_protection)
pin_button.setText(setchange[features.pin_protection])
pin_msg.setVisible(not features.pin_protection)
passphrase_button.setText(endis[features.passphrase_protection])
language_label.setText(features.language)
def set_label_enabled():
label_apply.setEnabled(label_edit.text() != self.features.label)
def rename():
invoke_client('change_label', label_edit.text())
def toggle_passphrase():
title = _("Confirm Toggle Passphrase Protection")
currently_enabled = self.features.passphrase_protection
if currently_enabled:
msg = _("After disabling passphrases, you can only pair this "
"Electrum wallet if it had an empty passphrase. "
"If its passphrase was not empty, you will need to "
"create a new wallet with the install wizard. You "
"can use this wallet again at any time by re-enabling "
"passphrases and entering its passphrase.")
else:
msg = _("Your current Electrum wallet can only be used with "
"an empty passphrase. You must create a separate "
"wallet with the install wizard for other passphrases "
"as each one generates a new set of addresses.")
msg += "\n\n" + _("Are you sure you want to proceed?")
if not self.question(msg, title=title):
return
invoke_client('toggle_passphrase', unpair_after=currently_enabled)
def change_homescreen():
dialog = QFileDialog(self, _("Choose Homescreen"))
filename, __ = dialog.getOpenFileName()
if not filename:
return # user cancelled
if filename.endswith('.toif'):
img = open(filename, 'rb').read()
if img[:8] != b'TOIf\x90\x00\x90\x00':
handler.show_error('File is not a TOIF file with size of 144x144')
return
else:
from PIL import Image # FIXME
im = Image.open(filename)
if im.size != (128, 64):
handler.show_error('Image must be 128 x 64 pixels')
return
im = im.convert('1')
pix = im.load()
img = bytearray(1024)
for j in range(64):
for i in range(128):
if pix[i, j]:
o = (i + j * 128)
img[o // 8] |= (1 << (7 - o % 8))
img = bytes(img)
invoke_client('change_homescreen', img)
def clear_homescreen():
invoke_client('change_homescreen', b'\x00')
def set_pin():
invoke_client('set_pin', remove=False)
def clear_pin():
invoke_client('set_pin', remove=True)
def wipe_device():
wallet = window.wallet
if wallet and sum(wallet.get_balance()):
title = _("Confirm Device Wipe")
msg = _("Are you SURE you want to wipe the device?\n"
"Your wallet still has bitcoins in it!")
if not self.question(msg, title=title,
icon=QMessageBox.Critical):
return
invoke_client('wipe_device', unpair_after=True)
def slider_moved():
mins = timeout_slider.sliderPosition()
timeout_minutes.setText(_("%2d minutes") % mins)
def slider_released():
config.set_session_timeout(timeout_slider.sliderPosition() * 60)
# Information tab
info_tab = QWidget()
info_layout = QVBoxLayout(info_tab)
info_glayout = QGridLayout()
info_glayout.setColumnStretch(2, 1)
device_label = QLabel()
pin_set_label = QLabel()
passphrases_label = QLabel()
version_label = QLabel()
device_id_label = QLabel()
bl_hash_label = QLabel()
bl_hash_label.setWordWrap(True)
language_label = QLabel()
initialized_label = QLabel()
rows = [
(_("Device Label"), device_label),
(_("PIN set"), pin_set_label),
(_("Passphrases"), passphrases_label),
(_("Firmware Version"), version_label),
(_("Device ID"), device_id_label),
(_("Bootloader Hash"), bl_hash_label),
(_("Language"), language_label),
(_("Initialized"), initialized_label),
]
for row_num, (label, widget) in enumerate(rows):
info_glayout.addWidget(QLabel(label), row_num, 0)
info_glayout.addWidget(widget, row_num, 1)
info_layout.addLayout(info_glayout)
# Settings tab
settings_tab = QWidget()
settings_layout = QVBoxLayout(settings_tab)
settings_glayout = QGridLayout()
# Settings tab - Label
label_msg = QLabel(_("Name this {}. If you have multiple devices "
"their labels help distinguish them.")
.format(plugin.device))
label_msg.setWordWrap(True)
label_label = QLabel(_("Device Label"))
label_edit = QLineEdit()
label_edit.setMinimumWidth(150)
label_edit.setMaxLength(plugin.MAX_LABEL_LEN)
label_apply = QPushButton(_("Apply"))
label_apply.clicked.connect(rename)
label_edit.textChanged.connect(set_label_enabled)
settings_glayout.addWidget(label_label, 0, 0)
settings_glayout.addWidget(label_edit, 0, 1, 1, 2)
settings_glayout.addWidget(label_apply, 0, 3)
settings_glayout.addWidget(label_msg, 1, 1, 1, -1)
# Settings tab - PIN
pin_label = QLabel(_("PIN Protection"))
pin_button = QPushButton()
pin_button.clicked.connect(set_pin)
settings_glayout.addWidget(pin_label, 2, 0)
settings_glayout.addWidget(pin_button, 2, 1)
pin_msg = QLabel(_("PIN protection is strongly recommended. "
"A PIN is your only protection against someone "
"stealing your bitcoins if they obtain physical "
"access to your {}.").format(plugin.device))
pin_msg.setWordWrap(True)
pin_msg.setStyleSheet("color: red")
settings_glayout.addWidget(pin_msg, 3, 1, 1, -1)
# Settings tab - Homescreen
homescreen_label = QLabel(_("Homescreen"))
homescreen_change_button = QPushButton(_("Change..."))
homescreen_clear_button = QPushButton(_("Reset"))
homescreen_change_button.clicked.connect(change_homescreen)
try:
import PIL
except ImportError:
homescreen_change_button.setDisabled(True)
homescreen_change_button.setToolTip(
_("Required package 'PIL' is not available - Please install it.")
)
homescreen_clear_button.clicked.connect(clear_homescreen)
homescreen_msg = QLabel(_("You can set the homescreen on your "
"device to personalize it. You must "
"choose a {} x {} monochrome black and "
"white image.").format(hs_rows, hs_cols))
homescreen_msg.setWordWrap(True)
settings_glayout.addWidget(homescreen_label, 4, 0)
settings_glayout.addWidget(homescreen_change_button, 4, 1)
settings_glayout.addWidget(homescreen_clear_button, 4, 2)
settings_glayout.addWidget(homescreen_msg, 5, 1, 1, -1)
# Settings tab - Session Timeout
timeout_label = QLabel(_("Session Timeout"))
timeout_minutes = QLabel()
timeout_slider = QSlider(Qt.Horizontal)
timeout_slider.setRange(1, 60)
timeout_slider.setSingleStep(1)
timeout_slider.setTickInterval(5)
timeout_slider.setTickPosition(QSlider.TicksBelow)
timeout_slider.setTracking(True)
timeout_msg = QLabel(
_("Clear the session after the specified period "
"of inactivity. Once a session has timed out, "
"your PIN and passphrase (if enabled) must be "
"re-entered to use the device."))
timeout_msg.setWordWrap(True)
timeout_slider.setSliderPosition(config.get_session_timeout() // 60)
slider_moved()
timeout_slider.valueChanged.connect(slider_moved)
timeout_slider.sliderReleased.connect(slider_released)
settings_glayout.addWidget(timeout_label, 6, 0)
settings_glayout.addWidget(timeout_slider, 6, 1, 1, 3)
settings_glayout.addWidget(timeout_minutes, 6, 4)
settings_glayout.addWidget(timeout_msg, 7, 1, 1, -1)
settings_layout.addLayout(settings_glayout)
settings_layout.addStretch(1)
# Advanced tab
advanced_tab = QWidget()
advanced_layout = QVBoxLayout(advanced_tab)
advanced_glayout = QGridLayout()
# Advanced tab - clear PIN
clear_pin_button = QPushButton(_("Disable PIN"))
clear_pin_button.clicked.connect(clear_pin)
clear_pin_warning = QLabel(
_("If you disable your PIN, anyone with physical access to your "
"{} device can spend your bitcoins.").format(plugin.device))
clear_pin_warning.setWordWrap(True)
clear_pin_warning.setStyleSheet("color: red")
advanced_glayout.addWidget(clear_pin_button, 0, 2)
advanced_glayout.addWidget(clear_pin_warning, 1, 0, 1, 5)
# Advanced tab - toggle passphrase protection
passphrase_button = QPushButton()
passphrase_button.clicked.connect(toggle_passphrase)
passphrase_msg = WWLabel(PASSPHRASE_HELP)
passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN)
passphrase_warning.setStyleSheet("color: red")
advanced_glayout.addWidget(passphrase_button, 3, 2)
advanced_glayout.addWidget(passphrase_msg, 4, 0, 1, 5)
advanced_glayout.addWidget(passphrase_warning, 5, 0, 1, 5)
# Advanced tab - wipe device
wipe_device_button = QPushButton(_("Wipe Device"))
wipe_device_button.clicked.connect(wipe_device)
wipe_device_msg = QLabel(
_("Wipe the device, removing all data from it. The firmware "
"is left unchanged."))
wipe_device_msg.setWordWrap(True)
wipe_device_warning = QLabel(
_("Only wipe a device if you have the recovery seed written down "
"and the device wallet(s) are empty, otherwise the bitcoins "
"will be lost forever."))
wipe_device_warning.setWordWrap(True)
wipe_device_warning.setStyleSheet("color: red")
advanced_glayout.addWidget(wipe_device_button, 6, 2)
advanced_glayout.addWidget(wipe_device_msg, 7, 0, 1, 5)
advanced_glayout.addWidget(wipe_device_warning, 8, 0, 1, 5)
advanced_layout.addLayout(advanced_glayout)
advanced_layout.addStretch(1)
tabs = QTabWidget(self)
tabs.addTab(info_tab, _("Information"))
tabs.addTab(settings_tab, _("Settings"))
tabs.addTab(advanced_tab, _("Advanced"))
dialog_vbox = QVBoxLayout(self)
dialog_vbox.addWidget(tabs)
dialog_vbox.addLayout(Buttons(CloseButton(self)))
# Update information
invoke_client(None)

509
electrum/plugins/safe_t/safe_t.py

@ -0,0 +1,509 @@
from binascii import hexlify, unhexlify
import traceback
import sys
from electrum.util import bfh, bh2u, versiontuple, UserCancelled
from electrum.bitcoin import (b58_address_to_hash160, xpub_from_pubkey, deserialize_xpub,
TYPE_ADDRESS, TYPE_SCRIPT, is_address)
from electrum import constants
from electrum.i18n import _
from electrum.plugin import BasePlugin, Device
from electrum.transaction import deserialize, Transaction
from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey, xtype_from_derivation
from electrum.base_wizard import ScriptTypeNotSupported
from ..hw_wallet import HW_PluginBase
from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data
# Safe-T mini initialization methods
TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4)
# script "generation"
SCRIPT_GEN_LEGACY, SCRIPT_GEN_P2SH_SEGWIT, SCRIPT_GEN_NATIVE_SEGWIT = range(0, 3)
class SafeTKeyStore(Hardware_KeyStore):
hw_type = 'safe_t'
device = 'Safe-T mini'
def get_derivation(self):
return self.derivation
def get_script_gen(self):
xtype = xtype_from_derivation(self.derivation)
if xtype in ('p2wpkh', 'p2wsh'):
return SCRIPT_GEN_NATIVE_SEGWIT
elif xtype in ('p2wpkh-p2sh', 'p2wsh-p2sh'):
return SCRIPT_GEN_P2SH_SEGWIT
else:
return SCRIPT_GEN_LEGACY
def get_client(self, force_pair=True):
return self.plugin.get_client(self, force_pair)
def decrypt_message(self, sequence, message, password):
raise RuntimeError(_('Encryption and decryption are not implemented by {}').format(self.device))
def sign_message(self, sequence, message, password):
client = self.get_client()
address_path = self.get_derivation() + "/%d/%d"%sequence
address_n = client.expand_path(address_path)
msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message)
return msg_sig.signature
def sign_transaction(self, tx, password):
if tx.is_complete():
return
# previous transactions used as inputs
prev_tx = {}
# path of the xpubs that are involved
xpub_path = {}
for txin in tx.inputs():
pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin)
tx_hash = txin['prevout_hash']
if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin):
raise Exception(_('Offline signing with {} is not supported for legacy inputs.').format(self.device))
prev_tx[tx_hash] = txin['prev_tx']
for x_pubkey in x_pubkeys:
if not is_xpubkey(x_pubkey):
continue
xpub, s = parse_xpubkey(x_pubkey)
if xpub == self.get_master_public_key():
xpub_path[xpub] = self.get_derivation()
self.plugin.sign_transaction(self, tx, prev_tx, xpub_path)
class SafeTPlugin(HW_PluginBase):
# Derived classes provide:
#
# class-static variables: client_class, firmware_URL, handler_class,
# libraries_available, libraries_URL, minimum_firmware,
# wallet_class, types
firmware_URL = 'https://safe-t.io'
libraries_URL = 'https://github.com/archos-safe-t/python-safet'
minimum_firmware = (1, 0, 5)
keystore_class = SafeTKeyStore
minimum_library = (0, 1, 0)
SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh')
MAX_LABEL_LEN = 32
def __init__(self, parent, config, name):
HW_PluginBase.__init__(self, parent, config, name)
try:
# Minimal test if python-safet is installed
import safetlib
try:
library_version = safetlib.__version__
except AttributeError:
# python-safet only introduced __version__ in 0.1.0
library_version = 'unknown'
if library_version == 'unknown' or \
versiontuple(library_version) < self.minimum_library:
self.libraries_available_message = (
_("Library version for '{}' is too old.").format(name)
+ '\nInstalled: {}, Needed: {}'
.format(library_version, self.minimum_library))
self.print_stderr(self.libraries_available_message)
raise ImportError()
self.libraries_available = True
except ImportError:
self.libraries_available = False
return
from . import client
from . import transport
import safetlib.messages
self.client_class = client.SafeTClient
self.types = safetlib.messages
self.DEVICE_IDS = ('Safe-T mini',)
self.transport_handler = transport.SafeTTransport()
self.device_manager().register_enumerate_func(self.enumerate)
def enumerate(self):
devices = self.transport_handler.enumerate_devices()
return [Device(d.get_path(), -1, d.get_path(), 'Safe-T mini', 0) for d in devices]
def create_client(self, device, handler):
try:
self.print_error("connecting to device at", device.path)
transport = self.transport_handler.get_transport(device.path)
except BaseException as e:
self.print_error("cannot connect at", device.path, str(e))
return None
if not transport:
self.print_error("cannot connect at", device.path)
return
self.print_error("connected to device at", device.path)
client = self.client_class(transport, handler, self)
# Try a ping for device sanity
try:
client.ping('t')
except BaseException as e:
self.print_error("ping failed", str(e))
return None
if not client.atleast_version(*self.minimum_firmware):
msg = (_('Outdated {} firmware for device labelled {}. Please '
'download the updated firmware from {}')
.format(self.device, client.label(), self.firmware_URL))
self.print_error(msg)
if handler:
handler.show_error(msg)
else:
raise Exception(msg)
return None
return client
def get_client(self, keystore, force_pair=True):
devmgr = self.device_manager()
handler = keystore.handler
with devmgr.hid_lock:
client = devmgr.client_for_keystore(self, handler, keystore, force_pair)
# returns the client for a given keystore. can use xpub
if client:
client.used()
return client
def get_coin_name(self):
return "Testnet" if constants.net.TESTNET else "Bitcoin"
def initialize_device(self, device_id, wizard, handler):
# Initialization method
msg = _("Choose how you want to initialize your {}.\n\n"
"The first two methods are secure as no secret information "
"is entered into your computer.\n\n"
"For the last two methods you input secrets on your keyboard "
"and upload them to your {}, and so you should "
"only do those on a computer you know to be trustworthy "
"and free of malware."
).format(self.device, self.device)
choices = [
# Must be short as QT doesn't word-wrap radio button text
(TIM_NEW, _("Let the device generate a completely new seed randomly")),
(TIM_RECOVER, _("Recover from a seed you have previously written down")),
(TIM_MNEMONIC, _("Upload a BIP39 mnemonic to generate the seed")),
(TIM_PRIVKEY, _("Upload a master private key"))
]
def f(method):
import threading
settings = self.request_safe_t_init_settings(wizard, method, self.device)
t = threading.Thread(target=self._initialize_device_safe, args=(settings, method, device_id, wizard, handler))
t.setDaemon(True)
t.start()
exit_code = wizard.loop.exec_()
if exit_code != 0:
# this method (initialize_device) was called with the expectation
# of leaving the device in an initialized state when finishing.
# signal that this is not the case:
raise UserCancelled()
wizard.choice_dialog(title=_('Initialize Device'), message=msg, choices=choices, run_next=f)
def _initialize_device_safe(self, settings, method, device_id, wizard, handler):
exit_code = 0
try:
self._initialize_device(settings, method, device_id, wizard, handler)
except UserCancelled:
exit_code = 1
except BaseException as e:
traceback.print_exc(file=sys.stderr)
handler.show_error(str(e))
exit_code = 1
finally:
wizard.loop.exit(exit_code)
def _initialize_device(self, settings, method, device_id, wizard, handler):
item, label, pin_protection, passphrase_protection = settings
if method == TIM_RECOVER:
handler.show_error(_(
"You will be asked to enter 24 words regardless of your "
"seed's actual length. If you enter a word incorrectly or "
"misspell it, you cannot change it or go back - you will need "
"to start again from the beginning.\n\nSo please enter "
"the words carefully!"),
blocking=True)
language = 'english'
devmgr = self.device_manager()
client = devmgr.client_by_id(device_id)
if method == TIM_NEW:
strength = 64 * (item + 2) # 128, 192 or 256
u2f_counter = 0
skip_backup = False
client.reset_device(True, strength, passphrase_protection,
pin_protection, label, language,
u2f_counter, skip_backup)
elif method == TIM_RECOVER:
word_count = 6 * (item + 2) # 12, 18 or 24
client.step = 0
client.recovery_device(word_count, passphrase_protection,
pin_protection, label, language)
elif method == TIM_MNEMONIC:
pin = pin_protection # It's the pin, not a boolean
client.load_device_by_mnemonic(str(item), pin,
passphrase_protection,
label, language)
else:
pin = pin_protection # It's the pin, not a boolean
client.load_device_by_xprv(item, pin, passphrase_protection,
label, language)
def _make_node_path(self, xpub, address_n):
_, depth, fingerprint, child_num, chain_code, key = deserialize_xpub(xpub)
node = self.types.HDNodeType(
depth=depth,
fingerprint=int.from_bytes(fingerprint, 'big'),
child_num=int.from_bytes(child_num, 'big'),
chain_code=chain_code,
public_key=key,
)
return self.types.HDNodePathType(node=node, address_n=address_n)
def setup_device(self, device_info, wizard, purpose):
devmgr = self.device_manager()
device_id = device_info.device.id_
client = devmgr.client_by_id(device_id)
if client is None:
raise Exception(_('Failed to create a client for this device.') + '\n' +
_('Make sure it is in the correct state.'))
# fixme: we should use: client.handler = wizard
client.handler = self.create_handler(wizard)
if not device_info.initialized:
self.initialize_device(device_id, wizard, client.handler)
client.get_xpub('m', 'standard')
client.used()
def get_xpub(self, device_id, derivation, xtype, wizard):
if xtype not in self.SUPPORTED_XTYPES:
raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device))
devmgr = self.device_manager()
client = devmgr.client_by_id(device_id)
client.handler = wizard
xpub = client.get_xpub(derivation, xtype)
client.used()
return xpub
def get_safet_input_script_type(self, script_gen, is_multisig):
if script_gen == SCRIPT_GEN_NATIVE_SEGWIT:
return self.types.InputScriptType.SPENDWITNESS
elif script_gen == SCRIPT_GEN_P2SH_SEGWIT:
return self.types.InputScriptType.SPENDP2SHWITNESS
else:
if is_multisig:
return self.types.InputScriptType.SPENDMULTISIG
else:
return self.types.InputScriptType.SPENDADDRESS
def sign_transaction(self, keystore, tx, prev_tx, xpub_path):
self.prev_tx = prev_tx
self.xpub_path = xpub_path
client = self.get_client(keystore)
inputs = self.tx_inputs(tx, True, keystore.get_script_gen())
outputs = self.tx_outputs(keystore.get_derivation(), tx, keystore.get_script_gen())
signatures = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime)[0]
signatures = [(bh2u(x) + '01') for x in signatures]
tx.update_signatures(signatures)
def show_address(self, wallet, address, keystore=None):
if keystore is None:
keystore = wallet.get_keystore()
if not self.show_address_helper(wallet, address, keystore):
return
client = self.get_client(keystore)
if not client.atleast_version(1, 0):
keystore.handler.show_error(_("Your device firmware is too old"))
return
change, index = wallet.get_address_index(address)
derivation = keystore.derivation
address_path = "%s/%d/%d"%(derivation, change, index)
address_n = client.expand_path(address_path)
xpubs = wallet.get_master_public_keys()
if len(xpubs) == 1:
script_gen = keystore.get_script_gen()
script_type = self.get_safet_input_script_type(script_gen, is_multisig=False)
client.get_address(self.get_coin_name(), address_n, True, script_type=script_type)
else:
def f(xpub):
return self._make_node_path(xpub, [change, index])
pubkeys = wallet.get_public_keys(address)
# sort xpubs using the order of pubkeys
sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs)))
pubkeys = list(map(f, sorted_xpubs))
multisig = self.types.MultisigRedeemScriptType(
pubkeys=pubkeys,
signatures=[b''] * wallet.n,
m=wallet.m,
)
script_gen = keystore.get_script_gen()
script_type = self.get_safet_input_script_type(script_gen, is_multisig=True)
client.get_address(self.get_coin_name(), address_n, True, multisig=multisig, script_type=script_type)
def tx_inputs(self, tx, for_sig=False, script_gen=SCRIPT_GEN_LEGACY):
inputs = []
for txin in tx.inputs():
txinputtype = self.types.TxInputType()
if txin['type'] == 'coinbase':
prev_hash = b"\x00"*32
prev_index = 0xffffffff # signed int -1
else:
if for_sig:
x_pubkeys = txin['x_pubkeys']
if len(x_pubkeys) == 1:
x_pubkey = x_pubkeys[0]
xpub, s = parse_xpubkey(x_pubkey)
xpub_n = self.client_class.expand_path(self.xpub_path[xpub])
txinputtype._extend_address_n(xpub_n + s)
txinputtype.script_type = self.get_safet_input_script_type(script_gen, is_multisig=False)
else:
def f(x_pubkey):
if is_xpubkey(x_pubkey):
xpub, s = parse_xpubkey(x_pubkey)
else:
xpub = xpub_from_pubkey(0, bfh(x_pubkey))
s = []
return self._make_node_path(xpub, s)
pubkeys = list(map(f, x_pubkeys))
multisig = self.types.MultisigRedeemScriptType(
pubkeys=pubkeys,
signatures=list(map(lambda x: bfh(x)[:-1] if x else b'', txin.get('signatures'))),
m=txin.get('num_sig'),
)
script_type = self.get_safet_input_script_type(script_gen, is_multisig=True)
txinputtype = self.types.TxInputType(
script_type=script_type,
multisig=multisig
)
# find which key is mine
for x_pubkey in x_pubkeys:
if is_xpubkey(x_pubkey):
xpub, s = parse_xpubkey(x_pubkey)
if xpub in self.xpub_path:
xpub_n = self.client_class.expand_path(self.xpub_path[xpub])
txinputtype._extend_address_n(xpub_n + s)
break
prev_hash = unhexlify(txin['prevout_hash'])
prev_index = txin['prevout_n']
if 'value' in txin:
txinputtype.amount = txin['value']
txinputtype.prev_hash = prev_hash
txinputtype.prev_index = prev_index
if txin.get('scriptSig') is not None:
script_sig = bfh(txin['scriptSig'])
txinputtype.script_sig = script_sig
txinputtype.sequence = txin.get('sequence', 0xffffffff - 1)
inputs.append(txinputtype)
return inputs
def tx_outputs(self, derivation, tx, script_gen=SCRIPT_GEN_LEGACY):
def create_output_by_derivation(info):
index, xpubs, m = info
if len(xpubs) == 1:
if script_gen == SCRIPT_GEN_NATIVE_SEGWIT:
script_type = self.types.OutputScriptType.PAYTOWITNESS
elif script_gen == SCRIPT_GEN_P2SH_SEGWIT:
script_type = self.types.OutputScriptType.PAYTOP2SHWITNESS
else:
script_type = self.types.OutputScriptType.PAYTOADDRESS
address_n = self.client_class.expand_path(derivation + "/%d/%d" % index)
txoutputtype = self.types.TxOutputType(
amount=amount,
script_type=script_type,
address_n=address_n,
)
else:
if script_gen == SCRIPT_GEN_NATIVE_SEGWIT:
script_type = self.types.OutputScriptType.PAYTOWITNESS
elif script_gen == SCRIPT_GEN_P2SH_SEGWIT:
script_type = self.types.OutputScriptType.PAYTOP2SHWITNESS
else:
script_type = self.types.OutputScriptType.PAYTOMULTISIG
address_n = self.client_class.expand_path("/%d/%d" % index)
pubkeys = [self._make_node_path(xpub, address_n) for xpub in xpubs]
multisig = self.types.MultisigRedeemScriptType(
pubkeys=pubkeys,
signatures=[b''] * len(pubkeys),
m=m)
txoutputtype = self.types.TxOutputType(
multisig=multisig,
amount=amount,
address_n=self.client_class.expand_path(derivation + "/%d/%d" % index),
script_type=script_type)
return txoutputtype
def create_output_by_address():
txoutputtype = self.types.TxOutputType()
txoutputtype.amount = amount
if _type == TYPE_SCRIPT:
txoutputtype.script_type = self.types.OutputScriptType.PAYTOOPRETURN
txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(o)
elif _type == TYPE_ADDRESS:
txoutputtype.script_type = self.types.OutputScriptType.PAYTOADDRESS
txoutputtype.address = address
return txoutputtype
outputs = []
has_change = False
any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)
for o in tx.outputs():
_type, address, amount = o.type, o.address, o.value
use_create_by_derivation = False
info = tx.output_info.get(address)
if info is not None and not has_change:
index, xpubs, m = info
on_change_branch = index[0] == 1
# prioritise hiding outputs on the 'change' branch from user
# because no more than one change address allowed
# note: ^ restriction can be removed once we require fw
# that has https://github.com/trezor/trezor-mcu/pull/306
if on_change_branch == any_output_on_change_branch:
use_create_by_derivation = True
has_change = True
if use_create_by_derivation:
txoutputtype = create_output_by_derivation(info)
else:
txoutputtype = create_output_by_address()
outputs.append(txoutputtype)
return outputs
def electrum_tx_to_txtype(self, tx):
t = self.types.TransactionType()
if tx is None:
# probably for segwit input and we don't need this prev txn
return t
d = deserialize(tx.raw)
t.version = d['version']
t.lock_time = d['lockTime']
inputs = self.tx_inputs(tx)
t._extend_inputs(inputs)
for vout in d['outputs']:
o = t._add_bin_outputs()
o.amount = vout['value']
o.script_pubkey = bfh(vout['scriptPubKey'])
return t
# This function is called from the TREZOR libraries (via tx_api)
def get_tx(self, tx_hash):
tx = self.prev_tx[tx_hash]
return self.electrum_tx_to_txtype(tx)

95
electrum/plugins/safe_t/transport.py

@ -0,0 +1,95 @@
from electrum.util import PrintError
class SafeTTransport(PrintError):
@staticmethod
def all_transports():
"""Reimplemented safetlib.transport.all_transports so that we can
enable/disable specific transports.
"""
try:
# only to detect safetlib version
from safetlib.transport import all_transports
except ImportError:
# old safetlib. compat for safetlib < 0.9.2
transports = []
#try:
# from safetlib.transport_bridge import BridgeTransport
# transports.append(BridgeTransport)
#except BaseException:
# pass
try:
from safetlib.transport_hid import HidTransport
transports.append(HidTransport)
except BaseException:
pass
try:
from safetlib.transport_udp import UdpTransport
transports.append(UdpTransport)
except BaseException:
pass
try:
from safetlib.transport_webusb import WebUsbTransport
transports.append(WebUsbTransport)
except BaseException:
pass
else:
# new safetlib.
transports = []
#try:
# from safetlib.transport.bridge import BridgeTransport
# transports.append(BridgeTransport)
#except BaseException:
# pass
try:
from safetlib.transport.hid import HidTransport
transports.append(HidTransport)
except BaseException:
pass
try:
from safetlib.transport.udp import UdpTransport
transports.append(UdpTransport)
except BaseException:
pass
try:
from safetlib.transport.webusb import WebUsbTransport
transports.append(WebUsbTransport)
except BaseException:
pass
return transports
return transports
def enumerate_devices(self):
"""Just like safetlib.transport.enumerate_devices,
but with exception catching, so that transports can fail separately.
"""
devices = []
for transport in self.all_transports():
try:
new_devices = transport.enumerate()
except BaseException as e:
self.print_error('enumerate failed for {}. error {}'
.format(transport.__name__, str(e)))
else:
devices.extend(new_devices)
return devices
def get_transport(self, path=None):
"""Reimplemented safetlib.transport.get_transport,
(1) for old safetlib
(2) to be able to disable specific transports
(3) to call our own enumerate_devices that catches exceptions
"""
if path is None:
try:
return self.enumerate_devices()[0]
except IndexError:
raise Exception("No Safe-T mini found") from None
def match_prefix(a, b):
return a.startswith(b) or b.startswith(a)
transports = [t for t in self.all_transports() if match_prefix(path, t.PATH_PREFIX)]
if transports:
return transports[0].find_by_path(path)
raise Exception("Unknown path prefix '%s'" % path)

7
electrum/plugins/trezor/trezor.py

@ -13,7 +13,7 @@ from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey, xtyp
from electrum.base_wizard import ScriptTypeNotSupported from electrum.base_wizard import ScriptTypeNotSupported
from ..hw_wallet import HW_PluginBase from ..hw_wallet import HW_PluginBase
from ..hw_wallet.plugin import is_any_tx_output_on_change_branch from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data
# TREZOR initialization methods # TREZOR initialization methods
@ -464,7 +464,7 @@ class TrezorPlugin(HW_PluginBase):
txoutputtype.amount = amount txoutputtype.amount = amount
if _type == TYPE_SCRIPT: if _type == TYPE_SCRIPT:
txoutputtype.script_type = self.types.OutputScriptType.PAYTOOPRETURN txoutputtype.script_type = self.types.OutputScriptType.PAYTOOPRETURN
txoutputtype.op_return_data = address[2:] txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(o)
elif _type == TYPE_ADDRESS: elif _type == TYPE_ADDRESS:
txoutputtype.script_type = self.types.OutputScriptType.PAYTOADDRESS txoutputtype.script_type = self.types.OutputScriptType.PAYTOADDRESS
txoutputtype.address = address txoutputtype.address = address
@ -474,7 +474,8 @@ class TrezorPlugin(HW_PluginBase):
has_change = False has_change = False
any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)
for _type, address, amount in tx.outputs(): for o in tx.outputs():
_type, address, amount = o.type, o.address, o.value
use_create_by_derivation = False use_create_by_derivation = False
info = tx.output_info.get(address) info = tx.output_info.get(address)

9
electrum/plugins/trustedcoin/trustedcoin.py

@ -33,6 +33,7 @@ from urllib.parse import quote
from electrum import bitcoin, ecc, constants, keystore, version from electrum import bitcoin, ecc, constants, keystore, version
from electrum.bitcoin import * from electrum.bitcoin import *
from electrum.transaction import TxOutput
from electrum.mnemonic import Mnemonic from electrum.mnemonic import Mnemonic
from electrum.wallet import Multisig_Wallet, Deterministic_Wallet from electrum.wallet import Multisig_Wallet, Deterministic_Wallet
from electrum.i18n import _ from electrum.i18n import _
@ -273,7 +274,7 @@ class Wallet_2fa(Multisig_Wallet):
fee = self.extra_fee(config) if not is_sweep else 0 fee = self.extra_fee(config) if not is_sweep else 0
if fee: if fee:
address = self.billing_info['billing_address'] address = self.billing_info['billing_address']
fee_output = (TYPE_ADDRESS, address, fee) fee_output = TxOutput(TYPE_ADDRESS, address, fee)
try: try:
tx = mk_tx(outputs + [fee_output]) tx = mk_tx(outputs + [fee_output])
except NotEnoughFunds: except NotEnoughFunds:
@ -395,9 +396,9 @@ class TrustedCoinPlugin(BasePlugin):
def get_tx_extra_fee(self, wallet, tx): def get_tx_extra_fee(self, wallet, tx):
if type(wallet) != Wallet_2fa: if type(wallet) != Wallet_2fa:
return return
for _type, addr, amount in tx.outputs(): for o in tx.outputs():
if _type == TYPE_ADDRESS and wallet.is_billing_address(addr): if o.type == TYPE_ADDRESS and wallet.is_billing_address(o.address):
return addr, amount return o.address, o.value
def finish_requesting(func): def finish_requesting(func):
def f(self, *args, **kwargs): def f(self, *args, **kwargs):

20
electrum/simple_config.py

@ -4,7 +4,7 @@ import time
import os import os
import stat import stat
from decimal import Decimal from decimal import Decimal
from typing import Union from typing import Union, Optional
from numbers import Real from numbers import Real
from copy import deepcopy from copy import deepcopy
@ -296,19 +296,27 @@ class SimpleConfig(PrintError):
return fee return fee
return get_fee_within_limits return get_fee_within_limits
@impose_hard_limits_on_fee def eta_to_fee(self, slider_pos) -> Optional[int]:
def eta_to_fee(self, slider_pos) -> Union[int, None]:
"""Returns fee in sat/kbyte.""" """Returns fee in sat/kbyte."""
slider_pos = max(slider_pos, 0) slider_pos = max(slider_pos, 0)
slider_pos = min(slider_pos, len(FEE_ETA_TARGETS)) slider_pos = min(slider_pos, len(FEE_ETA_TARGETS))
if slider_pos < len(FEE_ETA_TARGETS): if slider_pos < len(FEE_ETA_TARGETS):
target_blocks = FEE_ETA_TARGETS[slider_pos] num_blocks = FEE_ETA_TARGETS[slider_pos]
fee = self.fee_estimates.get(target_blocks) fee = self.eta_target_to_fee(num_blocks)
else: else:
fee = self.eta_target_to_fee(1)
return fee
@impose_hard_limits_on_fee
def eta_target_to_fee(self, num_blocks: int) -> Optional[int]:
"""Returns fee in sat/kbyte."""
if num_blocks == 1:
fee = self.fee_estimates.get(2) fee = self.fee_estimates.get(2)
if fee is not None: if fee is not None:
fee += fee/2 fee += fee / 2
fee = int(fee) fee = int(fee)
else:
fee = self.fee_estimates.get(num_blocks)
return fee return fee
def fee_to_depth(self, target_fee: Real) -> int: def fee_to_depth(self, target_fee: Real) -> int:

16
electrum/storage.py

@ -44,7 +44,7 @@ from .keystore import bip44_derivation
OLD_SEED_VERSION = 4 # electrum versions < 2.0 OLD_SEED_VERSION = 4 # electrum versions < 2.0
NEW_SEED_VERSION = 11 # electrum versions >= 2.0 NEW_SEED_VERSION = 11 # electrum versions >= 2.0
FINAL_SEED_VERSION = 17 # electrum >= 2.7 will set this to prevent FINAL_SEED_VERSION = 18 # electrum >= 2.7 will set this to prevent
# old versions from overwriting new format # old versions from overwriting new format
@ -319,7 +319,7 @@ class WalletStorage(JsonDB):
storage2.upgrade() storage2.upgrade()
storage2.write() storage2.write()
result = [storage1.path, storage2.path] result = [storage1.path, storage2.path]
elif wallet_type in ['bip44', 'trezor', 'keepkey', 'ledger', 'btchip', 'digitalbitbox']: elif wallet_type in ['bip44', 'trezor', 'keepkey', 'ledger', 'btchip', 'digitalbitbox', 'safe_t']:
mpk = storage.get('master_public_keys') mpk = storage.get('master_public_keys')
for k in d.keys(): for k in d.keys():
i = int(k) i = int(k)
@ -356,6 +356,7 @@ class WalletStorage(JsonDB):
self.convert_version_15() self.convert_version_15()
self.convert_version_16() self.convert_version_16()
self.convert_version_17() self.convert_version_17()
self.convert_version_18()
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
self.write() self.write()
@ -416,7 +417,7 @@ class WalletStorage(JsonDB):
self.put('wallet_type', 'standard') self.put('wallet_type', 'standard')
self.put('keystore', d) self.put('keystore', d)
elif wallet_type in ['trezor', 'keepkey', 'ledger', 'digitalbitbox']: elif wallet_type in ['trezor', 'keepkey', 'ledger', 'digitalbitbox', 'safe_t']:
xpub = xpubs["x/0'"] xpub = xpubs["x/0'"]
derivation = self.get('derivation', bip44_derivation(0)) derivation = self.get('derivation', bip44_derivation(0))
d = { d = {
@ -570,6 +571,15 @@ class WalletStorage(JsonDB):
self.put('seed_version', 17) self.put('seed_version', 17)
def convert_version_18(self):
# delete verified_tx3 as its structure changed
if not self._is_upgrade_method_needed(17, 17):
return
self.put('verified_tx3', None)
self.put('seed_version', 18)
def convert_imported(self): def convert_imported(self):
if not self._is_upgrade_method_needed(0, 13): if not self._is_upgrade_method_needed(0, 13):
return return

45
electrum/tests/test_wallet_vertical.py

@ -10,6 +10,7 @@ from electrum import SimpleConfig
from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT
from electrum.wallet import sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet from electrum.wallet import sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet
from electrum.util import bfh, bh2u from electrum.util import bfh, bh2u
from electrum.transaction import TxOutput
from electrum.plugins.trustedcoin import trustedcoin from electrum.plugins.trustedcoin import trustedcoin
@ -532,7 +533,7 @@ class TestWalletSending(TestCaseForTestnet):
wallet1.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet1.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# wallet1 -> wallet2 # wallet1 -> wallet2
outputs = [(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 250000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 250000)]
tx = wallet1.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx = wallet1.mktx(outputs=outputs, password=None, config=self.config, fee=5000)
self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_complete())
@ -552,7 +553,7 @@ class TestWalletSending(TestCaseForTestnet):
wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
# wallet2 -> wallet1 # wallet2 -> wallet1
outputs = [(bitcoin.TYPE_ADDRESS, wallet1.get_receiving_address(), 100000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet1.get_receiving_address(), 100000)]
tx = wallet2.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx = wallet2.mktx(outputs=outputs, password=None, config=self.config, fee=5000)
self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_complete())
@ -605,7 +606,7 @@ class TestWalletSending(TestCaseForTestnet):
wallet1a.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet1a.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# wallet1 -> wallet2 # wallet1 -> wallet2
outputs = [(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 370000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 370000)]
tx = wallet1a.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx = wallet1a.mktx(outputs=outputs, password=None, config=self.config, fee=5000)
tx = Transaction(tx.serialize()) # simulates moving partial txn between cosigners tx = Transaction(tx.serialize()) # simulates moving partial txn between cosigners
self.assertFalse(tx.is_complete()) self.assertFalse(tx.is_complete())
@ -628,7 +629,7 @@ class TestWalletSending(TestCaseForTestnet):
wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
# wallet2 -> wallet1 # wallet2 -> wallet1
outputs = [(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 100000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 100000)]
tx = wallet2.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx = wallet2.mktx(outputs=outputs, password=None, config=self.config, fee=5000)
self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_complete())
@ -696,7 +697,7 @@ class TestWalletSending(TestCaseForTestnet):
wallet1a.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet1a.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# wallet1 -> wallet2 # wallet1 -> wallet2
outputs = [(bitcoin.TYPE_ADDRESS, wallet2a.get_receiving_address(), 165000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet2a.get_receiving_address(), 165000)]
tx = wallet1a.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx = wallet1a.mktx(outputs=outputs, password=None, config=self.config, fee=5000)
txid = tx.txid() txid = tx.txid()
tx = Transaction(tx.serialize()) # simulates moving partial txn between cosigners tx = Transaction(tx.serialize()) # simulates moving partial txn between cosigners
@ -722,7 +723,7 @@ class TestWalletSending(TestCaseForTestnet):
wallet2a.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) wallet2a.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
# wallet2 -> wallet1 # wallet2 -> wallet1
outputs = [(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 100000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 100000)]
tx = wallet2a.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx = wallet2a.mktx(outputs=outputs, password=None, config=self.config, fee=5000)
txid = tx.txid() txid = tx.txid()
tx = Transaction(tx.serialize()) # simulates moving partial txn between cosigners tx = Transaction(tx.serialize()) # simulates moving partial txn between cosigners
@ -776,7 +777,7 @@ class TestWalletSending(TestCaseForTestnet):
wallet1a.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet1a.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# wallet1 -> wallet2 # wallet1 -> wallet2
outputs = [(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 1000000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 1000000)]
tx = wallet1a.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx = wallet1a.mktx(outputs=outputs, password=None, config=self.config, fee=5000)
self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_complete())
@ -796,7 +797,7 @@ class TestWalletSending(TestCaseForTestnet):
wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
# wallet2 -> wallet1 # wallet2 -> wallet1
outputs = [(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 300000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 300000)]
tx = wallet2.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx = wallet2.mktx(outputs=outputs, password=None, config=self.config, fee=5000)
self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_complete())
@ -832,7 +833,7 @@ class TestWalletSending(TestCaseForTestnet):
wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# create tx # create tx
outputs = [(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)]
coins = wallet.get_spendable_coins(domain=None, config=self.config) coins = wallet.get_spendable_coins(domain=None, config=self.config)
tx = wallet.make_unsigned_transaction(coins, outputs, config=self.config, fixed_fee=5000) tx = wallet.make_unsigned_transaction(coins, outputs, config=self.config, fixed_fee=5000)
tx.set_rbf(True) tx.set_rbf(True)
@ -918,7 +919,7 @@ class TestWalletSending(TestCaseForTestnet):
wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# create tx # create tx
outputs = [(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)]
coins = wallet.get_spendable_coins(domain=None, config=self.config) coins = wallet.get_spendable_coins(domain=None, config=self.config)
tx = wallet.make_unsigned_transaction(coins, outputs, config=self.config, fixed_fee=5000) tx = wallet.make_unsigned_transaction(coins, outputs, config=self.config, fixed_fee=5000)
tx.set_rbf(True) tx.set_rbf(True)
@ -1048,7 +1049,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# create unsigned tx # create unsigned tx
outputs = [(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)]
tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000)
tx.set_rbf(True) tx.set_rbf(True)
tx.locktime = 1325340 tx.locktime = 1325340
@ -1088,7 +1089,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# create unsigned tx # create unsigned tx
outputs = [(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)]
tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000)
tx.set_rbf(True) tx.set_rbf(True)
tx.locktime = 1325341 tx.locktime = 1325341
@ -1129,7 +1130,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# create unsigned tx # create unsigned tx
outputs = [(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)]
tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000)
tx.set_rbf(True) tx.set_rbf(True)
tx.locktime = 1325341 tx.locktime = 1325341
@ -1165,7 +1166,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# create unsigned tx # create unsigned tx
outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000)
tx.set_rbf(True) tx.set_rbf(True)
tx.locktime = 1325340 tx.locktime = 1325340
@ -1199,7 +1200,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# create unsigned tx # create unsigned tx
outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000)
tx.set_rbf(True) tx.set_rbf(True)
tx.locktime = 1325340 tx.locktime = 1325340
@ -1233,7 +1234,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# create unsigned tx # create unsigned tx
outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000)
tx.set_rbf(True) tx.set_rbf(True)
tx.locktime = 1325340 tx.locktime = 1325340
@ -1270,7 +1271,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# create unsigned tx # create unsigned tx
outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000)
tx.set_rbf(True) tx.set_rbf(True)
tx.locktime = 1325340 tx.locktime = 1325340
@ -1307,7 +1308,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# create unsigned tx # create unsigned tx
outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000)
tx.set_rbf(True) tx.set_rbf(True)
tx.locktime = 1325340 tx.locktime = 1325340
@ -1344,7 +1345,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# create unsigned tx # create unsigned tx
outputs = [(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000)
tx.set_rbf(True) tx.set_rbf(True)
tx.locktime = 1325340 tx.locktime = 1325340
@ -1393,7 +1394,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# create unsigned tx # create unsigned tx
outputs = [(bitcoin.TYPE_ADDRESS, '2MuCQQHJNnrXzQzuqfUCfAwAjPqpyEHbgue', 2500000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2MuCQQHJNnrXzQzuqfUCfAwAjPqpyEHbgue', 2500000)]
tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000)
tx.set_rbf(True) tx.set_rbf(True)
tx.locktime = 1325503 tx.locktime = 1325503
@ -1450,7 +1451,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# create unsigned tx # create unsigned tx
outputs = [(bitcoin.TYPE_ADDRESS, '2N8CtJRwxb2GCaiWWdSHLZHHLoZy53CCyxf', 2500000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N8CtJRwxb2GCaiWWdSHLZHHLoZy53CCyxf', 2500000)]
tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000)
tx.set_rbf(True) tx.set_rbf(True)
tx.locktime = 1325504 tx.locktime = 1325504
@ -1509,7 +1510,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
# create unsigned tx # create unsigned tx
outputs = [(bitcoin.TYPE_ADDRESS, '2MyoZVy8T1t94yLmyKu8DP1SmbWvnxbkwRA', 2500000)] outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2MyoZVy8T1t94yLmyKu8DP1SmbWvnxbkwRA', 2500000)]
tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000)
tx.set_rbf(True) tx.set_rbf(True)
tx.locktime = 1325505 tx.locktime = 1325505

28
electrum/transaction.py

@ -27,7 +27,7 @@
# Note: The deserialization code originally comes from ABE. # Note: The deserialization code originally comes from ABE.
from typing import Sequence, Union from typing import Sequence, Union, NamedTuple
from .util import print_error, profiler from .util import print_error, profiler
@ -59,6 +59,10 @@ class NotRecognizedRedeemScript(Exception):
pass pass
TxOutput = NamedTuple("TxOutput", [('type', int), ('address', str), ('value', Union[int, str])])
# ^ value is str when the output is set to max: '!'
class BCDataStream(object): class BCDataStream(object):
def __init__(self): def __init__(self):
self.input = None self.input = None
@ -672,7 +676,7 @@ class Transaction:
`signatures` is expected to be a list of sigs with signatures[i] `signatures` is expected to be a list of sigs with signatures[i]
intended for self._inputs[i]. intended for self._inputs[i].
This is used by the Trezor and KeepKey plugins. This is used by the Trezor, KeepKey an Safe-T plugins.
""" """
if self.is_complete(): if self.is_complete():
return return
@ -721,7 +725,7 @@ class Transaction:
return return
d = deserialize(self.raw, force_full_parse) d = deserialize(self.raw, force_full_parse)
self._inputs = d['inputs'] self._inputs = d['inputs']
self._outputs = [(x['type'], x['address'], x['value']) for x in d['outputs']] self._outputs = [TxOutput(x['type'], x['address'], x['value']) for x in d['outputs']]
self.locktime = d['lockTime'] self.locktime = d['lockTime']
self.version = d['version'] self.version = d['version']
self.is_partial_originally = d['partial'] self.is_partial_originally = d['partial']
@ -1180,17 +1184,17 @@ class Transaction:
def get_outputs(self): def get_outputs(self):
"""convert pubkeys to addresses""" """convert pubkeys to addresses"""
o = [] outputs = []
for type, x, v in self.outputs(): for o in self.outputs():
if type == TYPE_ADDRESS: if o.type == TYPE_ADDRESS:
addr = x addr = o.address
elif type == TYPE_PUBKEY: elif o.type == TYPE_PUBKEY:
# TODO do we really want this conversion? it's not really that address after all # TODO do we really want this conversion? it's not really that address after all
addr = bitcoin.public_key_to_p2pkh(bfh(x)) addr = bitcoin.public_key_to_p2pkh(bfh(o.address))
else: else:
addr = 'SCRIPT ' + x addr = 'SCRIPT ' + o.address
o.append((addr,v)) # consider using yield (addr, v) outputs.append((addr, o.value)) # consider using yield (addr, v)
return o return outputs
def get_output_addresses(self): def get_output_addresses(self):
return [addr for addr, val in self.get_outputs()] return [addr for addr, val in self.get_outputs()]

43
electrum/util.py

@ -23,6 +23,7 @@
import binascii import binascii
import os, sys, re, json import os, sys, re, json
from collections import defaultdict from collections import defaultdict
from typing import NamedTuple
from datetime import datetime from datetime import datetime
import decimal import decimal
from decimal import Decimal from decimal import Decimal
@ -31,6 +32,8 @@ import urllib
import threading import threading
import hmac import hmac
import stat import stat
import inspect
from locale import localeconv
from .i18n import _ from .i18n import _
@ -118,6 +121,8 @@ class UserCancelled(Exception):
pass pass
class Satoshis(object): class Satoshis(object):
__slots__ = ('value',)
def __new__(cls, value): def __new__(cls, value):
self = super(Satoshis, cls).__new__(cls) self = super(Satoshis, cls).__new__(cls)
self.value = value self.value = value
@ -130,6 +135,8 @@ class Satoshis(object):
return format_satoshis(self.value) + " BTC" return format_satoshis(self.value) + " BTC"
class Fiat(object): class Fiat(object):
__slots__ = ('value', 'ccy')
def __new__(cls, value, ccy): def __new__(cls, value, ccy):
self = super(Fiat, cls).__new__(cls) self = super(Fiat, cls).__new__(cls)
self.ccy = ccy self.ccy = ccy
@ -171,7 +178,7 @@ class PrintError(object):
def print_error(self, *msg): def print_error(self, *msg):
if self.verbosity_filter in verbosity or verbosity == '*': if self.verbosity_filter in verbosity or verbosity == '*':
print_stderr("[%s]" % self.diagnostic_name(), *msg) print_error("[%s]" % self.diagnostic_name(), *msg)
def print_stderr(self, *msg): def print_stderr(self, *msg):
print_stderr("[%s]" % self.diagnostic_name(), *msg) print_stderr("[%s]" % self.diagnostic_name(), *msg)
@ -309,14 +316,24 @@ def constant_time_compare(val1, val2):
# decorator that prints execution time # decorator that prints execution time
def profiler(func): def profiler(func):
def do_profile(func, args, kw_args): def get_func_name(args):
n = func.__name__ arg_names_from_sig = inspect.getfullargspec(func).args
# prepend class name if there is one (and if we can find it)
if len(arg_names_from_sig) > 0 and len(args) > 0 \
and arg_names_from_sig[0] in ('self', 'cls', 'klass'):
classname = args[0].__class__.__name__
else:
classname = ''
name = '{}.{}'.format(classname, func.__name__) if classname else func.__name__
return name
def do_profile(args, kw_args):
name = get_func_name(args)
t0 = time.time() t0 = time.time()
o = func(*args, **kw_args) o = func(*args, **kw_args)
t = time.time() - t0 t = time.time() - t0
print_error("[profiler]", n, "%.4f"%t) print_error("[profiler]", name, "%.4f"%t)
return o return o
return lambda *args, **kw_args: do_profile(func, args, kw_args) return lambda *args, **kw_args: do_profile(args, kw_args)
def android_ext_dir(): def android_ext_dir():
@ -465,8 +482,10 @@ def format_satoshis_plain(x, decimal_point = 8):
return "{:.8f}".format(Decimal(x) / scale_factor).rstrip('0').rstrip('.') return "{:.8f}".format(Decimal(x) / scale_factor).rstrip('0').rstrip('.')
DECIMAL_POINT = localeconv()['decimal_point']
def format_satoshis(x, num_zeros=0, decimal_point=8, precision=None, is_diff=False, whitespaces=False): def format_satoshis(x, num_zeros=0, decimal_point=8, precision=None, is_diff=False, whitespaces=False):
from locale import localeconv
if x is None: if x is None:
return 'unknown' return 'unknown'
if precision is None: if precision is None:
@ -476,7 +495,7 @@ def format_satoshis(x, num_zeros=0, decimal_point=8, precision=None, is_diff=Fal
decimal_format = '+' + decimal_format decimal_format = '+' + decimal_format
result = ("{:" + decimal_format + "f}").format(x / pow (10, decimal_point)).rstrip('0') result = ("{:" + decimal_format + "f}").format(x / pow (10, decimal_point)).rstrip('0')
integer_part, fract_part = result.split(".") integer_part, fract_part = result.split(".")
dp = localeconv()['decimal_point'] dp = DECIMAL_POINT
if len(fract_part) < num_zeros: if len(fract_part) < num_zeros:
fract_part += "0" * (num_zeros - len(fract_part)) fract_part += "0" * (num_zeros - len(fract_part))
result = integer_part + dp + fract_part result = integer_part + dp + fract_part
@ -903,3 +922,13 @@ def make_dir(path, allow_symlink=True):
raise Exception('Dangling link: ' + path) raise Exception('Dangling link: ' + path)
os.mkdir(path) os.mkdir(path)
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
TxMinedStatus = NamedTuple("TxMinedStatus", [("height", int),
("conf", int),
("timestamp", int),
("header_hash", str)])
VerifiedTxInfo = NamedTuple("VerifiedTxInfo", [("height", int),
("timestamp", int),
("txpos", int),
("header_hash", str)])

9
electrum/verifier.py

@ -23,9 +23,10 @@
from typing import Sequence, Optional from typing import Sequence, Optional
from .util import ThreadJob, bh2u from .util import ThreadJob, bh2u, VerifiedTxInfo
from .bitcoin import Hash, hash_decode, hash_encode from .bitcoin import Hash, hash_decode, hash_encode
from .transaction import Transaction from .transaction import Transaction
from .blockchain import hash_header
class MerkleVerificationFailure(Exception): pass class MerkleVerificationFailure(Exception): pass
@ -108,7 +109,9 @@ class SPV(ThreadJob):
self.requested_merkle.remove(tx_hash) self.requested_merkle.remove(tx_hash)
except KeyError: pass except KeyError: pass
self.print_error("verified %s" % tx_hash) self.print_error("verified %s" % tx_hash)
self.wallet.add_verified_tx(tx_hash, (tx_height, header.get('timestamp'), pos)) header_hash = hash_header(header)
vtx_info = VerifiedTxInfo(tx_height, header.get('timestamp'), pos, header_hash)
self.wallet.add_verified_tx(tx_hash, vtx_info)
if self.is_up_to_date() and self.wallet.is_up_to_date(): if self.is_up_to_date() and self.wallet.is_up_to_date():
self.wallet.save_verified_tx(write=True) self.wallet.save_verified_tx(write=True)
@ -142,7 +145,7 @@ class SPV(ThreadJob):
raise InnerNodeOfSpvProofIsValidTx() raise InnerNodeOfSpvProofIsValidTx()
def undo_verifications(self): def undo_verifications(self):
height = self.blockchain.get_checkpoint() height = self.blockchain.get_forkpoint()
tx_hashes = self.wallet.undo_verifications(self.blockchain, height) tx_hashes = self.wallet.undo_verifications(self.blockchain, height)
for tx_hash in tx_hashes: for tx_hash in tx_hashes:
self.print_error("redoing", tx_hash) self.print_error("redoing", tx_hash)

90
electrum/wallet.py

@ -43,7 +43,7 @@ from .i18n import _
from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler, from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler,
format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates, format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates,
TimeoutException, WalletFileException, BitcoinException, TimeoutException, WalletFileException, BitcoinException,
InvalidPassword) InvalidPassword, format_time)
from .bitcoin import * from .bitcoin import *
from .version import * from .version import *
@ -51,7 +51,7 @@ from .keystore import load_keystore, Hardware_KeyStore
from .storage import multisig_type, STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW from .storage import multisig_type, STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW
from . import transaction, bitcoin, coinchooser, paymentrequest, contacts from . import transaction, bitcoin, coinchooser, paymentrequest, contacts
from .transaction import Transaction from .transaction import Transaction, TxOutput
from .plugin import run_hook from .plugin import run_hook
from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL, from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL,
TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED) TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED)
@ -133,7 +133,7 @@ def sweep(privkeys, network, config, recipient, fee=None, imax=100):
inputs, keypairs = sweep_preparations(privkeys, network, imax) inputs, keypairs = sweep_preparations(privkeys, network, imax)
total = sum(i.get('value') for i in inputs) total = sum(i.get('value') for i in inputs)
if fee is None: if fee is None:
outputs = [(TYPE_ADDRESS, recipient, total)] outputs = [TxOutput(TYPE_ADDRESS, recipient, total)]
tx = Transaction.from_io(inputs, outputs) tx = Transaction.from_io(inputs, outputs)
fee = config.estimate_fee(tx.estimated_size()) fee = config.estimate_fee(tx.estimated_size())
if total - fee < 0: if total - fee < 0:
@ -141,7 +141,7 @@ def sweep(privkeys, network, config, recipient, fee=None, imax=100):
if total - fee < dust_threshold(network): if total - fee < dust_threshold(network):
raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d\nDust Threshold: %d'%(total, fee, dust_threshold(network))) raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d\nDust Threshold: %d'%(total, fee, dust_threshold(network)))
outputs = [(TYPE_ADDRESS, recipient, total - fee)] outputs = [TxOutput(TYPE_ADDRESS, recipient, total - fee)]
locktime = network.get_local_height() locktime = network.get_local_height()
tx = Transaction.from_io(inputs, outputs, locktime=locktime) tx = Transaction.from_io(inputs, outputs, locktime=locktime)
@ -318,7 +318,8 @@ class Abstract_Wallet(AddressSynchronizer):
if tx.is_complete(): if tx.is_complete():
if tx_hash in self.transactions.keys(): if tx_hash in self.transactions.keys():
label = self.get_label(tx_hash) label = self.get_label(tx_hash)
height, conf, timestamp = self.get_tx_height(tx_hash) tx_mined_status = self.get_tx_height(tx_hash)
height, conf = tx_mined_status.height, tx_mined_status.conf
if height > 0: if height > 0:
if conf: if conf:
status = _("{} confirmations").format(conf) status = _("{} confirmations").format(conf)
@ -368,8 +369,9 @@ class Abstract_Wallet(AddressSynchronizer):
def balance_at_timestamp(self, domain, target_timestamp): def balance_at_timestamp(self, domain, target_timestamp):
h = self.get_history(domain) h = self.get_history(domain)
for tx_hash, height, conf, timestamp, value, balance in h: balance = 0
if timestamp > target_timestamp: for tx_hash, tx_mined_status, value, balance in h:
if tx_mined_status.timestamp > target_timestamp:
return balance - value return balance - value
# return last balance # return last balance
return balance return balance
@ -384,21 +386,23 @@ class Abstract_Wallet(AddressSynchronizer):
fiat_income = Decimal(0) fiat_income = Decimal(0)
fiat_expenditures = Decimal(0) fiat_expenditures = Decimal(0)
h = self.get_history(domain) h = self.get_history(domain)
for tx_hash, height, conf, timestamp, value, balance in h: now = time.time()
if from_timestamp and (timestamp or time.time()) < from_timestamp: for tx_hash, tx_mined_status, value, balance in h:
timestamp = tx_mined_status.timestamp
if from_timestamp and (timestamp or now) < from_timestamp:
continue continue
if to_timestamp and (timestamp or time.time()) >= to_timestamp: if to_timestamp and (timestamp or now) >= to_timestamp:
continue continue
item = { item = {
'txid':tx_hash, 'txid': tx_hash,
'height':height, 'height': tx_mined_status.height,
'confirmations':conf, 'confirmations': tx_mined_status.conf,
'timestamp':timestamp, 'timestamp': timestamp,
'value': Satoshis(value), 'value': Satoshis(value),
'balance': Satoshis(balance) 'balance': Satoshis(balance),
'date': timestamp_to_datetime(timestamp),
'label': self.get_label(tx_hash),
} }
item['date'] = timestamp_to_datetime(timestamp)
item['label'] = self.get_label(tx_hash)
if show_addresses: if show_addresses:
tx = self.transactions.get(tx_hash) tx = self.transactions.get(tx_hash)
item['inputs'] = list(map(lambda x: dict((k, x[k]) for k in ('prevout_hash', 'prevout_n')), tx.inputs())) item['inputs'] = list(map(lambda x: dict((k, x[k]) for k in ('prevout_hash', 'prevout_n')), tx.inputs()))
@ -413,10 +417,9 @@ class Abstract_Wallet(AddressSynchronizer):
income += value income += value
# fiat computations # fiat computations
if fx and fx.is_enabled(): if fx and fx.is_enabled():
date = timestamp_to_datetime(timestamp)
fiat_value = self.get_fiat_value(tx_hash, fx.ccy) fiat_value = self.get_fiat_value(tx_hash, fx.ccy)
fiat_default = fiat_value is None fiat_default = fiat_value is None
fiat_value = fiat_value if fiat_value is not None else value / Decimal(COIN) * self.price_at_timestamp(tx_hash, fx.timestamp_rate) fiat_value = fiat_value if fiat_value is not None else value / Decimal(COIN) * self.price_at_timestamp(tx_hash, fx.timestamp_rate) #
item['fiat_value'] = Fiat(fiat_value, fx.ccy) item['fiat_value'] = Fiat(fiat_value, fx.ccy)
item['fiat_default'] = fiat_default item['fiat_default'] = fiat_default
if value < 0: if value < 0:
@ -483,9 +486,11 @@ class Abstract_Wallet(AddressSynchronizer):
return ', '.join(labels) return ', '.join(labels)
return '' return ''
def get_tx_status(self, tx_hash, height, conf, timestamp): def get_tx_status(self, tx_hash, tx_mined_status):
from .util import format_time
extra = [] extra = []
height = tx_mined_status.height
conf = tx_mined_status.conf
timestamp = tx_mined_status.timestamp
if conf == 0: if conf == 0:
tx = self.transactions.get(tx_hash) tx = self.transactions.get(tx_hash)
if not tx: if not tx:
@ -532,11 +537,10 @@ class Abstract_Wallet(AddressSynchronizer):
# check outputs # check outputs
i_max = None i_max = None
for i, o in enumerate(outputs): for i, o in enumerate(outputs):
_type, data, value = o if o.type == TYPE_ADDRESS:
if _type == TYPE_ADDRESS: if not is_address(o.address):
if not is_address(data): raise Exception("Invalid bitcoin address: {}".format(o.address))
raise Exception("Invalid bitcoin address: {}".format(data)) if o.value == '!':
if value == '!':
if i_max is not None: if i_max is not None:
raise Exception("More than one output set to spend max") raise Exception("More than one output set to spend max")
i_max = i i_max = i
@ -587,14 +591,13 @@ class Abstract_Wallet(AddressSynchronizer):
else: else:
# FIXME?? this might spend inputs with negative effective value... # FIXME?? this might spend inputs with negative effective value...
sendable = sum(map(lambda x:x['value'], inputs)) sendable = sum(map(lambda x:x['value'], inputs))
_type, data, value = outputs[i_max] outputs[i_max] = outputs[i_max]._replace(value=0)
outputs[i_max] = (_type, data, 0)
tx = Transaction.from_io(inputs, outputs[:]) tx = Transaction.from_io(inputs, outputs[:])
fee = fee_estimator(tx.estimated_size()) fee = fee_estimator(tx.estimated_size())
amount = sendable - tx.output_value() - fee amount = sendable - tx.output_value() - fee
if amount < 0: if amount < 0:
raise NotEnoughFunds() raise NotEnoughFunds()
outputs[i_max] = (_type, data, amount) outputs[i_max] = outputs[i_max]._replace(value=amount)
tx = Transaction.from_io(inputs, outputs[:]) tx = Transaction.from_io(inputs, outputs[:])
# Sort the inputs and outputs deterministically # Sort the inputs and outputs deterministically
@ -688,14 +691,13 @@ class Abstract_Wallet(AddressSynchronizer):
s = sorted(s, key=lambda x: x[2]) s = sorted(s, key=lambda x: x[2])
for o in s: for o in s:
i = outputs.index(o) i = outputs.index(o)
otype, address, value = o if o.value - delta >= self.dust_threshold():
if value - delta >= self.dust_threshold(): outputs[i] = o._replace(value=o.value-delta)
outputs[i] = otype, address, value - delta
delta = 0 delta = 0
break break
else: else:
del outputs[i] del outputs[i]
delta -= value delta -= o.value
if delta > 0: if delta > 0:
continue continue
if delta > 0: if delta > 0:
@ -708,8 +710,8 @@ class Abstract_Wallet(AddressSynchronizer):
def cpfp(self, tx, fee): def cpfp(self, tx, fee):
txid = tx.txid() txid = tx.txid()
for i, o in enumerate(tx.outputs()): for i, o in enumerate(tx.outputs()):
otype, address, value = o address, value = o.address, o.value
if otype == TYPE_ADDRESS and self.is_mine(address): if o.type == TYPE_ADDRESS and self.is_mine(address):
break break
else: else:
return return
@ -719,7 +721,7 @@ class Abstract_Wallet(AddressSynchronizer):
return return
self.add_input_info(item) self.add_input_info(item)
inputs = [item] inputs = [item]
outputs = [(TYPE_ADDRESS, address, value - fee)] outputs = [TxOutput(TYPE_ADDRESS, address, value - fee)]
locktime = self.get_local_height() locktime = self.get_local_height()
# note: no need to call tx.BIP_LI01_sort() here - single input/output # note: no need to call tx.BIP_LI01_sort() here - single input/output
return Transaction.from_io(inputs, outputs, locktime=locktime) return Transaction.from_io(inputs, outputs, locktime=locktime)
@ -839,8 +841,7 @@ class Abstract_Wallet(AddressSynchronizer):
txid, n = txo.split(':') txid, n = txo.split(':')
info = self.verified_tx.get(txid) info = self.verified_tx.get(txid)
if info: if info:
tx_height, timestamp, pos = info conf = local_height - info.height
conf = local_height - tx_height
else: else:
conf = 0 conf = 0
l.append((conf, v)) l.append((conf, v))
@ -1091,7 +1092,7 @@ class Abstract_Wallet(AddressSynchronizer):
def price_at_timestamp(self, txid, price_func): def price_at_timestamp(self, txid, price_func):
"""Returns fiat price of bitcoin at the time tx got confirmed.""" """Returns fiat price of bitcoin at the time tx got confirmed."""
height, conf, timestamp = self.get_tx_height(txid) timestamp = self.get_tx_height(txid).timestamp
return price_func(timestamp if timestamp else time.time()) return price_func(timestamp if timestamp else time.time())
def unrealized_gains(self, domain, price_func, ccy): def unrealized_gains(self, domain, price_func, ccy):
@ -1218,6 +1219,10 @@ class Imported_Wallet(Simple_Wallet):
def get_fingerprint(self): def get_fingerprint(self):
return '' return ''
def get_addresses(self):
# note: overridden so that the history can be cleared
return sorted(self.addresses.keys())
def get_receiving_addresses(self): def get_receiving_addresses(self):
return self.get_addresses() return self.get_addresses()
@ -1258,7 +1263,7 @@ class Imported_Wallet(Simple_Wallet):
self.verified_tx.pop(tx_hash, None) self.verified_tx.pop(tx_hash, None)
self.unverified_tx.pop(tx_hash, None) self.unverified_tx.pop(tx_hash, None)
self.transactions.pop(tx_hash, None) self.transactions.pop(tx_hash, None)
self.storage.put('verified_tx3', self.verified_tx) self.save_verified_tx()
self.save_transactions() self.save_transactions()
self.set_label(address, None) self.set_label(address, None)
@ -1350,7 +1355,8 @@ class Deterministic_Wallet(Abstract_Wallet):
return self.keystore.has_seed() return self.keystore.has_seed()
def get_addresses(self): def get_addresses(self):
# overloaded so that addresses are ordered based on derivation # note: overridden so that the history can be cleared.
# addresses are ordered based on derivation
out = [] out = []
out += self.get_receiving_addresses() out += self.get_receiving_addresses()
out += self.get_change_addresses() out += self.get_change_addresses()
@ -1661,4 +1667,4 @@ class Wallet(object):
return Multisig_Wallet return Multisig_Wallet
if wallet_type in wallet_constructors: if wallet_type in wallet_constructors:
return wallet_constructors[wallet_type] return wallet_constructors[wallet_type]
raise RuntimeError("Unknown wallet type: " + str(wallet_type)) raise WalletFileException("Unknown wallet type: " + str(wallet_type))

2
icons.qrc

@ -31,6 +31,8 @@
<file>icons/qrcode.png</file> <file>icons/qrcode.png</file>
<file>icons/qrcode_white.png</file> <file>icons/qrcode_white.png</file>
<file>icons/preferences.png</file> <file>icons/preferences.png</file>
<file>icons/safe-t_unpaired.png</file>
<file>icons/safe-t.png</file>
<file>icons/seed.png</file> <file>icons/seed.png</file>
<file>icons/status_connected.png</file> <file>icons/status_connected.png</file>
<file>icons/status_connected_proxy.png</file> <file>icons/status_connected_proxy.png</file>

BIN
icons/safe-t.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
icons/safe-t_unpaired.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

21
setup.py

@ -2,7 +2,7 @@
# python setup.py sdist --format=zip,gztar # python setup.py sdist --format=zip,gztar
from setuptools import setup from setuptools import setup, find_packages
import os import os
import sys import sys
import platform import platform
@ -57,28 +57,15 @@ setup(
'electrum.gui', 'electrum.gui',
'electrum.gui.qt', 'electrum.gui.qt',
'electrum.plugins', 'electrum.plugins',
'electrum.plugins.audio_modem', ] + [('electrum.plugins.'+pkg) for pkg in find_packages('electrum/plugins')],
'electrum.plugins.cosigner_pool',
'electrum.plugins.email_requests',
'electrum.plugins.greenaddress_instant',
'electrum.plugins.hw_wallet',
'electrum.plugins.keepkey',
'electrum.plugins.labels',
'electrum.plugins.ledger',
'electrum.plugins.revealer',
'electrum.plugins.trezor',
'electrum.plugins.digitalbitbox',
'electrum.plugins.trustedcoin',
'electrum.plugins.virtualkeyboard',
],
package_dir={ package_dir={
'electrum': 'electrum' 'electrum': 'electrum'
}, },
package_data={ package_data={
'': ['*.txt', '*.json', '*.ttf', '*.otf'], '': ['*.txt', '*.json', '*.ttf', '*.otf'],
'electrum': [ 'electrum': [
'electrum/wordlist/*.txt', 'wordlist/*.txt',
'electrum/locale/*/LC_MESSAGES/electrum.mo', 'locale/*/LC_MESSAGES/electrum.mo',
], ],
}, },
scripts=['electrum/electrum'], scripts=['electrum/electrum'],

Loading…
Cancel
Save