Files
purple-electrumwallet/electrum/wallet.py
T

1675 lines
62 KiB
Python
Raw Normal View History

2012-01-19 17:11:36 +01:00
# Electrum - lightweight Bitcoin client
2016-02-23 11:36:42 +01:00
# Copyright (C) 2015 Thomas Voegtlin
2012-01-19 17:11:36 +01:00
#
2016-02-23 11:36:42 +01:00
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
2012-01-19 17:11:36 +01:00
#
2016-02-23 11:36:42 +01:00
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
2012-01-19 17:11:36 +01:00
#
2016-02-23 11:36:42 +01:00
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
2012-01-19 17:11:36 +01:00
2017-01-22 21:25:24 +03:00
# Wallet classes:
# - Imported_Wallet: imported address, no keystore
# - Standard_Wallet: one keystore, P2PKH
# - Multisig_Wallet: several keystores, P2SH
2016-07-02 08:58:56 +02:00
2012-08-23 18:21:17 -07:00
import os
2018-07-18 11:18:57 +02:00
import sys
2012-08-23 18:21:17 -07:00
import random
import time
2014-08-20 12:47:53 +02:00
import json
2014-09-05 12:04:03 +02:00
import copy
2017-08-16 19:11:07 +02:00
import errno
2017-12-18 22:26:29 +01:00
import traceback
from functools import partial
2017-12-18 22:26:29 +01:00
from numbers import Number
2018-02-10 15:03:45 +01:00
from decimal import Decimal
2012-01-19 17:11:36 +01:00
2017-01-22 21:25:24 +03:00
from .i18n import _
2017-12-30 01:10:20 +01:00
from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler,
format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates,
2018-05-30 13:14:01 +02:00
TimeoutException, WalletFileException, BitcoinException,
2018-09-18 18:07:12 +02:00
InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
Fiat)
2017-01-22 21:25:24 +03:00
from .bitcoin import *
from .version import *
from .keystore import load_keystore, Hardware_KeyStore
from .storage import multisig_type, STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW
2018-07-11 17:38:47 +02:00
from . import transaction, bitcoin, coinchooser, paymentrequest, contacts
from .transaction import Transaction, TxOutput, TxOutputHwInfo
2018-07-11 17:38:47 +02:00
from .plugin import run_hook
2018-07-18 15:32:26 +02:00
from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL,
TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED)
2017-02-16 10:54:24 +01:00
from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
2017-03-15 12:13:20 +01:00
from .paymentrequest import InvoiceStore
from .contacts import Contacts
2015-07-22 09:06:03 +02:00
TX_STATUS = [
_('Unconfirmed'),
2018-02-27 15:13:44 +01:00
_('Unconfirmed parent'),
_('Not Verified'),
2018-02-27 12:16:50 +01:00
_('Local'),
]
def relayfee(network):
2018-02-21 03:22:26 +01:00
from .simple_config import FEERATE_DEFAULT_RELAY
MAX_RELAY_FEE = 50000
2018-02-21 03:22:26 +01:00
f = network.relay_fee if network and network.relay_fee else FEERATE_DEFAULT_RELAY
return min(f, MAX_RELAY_FEE)
def dust_threshold(network):
# Change <= dust threshold is added to the tx fee
return 182 * 3 * relayfee(network) / 1000
def append_utxos_to_inputs(inputs, network, pubkey, txin_type, imax):
if txin_type != 'p2pk':
address = bitcoin.pubkey_to_address(txin_type, pubkey)
scripthash = bitcoin.address_to_scripthash(address)
else:
script = bitcoin.public_key_to_p2pk_script(pubkey)
scripthash = bitcoin.script_to_scripthash(script)
address = '(pubkey)'
u = network.listunspent_for_scripthash(scripthash)
for item in u:
if len(inputs) >= imax:
break
item['address'] = address
item['type'] = txin_type
item['prevout_hash'] = item['tx_hash']
2018-05-31 20:18:39 +02:00
item['prevout_n'] = int(item['tx_pos'])
item['pubkeys'] = [pubkey]
item['x_pubkeys'] = [pubkey]
item['signatures'] = [None]
item['num_sig'] = 1
inputs.append(item)
2017-11-26 06:20:40 +01:00
def sweep_preparations(privkeys, network, imax=100):
def find_utxos_for_privkey(txin_type, privkey, compressed):
pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed)
append_utxos_to_inputs(inputs, network, pubkey, txin_type, imax)
keypairs[pubkey] = privkey, compressed
inputs = []
keypairs = {}
for sec in privkeys:
txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec)
find_utxos_for_privkey(txin_type, privkey, compressed)
# do other lookups to increase support coverage
if is_minikey(sec):
# minikeys don't have a compressed byte
# we lookup both compressed and uncompressed pubkeys
find_utxos_for_privkey(txin_type, privkey, not compressed)
elif txin_type == 'p2pkh':
# WIF serialization does not distinguish p2pkh and p2pk
# we also search for pay-to-pubkey outputs
find_utxos_for_privkey('p2pk', privkey, compressed)
if not inputs:
2018-04-07 17:10:30 +02:00
raise Exception(_('No inputs found. (Note that inputs need to be confirmed)'))
# FIXME actually inputs need not be confirmed now, see https://github.com/kyuupichan/electrumx/issues/365
2017-11-26 06:20:40 +01:00
return inputs, keypairs
def sweep(privkeys, network, config, recipient, fee=None, imax=100):
inputs, keypairs = sweep_preparations(privkeys, network, imax)
total = sum(i.get('value') for i in inputs)
if fee is None:
2018-07-31 18:24:37 +02:00
outputs = [TxOutput(TYPE_ADDRESS, recipient, total)]
tx = Transaction.from_io(inputs, outputs)
fee = config.estimate_fee(tx.estimated_size())
if total - fee < 0:
2018-04-07 17:10:30 +02:00
raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d'%(total, fee))
if total - fee < dust_threshold(network):
2018-04-07 17:10:30 +02:00
raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d\nDust Threshold: %d'%(total, fee, dust_threshold(network)))
2018-07-31 18:24:37 +02:00
outputs = [TxOutput(TYPE_ADDRESS, recipient, total - fee)]
locktime = network.get_local_height()
tx = Transaction.from_io(inputs, outputs, locktime=locktime)
2017-12-22 02:33:22 +01:00
tx.BIP_LI01_sort()
tx.set_rbf(True)
tx.sign(keypairs)
return tx
2018-07-18 11:18:57 +02:00
class CannotBumpFee(Exception): pass
2018-06-14 22:36:54 +02:00
2018-07-18 11:18:57 +02:00
class Abstract_Wallet(AddressSynchronizer):
"""
Wallet classes are created to handle various address generation methods.
Completion states (watching-only, single account, no seed, etc) are handled inside classes.
"""
2016-01-23 22:05:08 +09:00
max_change_outputs = 3
2018-07-18 11:18:57 +02:00
gap_limit_for_change = 6
2018-07-18 13:31:41 +02:00
verbosity_filter = 'w'
2016-01-23 22:05:08 +09:00
def __init__(self, storage):
2018-07-18 11:18:57 +02:00
AddressSynchronizer.__init__(self, storage)
2018-07-19 18:16:23 +02:00
2016-01-26 13:20:11 +01:00
self.electrum_version = ELECTRUM_VERSION
2012-01-19 17:11:36 +01:00
# saved fields
2016-05-20 10:38:48 +02:00
self.use_change = storage.get('use_change', True)
2016-01-15 14:54:26 +09:00
self.multiple_change = storage.get('multiple_change', False)
self.labels = storage.get('labels', {})
self.frozen_addresses = set(storage.get('frozen_addresses',[]))
self.fiat_value = storage.get('fiat_value', {})
2018-04-11 19:26:42 +02:00
self.receive_requests = storage.get('payment_requests', {})
2013-08-01 20:08:56 +02:00
self.calc_unused_change_addresses()
2014-08-20 18:54:37 +02:00
# save wallet type the first time
if self.storage.get('wallet_type') is None:
2015-12-20 15:39:57 +09:00
self.storage.put('wallet_type', self.wallet_type)
2014-08-20 18:54:37 +02:00
# invoices and contacts
self.invoices = InvoiceStore(self.storage)
self.contacts = Contacts(self.storage)
self.coin_price_cache = {}
2018-07-19 18:16:23 +02:00
def load_and_cleanup(self):
self.load_keystore()
self.load_addresses()
self.test_addresses_sanity()
super().load_and_cleanup()
2015-09-06 22:04:44 +09:00
def diagnostic_name(self):
return self.basename()
2016-01-02 09:43:56 +09:00
def __str__(self):
return self.basename()
2016-07-01 17:39:21 +02:00
def get_master_public_key(self):
2016-11-07 10:22:50 +01:00
return None
2016-07-01 17:39:21 +02:00
def basename(self):
return os.path.basename(self.storage.path)
2017-02-22 08:47:17 +01:00
def save_addresses(self):
self.storage.put('addresses', {'receiving':self.receiving_addresses, 'change':self.change_addresses})
2016-07-02 08:58:56 +02:00
def load_addresses(self):
2017-02-22 08:47:17 +01:00
d = self.storage.get('addresses', {})
2017-03-11 14:54:03 +01:00
if type(d) != dict: d={}
2017-02-22 08:47:17 +01:00
self.receiving_addresses = d.get('receiving', [])
self.change_addresses = d.get('change', [])
2015-12-31 11:36:33 +09:00
2018-03-09 03:18:53 +01:00
def test_addresses_sanity(self):
addrs = self.get_receiving_addresses()
if len(addrs) > 0:
if not bitcoin.is_address(addrs[0]):
raise WalletFileException('The addresses in this wallet are not bitcoin addresses.')
2018-03-09 03:18:53 +01:00
2014-04-30 11:40:53 +02:00
def synchronize(self):
2014-04-30 11:18:13 +02:00
pass
def calc_unused_change_addresses(self):
with self.lock:
if hasattr(self, '_unused_change_addresses'):
addrs = self._unused_change_addresses
else:
addrs = self.get_change_addresses()
self._unused_change_addresses = [addr for addr in addrs if
self.get_address_history_len(addr) == 0]
return list(self._unused_change_addresses)
2018-02-12 16:12:16 +01:00
def is_deterministic(self):
return self.keystore.is_deterministic()
2013-09-29 12:14:01 +02:00
def set_label(self, name, text = None):
changed = False
old_text = self.labels.get(name)
if text:
2017-02-19 22:37:45 +01:00
text = text.replace("\n", " ")
2013-09-29 12:14:01 +02:00
if old_text != text:
self.labels[name] = text
changed = True
else:
if old_text:
self.labels.pop(name)
changed = True
if changed:
2015-09-10 22:27:50 +09:00
run_hook('set_label', self, name, text)
2015-12-20 15:39:57 +09:00
self.storage.put('labels', self.labels)
2013-09-29 12:14:01 +02:00
return changed
def set_fiat_value(self, txid, ccy, text):
if txid not in self.transactions:
return
if not text:
d = self.fiat_value.get(ccy, {})
if d and txid in d:
d.pop(txid)
else:
return
else:
try:
Decimal(text)
except:
return
if ccy not in self.fiat_value:
self.fiat_value[ccy] = {}
self.fiat_value[ccy][txid] = text
self.storage.put('fiat_value', self.fiat_value)
def get_fiat_value(self, txid, ccy):
fiat_value = self.fiat_value.get(ccy, {}).get(txid)
try:
return Decimal(fiat_value)
except:
return
2018-07-19 18:16:23 +02:00
def is_mine(self, address):
try:
self.get_address_index(address)
except KeyError:
return False
return True
2012-01-19 17:11:36 +01:00
def is_change(self, address):
2016-07-02 08:58:56 +02:00
if not self.is_mine(address):
return False
2018-01-27 18:16:31 +01:00
return self.get_address_index(address)[0]
2012-01-19 17:11:36 +01:00
def get_address_index(self, address):
2018-01-30 00:59:12 +01:00
raise NotImplementedError()
2013-08-17 17:08:43 +02:00
def get_redeem_script(self, address):
return None
def export_private_key(self, address, password):
2013-11-12 11:14:16 +01:00
if self.is_watching_only():
return []
2016-12-20 16:53:01 +01:00
index = self.get_address_index(address)
pk, compressed = self.keystore.get_private_key(index, password)
txin_type = self.get_txin_type(address)
redeem_script = self.get_redeem_script(address)
serialized_privkey = bitcoin.serialize_privkey(pk, compressed, txin_type)
return serialized_privkey, redeem_script
2013-08-05 13:53:50 +02:00
def get_public_keys(self, address):
return [self.get_public_key(address)]
2014-04-30 11:18:13 +02:00
def is_found(self):
2014-06-24 16:12:43 +03:00
return self.history.values() != [[]] * len(self.history)
2013-02-27 09:04:22 +01:00
2016-06-08 11:06:51 +02:00
def get_tx_info(self, tx):
is_relevant, is_mine, v, fee = self.get_wallet_delta(tx)
exp_n = None
can_broadcast = False
2016-06-08 14:14:50 +02:00
can_bump = False
2016-06-09 18:09:45 +02:00
label = ''
2016-06-08 14:14:50 +02:00
height = conf = timestamp = None
tx_hash = tx.txid()
2016-06-08 11:06:51 +02:00
if tx.is_complete():
if tx_hash in self.transactions.keys():
label = self.get_label(tx_hash)
tx_mined_status = self.get_tx_height(tx_hash)
height, conf = tx_mined_status.height, tx_mined_status.conf
2016-06-08 11:06:51 +02:00
if height > 0:
if conf:
status = _("{} confirmations").format(conf)
2016-06-08 11:06:51 +02:00
else:
status = _('Not verified')
elif height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED):
2016-06-08 11:06:51 +02:00
status = _('Unconfirmed')
if fee is None:
fee = self.tx_fees.get(tx_hash)
2018-04-11 18:31:35 +02:00
if fee and self.network and self.network.config.has_fee_mempool():
2016-06-08 11:06:51 +02:00
size = tx.estimated_size()
2018-02-27 15:13:44 +01:00
fee_per_byte = fee / size
exp_n = self.network.config.fee_to_depth(fee_per_byte)
2016-06-08 14:14:50 +02:00
can_bump = is_mine and not tx.is_final()
else:
status = _('Local')
can_broadcast = self.network is not None
2016-06-08 11:06:51 +02:00
else:
status = _("Signed")
can_broadcast = self.network is not None
else:
s, r = tx.signature_count()
status = _("Unsigned") if s == 0 else _('Partially signed') + ' (%d/%d)'%(s,r)
if is_relevant:
if is_mine:
if fee is not None:
amount = v + fee
else:
amount = v
else:
amount = v
else:
amount = None
2016-06-08 14:14:50 +02:00
return tx_hash, status, label, can_broadcast, can_bump, amount, fee, height, conf, timestamp, exp_n
2016-06-08 11:06:51 +02:00
def get_spendable_coins(self, domain, config):
confirmed_only = config.get('confirmed_only', False)
return self.get_utxos(domain, excluded=self.frozen_addresses, mature=True, confirmed_only=confirmed_only)
2016-02-02 19:56:34 +01:00
def dummy_address(self):
2016-07-02 08:58:56 +02:00
return self.get_receiving_addresses()[0]
2016-02-02 19:56:34 +01:00
2013-04-12 14:29:11 +02:00
def get_frozen_balance(self):
2013-10-07 22:02:17 +02:00
return self.get_balance(self.frozen_addresses)
2014-06-24 16:12:43 +03:00
2018-02-14 10:42:09 +01:00
def balance_at_timestamp(self, domain, target_timestamp):
h = self.get_history(domain)
balance = 0
for tx_hash, tx_mined_status, value, balance in h:
if tx_mined_status.timestamp > target_timestamp:
2018-02-14 10:42:09 +01:00
return balance - value
# return last balance
return balance
@profiler
2018-02-15 14:59:05 +01:00
def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None, fx=None, show_addresses=False):
out = []
income = 0
expenditures = 0
2018-03-06 07:13:35 +01:00
capital_gains = Decimal(0)
fiat_income = Decimal(0)
fiat_expenditures = Decimal(0)
2018-02-15 14:59:05 +01:00
h = self.get_history(domain)
now = time.time()
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
if to_timestamp and (timestamp or now) >= to_timestamp:
continue
item = {
'txid': tx_hash,
'height': tx_mined_status.height,
'confirmations': tx_mined_status.conf,
'timestamp': timestamp,
2018-02-15 14:59:05 +01:00
'value': Satoshis(value),
'balance': Satoshis(balance),
'date': timestamp_to_datetime(timestamp),
'label': self.get_label(tx_hash),
}
if show_addresses:
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['outputs'] = list(map(lambda x:{'address':x[0], 'value':Satoshis(x[1])}, tx.get_outputs()))
# value may be None if wallet is not fully synchronized
if value is None:
continue
# fixme: use in and out values
if value < 0:
expenditures += -value
else:
income += value
# fiat computations
if fx and fx.is_enabled() and fx.get_history_config():
2018-02-14 10:42:09 +01:00
fiat_value = self.get_fiat_value(tx_hash, fx.ccy)
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) #
2018-02-15 14:59:05 +01:00
item['fiat_value'] = Fiat(fiat_value, fx.ccy)
item['fiat_default'] = fiat_default
if value < 0:
acquisition_price = - value / Decimal(COIN) * self.average_price(tx_hash, fx.timestamp_rate, fx.ccy)
liquidation_price = - fiat_value
2018-02-26 18:16:33 +01:00
item['acquisition_price'] = Fiat(acquisition_price, fx.ccy)
cg = liquidation_price - acquisition_price
2018-02-15 14:59:05 +01:00
item['capital_gain'] = Fiat(cg, fx.ccy)
capital_gains += cg
2018-02-27 15:26:39 +01:00
fiat_expenditures += -fiat_value
2018-02-14 10:42:09 +01:00
else:
fiat_income += fiat_value
out.append(item)
2018-02-21 11:52:40 +01:00
# add summary
if out:
2018-02-21 21:09:07 +01:00
b, v = out[0]['balance'].value, out[0]['value'].value
start_balance = None if b is None or v is None else b - v
2018-02-21 11:52:40 +01:00
end_balance = out[-1]['balance'].value
if from_timestamp is not None and to_timestamp is not None:
start_date = timestamp_to_datetime(from_timestamp)
end_date = timestamp_to_datetime(to_timestamp)
else:
2018-02-27 16:00:56 +01:00
start_date = None
end_date = None
2018-02-21 11:52:40 +01:00
summary = {
'start_date': start_date,
'end_date': end_date,
'start_balance': Satoshis(start_balance),
'end_balance': Satoshis(end_balance),
'income': Satoshis(income),
'expenditures': Satoshis(expenditures)
2018-02-21 11:52:40 +01:00
}
if fx and fx.is_enabled() and fx.get_history_config():
2018-02-21 11:52:40 +01:00
unrealized = self.unrealized_gains(domain, fx.timestamp_rate, fx.ccy)
summary['capital_gains'] = Fiat(capital_gains, fx.ccy)
summary['fiat_income'] = Fiat(fiat_income, fx.ccy)
summary['fiat_expenditures'] = Fiat(fiat_expenditures, fx.ccy)
2018-02-21 11:52:40 +01:00
summary['unrealized_gains'] = Fiat(unrealized, fx.ccy)
2018-02-27 16:00:56 +01:00
summary['start_fiat_balance'] = Fiat(fx.historical_value(start_balance, start_date), fx.ccy)
summary['end_fiat_balance'] = Fiat(fx.historical_value(end_balance, end_date), fx.ccy)
2018-04-19 14:05:19 +02:00
summary['start_fiat_value'] = Fiat(fx.historical_value(COIN, start_date), fx.ccy)
summary['end_fiat_value'] = Fiat(fx.historical_value(COIN, end_date), fx.ccy)
2018-02-15 14:59:05 +01:00
else:
2018-02-21 11:52:40 +01:00
summary = {}
return {
'transactions': out,
'summary': summary
2018-02-15 14:59:05 +01:00
}
2012-11-05 11:08:16 +01:00
def get_label(self, tx_hash):
2015-12-15 12:52:30 +01:00
label = self.labels.get(tx_hash, '')
2016-01-04 15:40:57 +01:00
if label is '':
label = self.get_default_label(tx_hash)
2015-12-15 12:52:30 +01:00
return label
2016-01-04 15:40:57 +01:00
def get_default_label(self, tx_hash):
if self.txi.get(tx_hash) == {}:
d = self.txo.get(tx_hash, {})
labels = []
for addr in d.keys():
label = self.labels.get(addr)
if label:
labels.append(label)
return ', '.join(labels)
return ''
def get_tx_status(self, tx_hash, tx_mined_status):
2018-02-27 15:13:44 +01:00
extra = []
height = tx_mined_status.height
conf = tx_mined_status.conf
timestamp = tx_mined_status.timestamp
if conf == 0:
tx = self.transactions.get(tx_hash)
2016-11-09 13:23:10 +01:00
if not tx:
2018-02-05 20:13:05 +01:00
return 2, 'unknown'
is_final = tx and tx.is_final()
2018-02-27 15:13:44 +01:00
if not is_final:
extra.append('rbf')
fee = self.get_wallet_delta(tx)[3]
if fee is None:
fee = self.tx_fees.get(tx_hash)
if fee is not None:
2017-11-22 12:09:56 +01:00
size = tx.estimated_size()
2018-02-27 15:13:44 +01:00
fee_per_byte = fee / size
extra.append(format_fee_satoshis(fee_per_byte) + ' sat/b')
2018-02-27 15:13:44 +01:00
if fee is not None and height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED) \
and self.network and self.network.config.has_fee_mempool():
exp_n = self.network.config.fee_to_depth(fee_per_byte)
if exp_n:
extra.append('%.2f MB'%(exp_n/1000000))
if height == TX_HEIGHT_LOCAL:
2018-02-27 15:13:44 +01:00
status = 3
elif height == TX_HEIGHT_UNCONF_PARENT:
status = 1
elif height == TX_HEIGHT_UNCONFIRMED:
2018-02-27 15:13:44 +01:00
status = 0
else:
2018-02-27 15:13:44 +01:00
status = 2
else:
2018-02-27 15:13:44 +01:00
status = 3 + min(conf, 6)
time_str = format_time(timestamp) if timestamp else _("unknown")
2018-02-27 15:13:44 +01:00
status_str = TX_STATUS[status] if status < 4 else time_str
if extra:
status_str += ' [%s]'%(', '.join(extra))
return status, status_str
def relayfee(self):
return relayfee(self.network)
2016-10-17 13:47:23 +02:00
def dust_threshold(self):
return dust_threshold(self.network)
2016-10-17 13:47:23 +02:00
2017-12-11 17:37:10 +01:00
def make_unsigned_transaction(self, inputs, outputs, config, fixed_fee=None,
change_addr=None, is_sweep=False):
2014-09-07 18:45:06 +02:00
# check outputs
2016-12-31 16:29:18 +01:00
i_max = None
for i, o in enumerate(outputs):
2018-07-31 18:24:37 +02:00
if o.type == TYPE_ADDRESS:
if not is_address(o.address):
raise Exception("Invalid bitcoin address: {}".format(o.address))
if o.value == '!':
2016-12-31 16:29:18 +01:00
if i_max is not None:
2018-04-07 17:10:30 +02:00
raise Exception("More than one output set to spend max")
2016-12-31 16:29:18 +01:00
i_max = i
2014-09-07 18:45:06 +02:00
2017-01-04 18:08:58 +01:00
# Avoid index-out-of-range with inputs[0] below
2016-12-31 16:29:18 +01:00
if not inputs:
2015-12-20 12:19:44 +09:00
raise NotEnoughFunds()
if fixed_fee is None and config.fee_per_kb() is None:
2017-12-30 01:10:20 +01:00
raise NoDynamicFeeEstimates()
2018-06-14 15:11:20 +02:00
for item in inputs:
self.add_input_info(item)
2014-09-07 18:45:06 +02:00
# change address
# if we leave it empty, coin_chooser will set it
change_addrs = []
2015-11-28 14:47:08 +09:00
if change_addr:
change_addrs = [change_addr]
elif self.use_change:
# Recalc and get unused change addresses
addrs = self.calc_unused_change_addresses()
# New change addresses are created only after a few
# confirmations.
if addrs:
# if there are any unused, select all
change_addrs = addrs
2014-09-07 18:45:06 +02:00
else:
# if there are none, take one randomly from the last few
addrs = self.get_change_addresses()[-self.gap_limit_for_change:]
change_addrs = [random.choice(addrs)] if addrs else []
2015-11-28 14:47:08 +09:00
2015-11-29 12:41:54 +09:00
# Fee estimator
if fixed_fee is None:
2017-11-29 19:04:18 +01:00
fee_estimator = config.estimate_fee
2017-12-18 22:26:29 +01:00
elif isinstance(fixed_fee, Number):
fee_estimator = lambda size: fixed_fee
2017-12-18 22:26:29 +01:00
elif callable(fixed_fee):
fee_estimator = fixed_fee
else:
2018-04-07 17:10:30 +02:00
raise Exception('Invalid argument fixed_fee: %s' % fixed_fee)
2016-12-31 16:29:18 +01:00
if i_max is None:
# Let the coin chooser select the coins to spend
max_change = self.max_change_outputs if self.multiple_change else 1
coin_chooser = coinchooser.get_coin_chooser(config)
tx = coin_chooser.make_tx(inputs, outputs, change_addrs[:max_change],
fee_estimator, self.dust_threshold())
else:
# FIXME?? this might spend inputs with negative effective value...
2016-12-31 16:29:18 +01:00
sendable = sum(map(lambda x:x['value'], inputs))
2018-07-31 18:24:37 +02:00
outputs[i_max] = outputs[i_max]._replace(value=0)
2016-12-31 16:29:18 +01:00
tx = Transaction.from_io(inputs, outputs[:])
fee = fee_estimator(tx.estimated_size())
amount = sendable - tx.output_value() - fee
if amount < 0:
raise NotEnoughFunds()
2018-07-31 18:24:37 +02:00
outputs[i_max] = outputs[i_max]._replace(value=amount)
2016-12-31 16:29:18 +01:00
tx = Transaction.from_io(inputs, outputs[:])
2014-09-07 18:45:06 +02:00
2015-06-06 20:45:07 +09:00
# Sort the inputs and outputs deterministically
tx.BIP_LI01_sort()
2017-04-19 11:55:31 +02:00
# Timelock tx to current height.
2017-10-11 11:45:52 +02:00
tx.locktime = self.get_local_height()
2015-09-09 16:24:11 +09:00
run_hook('make_unsigned_transaction', self, tx)
2014-09-07 18:45:06 +02:00
return tx
2013-09-04 10:33:14 +02:00
2015-08-04 07:15:54 +02:00
def mktx(self, outputs, password, config, fee=None, change_addr=None, domain=None):
coins = self.get_spendable_coins(domain, config)
2015-08-04 07:15:54 +02:00
tx = self.make_unsigned_transaction(coins, outputs, config, fee, change_addr)
self.sign_transaction(tx, password)
2013-09-04 10:33:14 +02:00
return tx
def is_frozen(self, addr):
return addr in self.frozen_addresses
def set_frozen_state(self, addrs, freeze):
'''Set frozen state of the addresses to FREEZE, True or False'''
if all(self.is_mine(addr) for addr in addrs):
if freeze:
self.frozen_addresses |= set(addrs)
else:
self.frozen_addresses -= set(addrs)
2015-12-20 15:39:57 +09:00
self.storage.put('frozen_addresses', list(self.frozen_addresses))
return True
return False
2012-03-30 14:15:05 +02:00
def wait_until_synchronized(self, callback=None):
2015-10-28 10:36:44 +01:00
def wait_for_wallet():
self.set_up_to_date(False)
while not self.is_up_to_date():
if callback:
msg = "{}\n{} {}".format(
_("Please wait..."),
_("Addresses generated:"),
len(self.get_addresses()))
2015-12-31 11:36:33 +09:00
callback(msg)
2015-10-28 10:36:44 +01:00
time.sleep(0.1)
def wait_for_network():
while not self.network.is_connected():
if callback:
msg = "{} \n".format(_("Connecting..."))
2015-12-31 11:36:33 +09:00
callback(msg)
2015-10-28 10:36:44 +01:00
time.sleep(0.1)
2015-12-31 11:36:33 +09:00
# wait until we are connected, because the user
# might have selected another server
2015-10-28 10:36:44 +01:00
if self.network:
self.print_error("waiting for network...")
2015-10-28 10:36:44 +01:00
wait_for_network()
self.print_error("waiting while wallet is syncing...")
2015-10-28 10:36:44 +01:00
wait_for_wallet()
else:
self.synchronize()
2014-04-30 11:40:53 +02:00
def can_export(self):
2017-07-10 09:46:11 +02:00
return not self.is_watching_only() and hasattr(self.keystore, 'get_private_key')
2014-06-24 16:47:58 +02:00
def address_is_old(self, address, age_limit=2):
age = -1
h = self.history.get(address, [])
for tx_hash, tx_height in h:
if tx_height <= 0:
2014-06-24 16:47:58 +02:00
tx_age = 0
else:
tx_age = self.get_local_height() - tx_height + 1
2014-06-24 16:47:58 +02:00
if tx_age > age:
age = tx_age
return age > age_limit
2016-05-20 10:38:48 +02:00
def bump_fee(self, tx, delta):
if tx.is_final():
2018-06-14 22:36:54 +02:00
raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('transaction is final'))
2018-06-14 22:19:06 +02:00
tx = Transaction(tx.serialize())
tx.deserialize(force_full_parse=True) # need to parse inputs
2016-05-20 10:38:48 +02:00
inputs = copy.deepcopy(tx.inputs())
outputs = copy.deepcopy(tx.outputs())
for txin in inputs:
txin['signatures'] = [None] * len(txin['signatures'])
2016-10-14 05:38:43 +02:00
self.add_input_info(txin)
2016-10-21 12:59:55 +02:00
# use own outputs
2017-02-04 20:59:22 +03:00
s = list(filter(lambda x: self.is_mine(x[1]), outputs))
2016-10-21 12:59:55 +02:00
# ... unless there is none
if not s:
s = outputs
2017-07-06 16:03:21 +02:00
x_fee = run_hook('get_tx_extra_fee', self, tx)
if x_fee:
x_fee_address, x_fee_amount = x_fee
s = filter(lambda x: x[1]!=x_fee_address, s)
2016-10-21 12:59:55 +02:00
# prioritize low value outputs, to get rid of dust
s = sorted(s, key=lambda x: x[2])
for o in s:
i = outputs.index(o)
2018-07-31 18:24:37 +02:00
if o.value - delta >= self.dust_threshold():
outputs[i] = o._replace(value=o.value-delta)
2016-10-27 14:32:27 +02:00
delta = 0
2016-05-20 10:38:48 +02:00
break
2016-10-21 12:59:55 +02:00
else:
del outputs[i]
2018-07-31 18:24:37 +02:00
delta -= o.value
2016-10-21 12:59:55 +02:00
if delta > 0:
continue
if delta > 0:
2018-06-14 22:36:54 +02:00
raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('could not find suitable outputs'))
locktime = self.get_local_height()
2017-12-22 02:33:22 +01:00
tx_new = Transaction.from_io(inputs, outputs, locktime=locktime)
tx_new.BIP_LI01_sort()
return tx_new
2016-05-20 10:38:48 +02:00
def cpfp(self, tx, fee):
txid = tx.txid()
for i, o in enumerate(tx.outputs()):
2018-07-31 18:24:37 +02:00
address, value = o.address, o.value
if o.type == TYPE_ADDRESS and self.is_mine(address):
break
else:
return
coins = self.get_addr_utxo(address)
2017-10-07 12:52:52 +02:00
item = coins.get(txid+':%d'%i)
if not item:
return
self.add_input_info(item)
inputs = [item]
2018-07-31 18:24:37 +02:00
outputs = [TxOutput(TYPE_ADDRESS, address, value - fee)]
locktime = self.get_local_height()
2017-12-22 02:33:22 +01:00
# note: no need to call tx.BIP_LI01_sort() here - single input/output
return Transaction.from_io(inputs, outputs, locktime=locktime)
def add_input_sig_info(self, txin, address):
raise NotImplementedError() # implemented by subclasses
2016-07-02 08:58:56 +02:00
def add_input_info(self, txin):
address = txin['address']
if self.is_mine(address):
txin['type'] = self.get_txin_type(address)
# segwit needs value to sign
if txin.get('value') is None and Transaction.is_input_value_needed(txin):
received, spent = self.get_addr_io(address)
item = received.get(txin['prevout_hash']+':%d'%txin['prevout_n'])
tx_height, value, is_cb = item
txin['value'] = value
2016-07-02 08:58:56 +02:00
self.add_input_sig_info(txin, address)
def add_input_info_to_all_inputs(self, tx):
if tx.is_complete():
return
for txin in tx.inputs():
self.add_input_info(txin)
2016-07-02 08:58:56 +02:00
def can_sign(self, tx):
if tx.is_complete():
return False
# add info to inputs if we can; otherwise we might return a false negative:
self.add_input_info_to_all_inputs(tx) # though note that this is a side-effect
2016-09-01 13:52:49 +02:00
for k in self.get_keystores():
if k.can_sign(tx):
return True
2016-10-14 05:38:43 +02:00
return False
2016-07-02 08:58:56 +02:00
2018-03-03 02:39:49 +01:00
def get_input_tx(self, tx_hash, ignore_timeout=False):
2016-07-02 08:58:56 +02:00
# First look up an input transaction in the wallet where it
# will likely be. If co-signing a transaction it may not have
# all the input txs, in which case we ask the network.
2018-03-03 02:39:49 +01:00
tx = self.transactions.get(tx_hash, None)
2016-09-01 13:52:49 +02:00
if not tx and self.network:
2018-03-03 02:39:49 +01:00
try:
tx = Transaction(self.network.get_transaction(tx_hash))
2018-03-03 02:39:49 +01:00
except TimeoutException as e:
self.print_error('getting input txn from network timed out for {}'.format(tx_hash))
if not ignore_timeout:
raise e
2016-07-02 08:58:56 +02:00
return tx
2014-04-30 11:18:13 +02:00
def add_hw_info(self, tx):
2016-07-02 08:58:56 +02:00
# add previous tx for hw wallets
for txin in tx.inputs():
tx_hash = txin['prevout_hash']
2018-03-03 02:39:49 +01:00
# segwit inputs might not be needed for some hw wallets
ignore_timeout = Transaction.is_segwit_input(txin)
txin['prev_tx'] = self.get_input_tx(tx_hash, ignore_timeout)
2016-07-02 08:58:56 +02:00
# add output info for hw wallets
info = {}
xpubs = self.get_master_public_keys()
for txout in tx.outputs():
2016-07-02 08:58:56 +02:00
_type, addr, amount = txout
if self.is_mine(addr):
index = self.get_address_index(addr)
pubkeys = self.get_public_keys(addr)
# sort xpubs using the order of pubkeys
sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs)))
num_sig = self.m if isinstance(self, Multisig_Wallet) else None
2018-08-14 19:38:19 +02:00
info[addr] = TxOutputHwInfo(index, sorted_xpubs, num_sig, self.txin_type)
tx.output_info = info
2016-07-02 08:58:56 +02:00
def sign_transaction(self, tx, password):
if self.is_watching_only():
return
self.add_input_info_to_all_inputs(tx)
# hardware wallets require extra info
if any([(isinstance(k, Hardware_KeyStore) and k.can_sign(tx)) for k in self.get_keystores()]):
self.add_hw_info(tx)
2018-04-03 01:50:37 +02:00
# sign. start with ready keystores.
for k in sorted(self.get_keystores(), key=lambda ks: ks.ready_to_sign(), reverse=True):
try:
2016-09-29 11:50:32 +02:00
if k.can_sign(tx):
k.sign_transaction(tx, password)
2016-09-22 13:38:59 +02:00
except UserCancelled:
continue
2018-05-18 18:07:52 +02:00
return tx
2016-07-02 08:58:56 +02:00
def get_unused_addresses(self):
# fixme: use slots from expired requests
2016-07-02 08:58:56 +02:00
domain = self.get_receiving_addresses()
return [addr for addr in domain if not self.history.get(addr)
and addr not in self.receive_requests.keys()]
2016-07-02 08:58:56 +02:00
def get_unused_address(self):
addrs = self.get_unused_addresses()
if addrs:
return addrs[0]
def get_receiving_address(self):
# always return an address
domain = self.get_receiving_addresses()
2017-02-22 08:47:17 +01:00
if not domain:
return
choice = domain[0]
for addr in domain:
2017-01-06 18:12:10 +01:00
if not self.history.get(addr):
if addr not in self.receive_requests.keys():
return addr
else:
choice = addr
return choice
def get_payment_status(self, address, amount):
local_height = self.get_local_height()
received, sent = self.get_addr_io(address)
l = []
for txo, x in received.items():
h, v, is_cb = x
txid, n = txo.split(':')
info = self.verified_tx.get(txid)
if info:
conf = local_height - info.height
else:
conf = 0
l.append((conf, v))
vsum = 0
for conf, v in reversed(sorted(l)):
vsum += v
if vsum >= amount:
return True, conf
return False, None
2015-06-08 12:51:45 +02:00
def get_payment_request(self, addr, config):
r = self.receive_requests.get(addr)
2015-06-07 18:44:33 +02:00
if not r:
return
2015-06-08 12:51:45 +02:00
out = copy.copy(r)
2017-02-16 10:54:24 +01:00
out['URI'] = 'bitcoin:' + addr + '?amount=' + format_satoshis(out.get('amount'))
status, conf = self.get_request_status(addr)
out['status'] = status
if conf is not None:
out['confirmations'] = conf
2015-06-08 12:51:45 +02:00
# check if bip70 file exists
rdir = config.get('requests_dir')
2015-06-12 20:18:06 +02:00
if rdir:
key = out.get('id', addr)
2016-10-01 05:40:40 +00:00
path = os.path.join(rdir, 'req', key[0], key[1], key)
2015-06-12 20:18:06 +02:00
if os.path.exists(path):
baseurl = 'file://' + rdir
rewrite = config.get('url_rewrite')
if rewrite:
2018-04-03 19:18:10 +02:00
try:
baseurl = baseurl.replace(*rewrite)
except BaseException as e:
self.print_stderr('Invalid config setting for "url_rewrite". err:', e)
2016-10-01 05:40:40 +00:00
out['request_url'] = os.path.join(baseurl, 'req', key[0], key[1], key, key)
2015-06-12 20:18:06 +02:00
out['URI'] += '&r=' + out['request_url']
out['index_url'] = os.path.join(baseurl, 'index.html') + '?id=' + key
2016-09-22 18:57:23 +00:00
websocket_server_announce = config.get('websocket_server_announce')
if websocket_server_announce:
out['websocket_server'] = websocket_server_announce
else:
out['websocket_server'] = config.get('websocket_server', 'localhost')
websocket_port_announce = config.get('websocket_port_announce')
if websocket_port_announce:
out['websocket_port'] = websocket_port_announce
else:
out['websocket_port'] = config.get('websocket_port', 9999)
2015-06-08 12:51:45 +02:00
return out
2015-06-02 09:18:39 +02:00
2015-06-07 18:44:33 +02:00
def get_request_status(self, key):
r = self.receive_requests.get(key)
if r is None:
return PR_UNKNOWN
2015-06-07 18:44:33 +02:00
address = r['address']
amount = r.get('amount')
2015-07-21 12:26:37 +02:00
timestamp = r.get('time', 0)
2015-07-22 15:28:43 +02:00
if timestamp and type(timestamp) != int:
timestamp = 0
2015-07-21 12:26:37 +02:00
expiration = r.get('exp')
2015-07-22 15:28:43 +02:00
if expiration and type(expiration) != int:
expiration = 0
conf = None
2015-06-02 11:36:06 +02:00
if amount:
2018-07-18 11:18:57 +02:00
if self.is_up_to_date():
paid, conf = self.get_payment_status(address, amount)
2015-06-12 09:58:29 +02:00
status = PR_PAID if paid else PR_UNPAID
if status == PR_UNPAID and expiration is not None and time.time() > timestamp + expiration:
status = PR_EXPIRED
else:
status = PR_UNKNOWN
2015-06-02 11:36:06 +02:00
else:
status = PR_UNKNOWN
return status, conf
2015-06-02 11:36:06 +02:00
def make_payment_request(self, addr, amount, message, expiration):
2015-06-08 12:51:45 +02:00
timestamp = int(time.time())
2017-01-22 21:25:24 +03:00
_id = bh2u(Hash(addr + "%d"%timestamp))[0:10]
2015-07-21 12:26:37 +02:00
r = {'time':timestamp, 'amount':amount, 'exp':expiration, 'address':addr, 'memo':message, 'id':_id}
return r
2015-07-22 09:06:03 +02:00
def sign_payment_request(self, key, alias, alias_addr, password):
req = self.receive_requests.get(key)
alias_privkey = self.export_private_key(alias_addr, password)[0]
2015-07-22 09:06:03 +02:00
pr = paymentrequest.make_unsigned_request(req)
paymentrequest.sign_request_with_alias(pr, alias, alias_privkey)
req['name'] = pr.pki_data
2017-01-22 21:25:24 +03:00
req['sig'] = bh2u(pr.signature)
2015-07-22 09:06:03 +02:00
self.receive_requests[key] = req
self.storage.put('payment_requests', self.receive_requests)
def add_payment_request(self, req, config):
addr = req['address']
if not bitcoin.is_address(addr):
raise Exception(_('Invalid Bitcoin address.'))
if not self.is_mine(addr):
raise Exception(_('Address not in wallet.'))
amount = req.get('amount')
message = req.get('memo')
self.receive_requests[addr] = req
self.storage.put('payment_requests', self.receive_requests)
2015-06-08 13:21:13 +02:00
self.set_label(addr, message) # should be a default label
2015-06-08 13:21:13 +02:00
rdir = config.get('requests_dir')
if rdir and amount is not None:
2015-06-08 13:21:13 +02:00
key = req.get('id', addr)
2015-07-22 09:06:03 +02:00
pr = paymentrequest.make_request(config, req)
2016-10-01 05:40:40 +00:00
path = os.path.join(rdir, 'req', key[0], key[1], key)
if not os.path.exists(path):
try:
os.makedirs(path)
except OSError as exc:
if exc.errno != errno.EEXIST:
raise
with open(os.path.join(path, key), 'wb') as f:
2015-07-11 21:09:56 +02:00
f.write(pr.SerializeToString())
2015-06-08 13:21:13 +02:00
# reload
req = self.get_payment_request(addr, config)
with open(os.path.join(path, key + '.json'), 'w', encoding='utf-8') as f:
2015-06-08 13:21:13 +02:00
f.write(json.dumps(req))
return req
2015-06-02 09:18:39 +02:00
2015-06-08 12:51:45 +02:00
def remove_payment_request(self, addr, config):
2015-06-02 09:18:39 +02:00
if addr not in self.receive_requests:
return False
2015-06-08 12:51:45 +02:00
r = self.receive_requests.pop(addr)
rdir = config.get('requests_dir')
if rdir:
key = r.get('id', addr)
2015-07-22 16:00:08 +02:00
for s in ['.json', '']:
2016-10-01 05:40:40 +00:00
n = os.path.join(rdir, 'req', key[0], key[1], key, key + s)
2015-06-08 12:51:45 +02:00
if os.path.exists(n):
os.unlink(n)
self.storage.put('payment_requests', self.receive_requests)
2015-06-02 09:18:39 +02:00
return True
2015-06-08 12:51:45 +02:00
def get_sorted_requests(self, config):
2018-03-12 12:19:45 +01:00
def f(addr):
try:
2018-03-12 12:19:45 +01:00
return self.get_address_index(addr)
except:
2018-03-12 12:19:45 +01:00
return
keys = map(lambda x: (f(x), x), self.receive_requests.keys())
sorted_keys = sorted(filter(lambda x: x[0] is not None, keys))
return [self.get_payment_request(x[1], config) for x in sorted_keys]
2015-06-07 18:44:33 +02:00
2016-02-22 15:44:31 +01:00
def get_fingerprint(self):
raise NotImplementedError()
2015-06-07 18:44:33 +02:00
2016-08-17 10:39:30 +02:00
def can_import_privkey(self):
2016-08-16 12:11:39 +02:00
return False
2016-08-17 10:39:30 +02:00
def can_import_address(self):
return False
2016-10-12 12:03:56 +02:00
def can_delete_address(self):
return False
2016-08-21 11:58:15 +02:00
def has_password(self):
return self.has_keystore_encryption() or self.has_storage_encryption()
def can_have_keystore_encryption(self):
return self.keystore and self.keystore.may_have_password()
def get_available_storage_encryption_version(self):
"""Returns the type of storage encryption offered to the user.
A wallet file (storage) is either encrypted with this version
or is stored in plaintext.
"""
if isinstance(self.keystore, Hardware_KeyStore):
return STO_EV_XPUB_PW
else:
return STO_EV_USER_PW
def has_keystore_encryption(self):
"""Returns whether encryption is enabled for the keystore.
If True, e.g. signing a transaction will require a password.
"""
if self.can_have_keystore_encryption():
return self.storage.get('use_encryption', False)
return False
def has_storage_encryption(self):
"""Returns whether encryption is enabled for the wallet file on disk."""
return self.storage.is_encrypted()
@classmethod
def may_have_password(cls):
return True
2016-08-21 11:58:15 +02:00
def check_password(self, password):
if self.has_keystore_encryption():
self.keystore.check_password(password)
self.storage.check_password(password)
def update_password(self, old_pw, new_pw, encrypt_storage=False):
if old_pw is None and self.has_password():
raise InvalidPassword()
self.check_password(old_pw)
if encrypt_storage:
enc_version = self.get_available_storage_encryption_version()
else:
enc_version = STO_EV_PLAINTEXT
self.storage.set_password(new_pw, enc_version)
# note: Encrypting storage with a hw device is currently only
# allowed for non-multisig wallets. Further,
# Hardware_KeyStore.may_have_password() == False.
# If these were not the case,
# extra care would need to be taken when encrypting keystores.
self._update_password_for_keystore(old_pw, new_pw)
encrypt_keystore = self.can_have_keystore_encryption()
self.storage.set_keystore_encryption(bool(new_pw) and encrypt_keystore)
self.storage.write()
def sign_message(self, address, message, password):
index = self.get_address_index(address)
return self.keystore.sign_message(index, message, password)
def decrypt_message(self, pubkey, message, password):
addr = self.pubkeys_to_address(pubkey)
index = self.get_address_index(addr)
return self.keystore.decrypt_message(index, message, password)
def txin_value(self, txin):
txid = txin['prevout_hash']
prev_n = txin['prevout_n']
2018-03-12 10:30:56 +01:00
for address, d in self.txo.get(txid, {}).items():
for n, v, cb in d:
if n == prev_n:
return v
2018-03-12 10:30:56 +01:00
# may occur if wallet is not synchronized
return None
def price_at_timestamp(self, txid, price_func):
"""Returns fiat price of bitcoin at the time tx got confirmed."""
timestamp = self.get_tx_height(txid).timestamp
2018-02-23 09:11:25 +01:00
return price_func(timestamp if timestamp else time.time())
2018-02-15 14:59:05 +01:00
def unrealized_gains(self, domain, price_func, ccy):
coins = self.get_utxos(domain)
now = time.time()
p = price_func(now)
ap = sum(self.coin_price(coin['prevout_hash'], price_func, ccy, self.txin_value(coin)) for coin in coins)
2018-02-15 14:59:05 +01:00
lp = sum([coin['value'] for coin in coins]) * p / Decimal(COIN)
return lp - ap
2018-02-15 14:59:05 +01:00
def average_price(self, txid, price_func, ccy):
""" Average acquisition price of the inputs of a transaction """
input_value = 0
total_price = 0
for addr, d in self.txi.get(txid, {}).items():
for ser, v in d:
input_value += v
total_price += self.coin_price(ser.split(':')[0], price_func, ccy, v)
return total_price / (input_value/Decimal(COIN))
def coin_price(self, txid, price_func, ccy, txin_value):
"""
Acquisition price of a coin.
This assumes that either all inputs are mine, or no input is mine.
"""
2018-03-12 10:30:56 +01:00
if txin_value is None:
return Decimal('NaN')
cache_key = "{}:{}:{}".format(str(txid), str(ccy), str(txin_value))
result = self.coin_price_cache.get(cache_key, None)
if result is not None:
return result
if self.txi.get(txid, {}) != {}:
result = self.average_price(txid, price_func, ccy) * txin_value/Decimal(COIN)
2018-09-05 14:38:43 +02:00
if not result.is_nan():
self.coin_price_cache[cache_key] = result
2018-03-06 17:15:14 +01:00
return result
else:
fiat_value = self.get_fiat_value(txid, ccy)
if fiat_value is not None:
2018-03-06 17:15:14 +01:00
return fiat_value
else:
2018-02-20 09:58:36 +01:00
p = self.price_at_timestamp(txid, price_func)
2018-03-06 17:15:14 +01:00
return p * txin_value/Decimal(COIN)
2018-07-02 14:23:14 +02:00
def is_billing_address(self, addr):
# overloaded for TrustedCoin wallets
return False
2016-07-02 08:58:56 +02:00
2017-10-15 10:14:55 +02:00
class Simple_Wallet(Abstract_Wallet):
# wallet with a single keystore
def get_keystore(self):
return self.keystore
def get_keystores(self):
return [self.keystore]
def is_watching_only(self):
return self.keystore.is_watching_only()
def _update_password_for_keystore(self, old_pw, new_pw):
if self.keystore and self.keystore.may_have_password():
self.keystore.update_password(old_pw, new_pw)
self.save_keystore()
2017-10-15 10:14:55 +02:00
def save_keystore(self):
self.storage.put('keystore', self.keystore.dump())
class Imported_Wallet(Simple_Wallet):
2016-07-02 08:58:56 +02:00
# wallet made of imported addresses
2014-08-20 18:54:37 +02:00
wallet_type = 'imported'
2017-03-20 10:47:03 +01:00
txin_type = 'address'
2014-04-30 11:18:13 +02:00
def __init__(self, storage):
Abstract_Wallet.__init__(self, storage)
2016-07-02 08:58:56 +02:00
def is_watching_only(self):
return self.keystore is None
def get_keystores(self):
return [self.keystore] if self.keystore else []
def can_import_privkey(self):
return bool(self.keystore)
2016-07-02 08:58:56 +02:00
def load_keystore(self):
self.keystore = load_keystore(self.storage, 'keystore') if self.storage.get('keystore') else None
2016-07-02 08:58:56 +02:00
def save_keystore(self):
self.storage.put('keystore', self.keystore.dump())
def load_addresses(self):
self.addresses = self.storage.get('addresses', {})
# fixme: a reference to addresses is needed
if self.keystore:
self.keystore.addresses = self.addresses
2016-07-02 08:58:56 +02:00
def save_addresses(self):
self.storage.put('addresses', self.addresses)
2016-07-02 08:58:56 +02:00
2016-08-17 10:39:30 +02:00
def can_import_address(self):
return self.is_watching_only()
def can_delete_address(self):
2017-10-10 11:48:27 +02:00
return True
2014-04-30 11:18:13 +02:00
2014-04-30 11:40:53 +02:00
def has_seed(self):
return False
2014-05-01 12:19:24 +02:00
def is_deterministic(self):
return False
def is_change(self, address):
return False
2014-06-24 16:47:58 +02:00
def get_master_public_keys(self):
return []
2014-06-24 16:47:58 +02:00
def is_beyond_limit(self, address):
return False
2014-04-30 11:18:13 +02:00
2016-02-22 15:44:31 +01:00
def get_fingerprint(self):
return ''
2014-07-03 08:24:25 +02:00
def get_addresses(self):
# note: overridden so that the history can be cleared
return sorted(self.addresses.keys())
def get_receiving_addresses(self):
return self.get_addresses()
def get_change_addresses(self):
return []
2016-07-02 08:58:56 +02:00
2016-08-17 10:39:30 +02:00
def import_address(self, address):
if not bitcoin.is_address(address):
return ''
2016-07-02 08:58:56 +02:00
if address in self.addresses:
return ''
self.addresses[address] = {}
2016-08-17 10:39:30 +02:00
self.add_address(address)
2018-07-19 18:16:23 +02:00
self.save_addresses()
self.save_transactions(write=True)
2016-07-02 08:58:56 +02:00
return address
2016-08-17 10:39:30 +02:00
def delete_address(self, address):
if address not in self.addresses:
return
transactions_to_remove = set() # only referred to by this address
transactions_new = set() # txs that are not only referred to by address
with self.lock:
for addr, details in self.history.items():
if addr == address:
for tx_hash, height in details:
transactions_to_remove.add(tx_hash)
else:
for tx_hash, height in details:
transactions_new.add(tx_hash)
transactions_to_remove -= transactions_new
self.history.pop(address, None)
for tx_hash in transactions_to_remove:
self.remove_transaction(tx_hash)
self.tx_fees.pop(tx_hash, None)
self.verified_tx.pop(tx_hash, None)
self.unverified_tx.pop(tx_hash, None)
self.transactions.pop(tx_hash, None)
self.save_verified_tx()
self.save_transactions()
self.set_label(address, None)
self.remove_payment_request(address, {})
self.set_frozen_state([address], False)
2017-10-10 11:38:30 +02:00
pubkey = self.get_public_key(address)
self.addresses.pop(address)
2017-10-10 11:38:30 +02:00
if pubkey:
2018-03-22 07:27:18 +01:00
# delete key iff no other address uses it (e.g. p2pkh and p2wpkh for same key)
2018-07-01 17:05:56 +02:00
for txin_type in bitcoin.WIF_SCRIPT_TYPES.keys():
2018-03-22 07:27:18 +01:00
try:
addr2 = bitcoin.pubkey_to_address(txin_type, pubkey)
except NotImplementedError:
pass
else:
if addr2 in self.addresses:
break
else:
self.keystore.delete_imported_key(pubkey)
self.save_keystore()
2018-07-19 18:16:23 +02:00
self.save_addresses()
2016-08-17 10:39:30 +02:00
self.storage.write()
def get_address_index(self, address):
return self.get_public_key(address)
def get_public_key(self, address):
return self.addresses[address].get('pubkey')
def import_private_key(self, sec, pw, redeem_script=None):
try:
txin_type, pubkey = self.keystore.import_privkey(sec, pw)
except Exception:
2018-03-10 08:00:41 +01:00
neutered_privkey = str(sec)[:3] + '..' + str(sec)[-2:]
raise BitcoinException('Invalid private key: {}'.format(neutered_privkey))
if txin_type in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']:
if redeem_script is not None:
raise BitcoinException('Cannot use redeem script with script type {}'.format(txin_type))
addr = bitcoin.pubkey_to_address(txin_type, pubkey)
elif txin_type in ['p2sh', 'p2wsh', 'p2wsh-p2sh']:
if redeem_script is None:
raise BitcoinException('Redeem script required for script type {}'.format(txin_type))
addr = bitcoin.redeem_script_to_address(txin_type, redeem_script)
else:
2017-10-25 06:54:51 +02:00
raise NotImplementedError(txin_type)
self.addresses[addr] = {'type':txin_type, 'pubkey':pubkey, 'redeem_script':redeem_script}
self.save_keystore()
self.add_address(addr)
2018-07-19 18:16:23 +02:00
self.save_addresses()
self.save_transactions(write=True)
return addr
2016-07-02 08:58:56 +02:00
def get_redeem_script(self, address):
2017-10-05 09:44:37 +02:00
d = self.addresses[address]
redeem_script = d['redeem_script']
return redeem_script
2016-07-02 08:58:56 +02:00
def get_txin_type(self, address):
return self.addresses[address].get('type', 'address')
def add_input_sig_info(self, txin, address):
if self.is_watching_only():
x_pubkey = 'fd' + address_to_script(address)
txin['x_pubkeys'] = [x_pubkey]
txin['signatures'] = [None]
return
if txin['type'] in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']:
pubkey = self.addresses[address]['pubkey']
txin['num_sig'] = 1
txin['x_pubkeys'] = [pubkey]
txin['signatures'] = [None]
else:
raise NotImplementedError('imported wallets for p2sh are not implemented')
def pubkeys_to_address(self, pubkey):
for addr, v in self.addresses.items():
if v.get('pubkey') == pubkey:
return addr
2016-07-02 08:58:56 +02:00
2014-04-30 11:18:13 +02:00
class Deterministic_Wallet(Abstract_Wallet):
def __init__(self, storage):
Abstract_Wallet.__init__(self, storage)
2016-07-02 08:58:56 +02:00
self.gap_limit = storage.get('gap_limit', 20)
2014-04-30 11:18:13 +02:00
2014-04-30 11:40:53 +02:00
def has_seed(self):
2016-07-02 08:58:56 +02:00
return self.keystore.has_seed()
2014-04-30 11:40:53 +02:00
2018-07-19 18:16:23 +02:00
def get_addresses(self):
# note: overridden so that the history can be cleared.
# addresses are ordered based on derivation
2018-07-19 18:16:23 +02:00
out = []
out += self.get_receiving_addresses()
out += self.get_change_addresses()
return out
2016-07-02 08:58:56 +02:00
def get_receiving_addresses(self):
return self.receiving_addresses
2014-06-24 16:12:43 +03:00
2016-07-02 08:58:56 +02:00
def get_change_addresses(self):
return self.change_addresses
2014-05-01 18:58:24 +02:00
2014-04-30 11:18:13 +02:00
def get_seed(self, password):
2016-07-02 08:58:56 +02:00
return self.keystore.get_seed(password)
def add_seed(self, seed, pw):
self.keystore.add_seed(seed, pw)
2014-04-30 11:18:13 +02:00
def change_gap_limit(self, value):
2016-01-12 09:35:45 +01:00
'''This method is not called in the code, it is kept for console use'''
2014-04-30 11:18:13 +02:00
if value >= self.gap_limit:
self.gap_limit = value
2015-12-20 15:39:57 +09:00
self.storage.put('gap_limit', self.gap_limit)
2014-04-30 11:18:13 +02:00
return True
elif value >= self.min_acceptable_gap():
2016-07-02 08:58:56 +02:00
addresses = self.get_receiving_addresses()
k = self.num_unused_trailing_addresses(addresses)
n = len(addresses) - k + value
self.receiving_addresses = self.receiving_addresses[0:n]
2014-04-30 11:18:13 +02:00
self.gap_limit = value
2015-12-20 15:39:57 +09:00
self.storage.put('gap_limit', self.gap_limit)
2017-02-22 08:47:17 +01:00
self.save_addresses()
2014-04-30 11:18:13 +02:00
return True
else:
return False
def num_unused_trailing_addresses(self, addresses):
k = 0
for a in addresses[::-1]:
if self.history.get(a):break
k = k + 1
return k
def min_acceptable_gap(self):
# fixme: this assumes wallet is synchronized
n = 0
nmax = 0
addresses = self.get_receiving_addresses()
2016-07-02 08:58:56 +02:00
k = self.num_unused_trailing_addresses(addresses)
for a in addresses[0:-k]:
if self.history.get(a):
n = 0
else:
n += 1
if n > nmax: nmax = n
2014-04-30 11:18:13 +02:00
return nmax + 1
def load_addresses(self):
super().load_addresses()
self._addr_to_addr_index = {} # key: address, value: (is_change, index)
for i, addr in enumerate(self.receiving_addresses):
self._addr_to_addr_index[addr] = (False, i)
for i, addr in enumerate(self.change_addresses):
self._addr_to_addr_index[addr] = (True, i)
2017-04-04 13:52:16 +02:00
def create_new_address(self, for_change=False):
assert type(for_change) is bool
2018-02-12 23:20:58 +01:00
with self.lock:
addr_list = self.change_addresses if for_change else self.receiving_addresses
n = len(addr_list)
x = self.derive_pubkeys(for_change, n)
address = self.pubkeys_to_address(x)
addr_list.append(address)
self._addr_to_addr_index[address] = (for_change, n)
self.save_addresses()
self.add_address(address)
if for_change:
# note: if it's actually used, it will get filtered later
self._unused_change_addresses.append(address)
2018-02-12 23:20:58 +01:00
return address
2016-07-02 08:58:56 +02:00
def synchronize_sequence(self, for_change):
limit = self.gap_limit_for_change if for_change else self.gap_limit
while True:
addresses = self.get_change_addresses() if for_change else self.get_receiving_addresses()
if len(addresses) < limit:
self.create_new_address(for_change)
continue
2017-01-30 12:36:56 +03:00
if list(map(lambda a: self.address_is_old(a), addresses[-limit:] )) == limit*[False]:
break
else:
self.create_new_address(for_change)
2014-04-30 11:18:13 +02:00
def synchronize(self):
with self.lock:
2018-02-12 16:12:16 +01:00
self.synchronize_sequence(False)
self.synchronize_sequence(True)
2016-07-02 08:58:56 +02:00
def is_beyond_limit(self, address):
is_change, i = self.get_address_index(address)
2016-07-02 08:58:56 +02:00
addr_list = self.get_change_addresses() if is_change else self.get_receiving_addresses()
limit = self.gap_limit_for_change if is_change else self.gap_limit
if i < limit:
return False
prev_addresses = addr_list[max(0, i - limit):max(0, i)]
for addr in prev_addresses:
if self.history.get(addr):
return False
return True
2018-01-30 00:59:12 +01:00
def get_address_index(self, address):
return self._addr_to_addr_index[address]
2014-08-13 16:05:43 +02:00
def get_master_public_keys(self):
return [self.get_master_public_key()]
2014-04-29 21:04:16 +02:00
2016-02-22 15:44:31 +01:00
def get_fingerprint(self):
return self.get_master_public_key()
2014-08-19 12:38:01 +02:00
def get_txin_type(self, address):
return self.txin_type
2014-08-21 10:04:06 +02:00
2017-10-15 10:14:55 +02:00
class Simple_Deterministic_Wallet(Simple_Wallet, Deterministic_Wallet):
2017-01-16 09:48:38 +01:00
""" Deterministic Wallet with a single pubkey per address """
def __init__(self, storage):
Deterministic_Wallet.__init__(self, storage)
def get_public_key(self, address):
sequence = self.get_address_index(address)
pubkey = self.get_pubkey(*sequence)
return pubkey
2017-01-16 09:48:38 +01:00
def load_keystore(self):
self.keystore = load_keystore(self.storage, 'keystore')
try:
2017-10-31 11:45:25 +01:00
xtype = bitcoin.xpub_type(self.keystore.xpub)
except:
xtype = 'standard'
2017-10-25 17:33:49 +02:00
self.txin_type = 'p2pkh' if xtype == 'standard' else xtype
2017-01-16 09:48:38 +01:00
def get_pubkey(self, c, i):
2017-02-22 08:47:17 +01:00
return self.derive_pubkeys(c, i)
2017-01-16 09:48:38 +01:00
def add_input_sig_info(self, txin, address):
derivation = self.get_address_index(address)
x_pubkey = self.keystore.get_xpubkey(*derivation)
2017-01-16 09:48:38 +01:00
txin['x_pubkeys'] = [x_pubkey]
txin['signatures'] = [None]
txin['num_sig'] = 1
2016-07-02 08:58:56 +02:00
def get_master_public_key(self):
return self.keystore.get_master_public_key()
2014-06-25 16:45:55 +02:00
2017-02-22 08:47:17 +01:00
def derive_pubkeys(self, c, i):
2016-07-02 08:58:56 +02:00
return self.keystore.derive_pubkey(c, i)
2016-07-02 08:58:56 +02:00
2016-10-12 12:03:56 +02:00
2014-08-19 12:38:01 +02:00
2017-01-16 09:48:38 +01:00
class Standard_Wallet(Simple_Deterministic_Wallet):
2017-01-16 09:48:38 +01:00
wallet_type = 'standard'
def pubkeys_to_address(self, pubkey):
return bitcoin.pubkey_to_address(self.txin_type, pubkey)
2017-01-16 09:48:38 +01:00
2017-09-01 14:15:54 +02:00
class Multisig_Wallet(Deterministic_Wallet):
2015-06-26 14:29:26 +02:00
# generic m of n
2016-07-02 08:58:56 +02:00
gap_limit = 20
2015-06-26 14:29:26 +02:00
def __init__(self, storage):
self.wallet_type = storage.get('wallet_type')
2016-08-21 11:58:15 +02:00
self.m, self.n = multisig_type(self.wallet_type)
2016-07-02 08:58:56 +02:00
Deterministic_Wallet.__init__(self, storage)
2015-06-26 14:29:26 +02:00
2016-07-02 08:58:56 +02:00
def get_pubkeys(self, c, i):
2017-02-22 08:47:17 +01:00
return self.derive_pubkeys(c, i)
2014-04-06 21:38:53 +02:00
def get_public_keys(self, address):
sequence = self.get_address_index(address)
return self.get_pubkeys(*sequence)
2017-09-18 08:52:06 +02:00
def pubkeys_to_address(self, pubkeys):
redeem_script = self.pubkeys_to_redeem_script(pubkeys)
return bitcoin.redeem_script_to_address(self.txin_type, redeem_script)
2017-09-01 14:15:54 +02:00
2017-01-16 09:48:38 +01:00
def pubkeys_to_redeem_script(self, pubkeys):
return transaction.multisig_script(sorted(pubkeys), self.m)
2014-04-06 21:38:53 +02:00
def get_redeem_script(self, address):
pubkeys = self.get_public_keys(address)
redeem_script = self.pubkeys_to_redeem_script(pubkeys)
return redeem_script
2017-02-22 08:47:17 +01:00
def derive_pubkeys(self, c, i):
return [k.derive_pubkey(c, i) for k in self.get_keystores()]
2016-07-02 08:58:56 +02:00
def load_keystore(self):
self.keystores = {}
2016-01-06 09:55:49 +01:00
for i in range(self.n):
2016-07-02 08:58:56 +02:00
name = 'x%d/'%(i+1)
self.keystores[name] = load_keystore(self.storage, name)
2016-08-21 11:58:15 +02:00
self.keystore = self.keystores['x1/']
2017-10-31 11:45:25 +01:00
xtype = bitcoin.xpub_type(self.keystore.xpub)
2017-10-26 15:47:47 +02:00
self.txin_type = 'p2sh' if xtype == 'standard' else xtype
2016-06-16 19:25:44 +02:00
def save_keystore(self):
for name, k in self.keystores.items():
self.storage.put(name, k.dump())
2016-07-02 08:58:56 +02:00
def get_keystore(self):
2016-08-21 11:58:15 +02:00
return self.keystores.get('x1/')
2016-01-06 09:55:49 +01:00
2016-07-02 08:58:56 +02:00
def get_keystores(self):
return [self.keystores[i] for i in sorted(self.keystores.keys())]
2016-07-02 08:58:56 +02:00
def can_have_keystore_encryption(self):
return any([k.may_have_password() for k in self.get_keystores()])
def _update_password_for_keystore(self, old_pw, new_pw):
2016-07-02 08:58:56 +02:00
for name, keystore in self.keystores.items():
if keystore.may_have_password():
2017-01-05 06:20:02 +01:00
keystore.update_password(old_pw, new_pw)
self.storage.put(name, keystore.dump())
def check_password(self, password):
for name, keystore in self.keystores.items():
if keystore.may_have_password():
keystore.check_password(password)
self.storage.check_password(password)
def get_available_storage_encryption_version(self):
# multisig wallets are not offered hw device encryption
return STO_EV_USER_PW
2016-02-22 15:44:31 +01:00
2016-07-02 08:58:56 +02:00
def has_seed(self):
return self.keystore.has_seed()
2013-09-03 10:58:07 +02:00
2016-07-02 08:58:56 +02:00
def is_watching_only(self):
return not any([not k.is_watching_only() for k in self.get_keystores()])
def get_master_public_key(self):
2016-07-02 08:58:56 +02:00
return self.keystore.get_master_public_key()
2014-04-25 10:16:07 +02:00
def get_master_public_keys(self):
return [k.get_master_public_key() for k in self.get_keystores()]
2016-07-02 08:58:56 +02:00
def get_fingerprint(self):
return ''.join(sorted(self.get_master_public_keys()))
2016-07-02 08:58:56 +02:00
def add_input_sig_info(self, txin, address):
# x_pubkeys are not sorted here because it would be too slow
# they are sorted in transaction.get_sorted_pubkeys
# pubkeys is set to None to signal that x_pubkeys are unsorted
2017-06-30 18:52:02 +02:00
derivation = self.get_address_index(address)
x_pubkeys_expected = [k.get_xpubkey(*derivation) for k in self.get_keystores()]
x_pubkeys_actual = txin.get('x_pubkeys')
# if 'x_pubkeys' is already set correctly (ignoring order, as above), leave it.
# otherwise we might delete signatures
if x_pubkeys_actual and set(x_pubkeys_actual) == set(x_pubkeys_expected):
return
txin['x_pubkeys'] = x_pubkeys_expected
2017-06-30 18:52:02 +02:00
txin['pubkeys'] = None
2017-02-22 08:47:17 +01:00
# we need n place holders
txin['signatures'] = [None] * self.n
2016-07-02 08:58:56 +02:00
txin['num_sig'] = self.m
2014-02-27 10:21:41 +01:00
2016-08-19 17:26:57 +02:00
wallet_types = ['standard', 'multisig', 'imported']
def register_wallet_type(category):
wallet_types.append(category)
wallet_constructors = {
'standard': Standard_Wallet,
'old': Standard_Wallet,
'xpub': Standard_Wallet,
'imported': Imported_Wallet
2016-08-19 17:26:57 +02:00
}
def register_constructor(wallet_type, constructor):
wallet_constructors[wallet_type] = constructor
2014-02-27 10:21:41 +01:00
# former WalletFactory
class Wallet(object):
2014-06-24 16:12:43 +03:00
"""The main wallet "entry point".
This class is actually a factory that will return a wallet of the correct
type when passed a WalletStorage instance."""
2014-02-27 10:21:41 +01:00
def __new__(self, storage):
wallet_type = storage.get('wallet_type')
2016-07-02 08:58:56 +02:00
WalletClass = Wallet.wallet_class(wallet_type)
2015-12-31 09:51:27 +09:00
wallet = WalletClass(storage)
# Convert hardware wallets restored with older versions of
# Electrum to BIP44 wallets. A hardware wallet does not have
# a seed and plugins do not need to handle having one.
rwc = getattr(wallet, 'restore_wallet_class', None)
if rwc and storage.get('seed', ''):
storage.print_error("converting wallet type to " + rwc.wallet_type)
storage.put('wallet_type', rwc.wallet_type)
wallet = rwc(storage)
return wallet
2014-02-27 10:21:41 +01:00
2016-01-01 23:39:19 +09:00
@staticmethod
2016-07-02 08:58:56 +02:00
def wallet_class(wallet_type):
2016-08-21 11:58:15 +02:00
if multisig_type(wallet_type):
2016-07-02 08:58:56 +02:00
return Multisig_Wallet
2016-08-19 17:26:57 +02:00
if wallet_type in wallet_constructors:
return wallet_constructors[wallet_type]
2018-07-24 18:57:49 +02:00
raise WalletFileException("Unknown wallet type: " + str(wallet_type))