Files
purple-electrumwallet/electrum/wallet.py
T

2340 lines
94 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
2019-06-10 14:05:02 +02:00
import operator
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
2019-10-23 17:09:41 +02:00
from typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequence, Dict, Any
2012-01-19 17:11:36 +01:00
2017-01-22 21:25:24 +03:00
from .i18n import _
from .bip32 import BIP32Node
2019-09-08 11:59:03 +02:00
from .crypto import sha256
2019-04-26 18:52:26 +02:00
from .util import (NotEnoughFunds, UserCancelled, profiler,
format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates,
2018-10-30 19:07:37 +01:00
WalletFileException, BitcoinException,
2018-09-18 18:07:12 +02:00
InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
2019-06-14 13:01:23 +02:00
Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex)
2019-09-08 11:59:03 +02:00
from .util import PR_TYPE_ONCHAIN, PR_TYPE_LN
from .simple_config import SimpleConfig
2019-10-23 17:09:41 +02:00
from .bitcoin import (COIN, is_address, address_to_script,
is_minikey, relayfee, dust_threshold)
2018-10-25 22:28:24 +02:00
from .crypto import sha256d
from . import keystore
2019-09-03 16:24:05 +02:00
from .keystore import load_keystore, Hardware_KeyStore, KeyStore
2019-02-19 11:56:46 +01:00
from .util import multisig_type
2019-09-04 13:31:49 +02:00
from .storage import StorageEncryptionVersion, WalletStorage
2018-10-25 22:20:33 +02:00
from . import transaction, bitcoin, coinchooser, paymentrequest, ecc, bip32
2019-10-23 17:09:41 +02:00
from .transaction import (Transaction, TxInput, UnknownTxinType,
PartialTransaction, PartialTxInput, PartialTxOutput, TxOutpoint)
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, TX_HEIGHT_FUTURE)
2019-08-11 14:47:06 +02:00
from .util import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT
2017-03-15 12:13:20 +01:00
from .contacts import Contacts
from .interface import NetworkException
from .ecc_fast import is_using_fast_ecc
from .mnemonic import Mnemonic
2019-04-26 18:52:26 +02:00
from .logging import get_logger
from .lnworker import LNWallet
2019-08-11 14:47:06 +02:00
from .paymentrequest import PaymentRequest
2018-10-25 23:01:53 +02:00
if TYPE_CHECKING:
from .network import Network
2018-10-22 16:41:25 +02:00
2015-07-22 09:06:03 +02:00
2019-04-26 18:52:26 +02:00
_logger = get_logger(__name__)
TX_STATUS = [
_('Unconfirmed'),
2018-02-27 15:13:44 +01:00
_('Unconfirmed parent'),
_('Not Verified'),
2018-02-27 12:16:50 +01:00
_('Local'),
]
2019-10-23 17:09:41 +02:00
def _append_utxos_to_inputs(inputs: List[PartialTxInput], network: 'Network', pubkey, txin_type, imax):
if txin_type in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'):
address = bitcoin.pubkey_to_address(txin_type, pubkey)
scripthash = bitcoin.address_to_scripthash(address)
2019-10-23 17:09:41 +02:00
elif txin_type == 'p2pk':
script = bitcoin.public_key_to_p2pk_script(pubkey)
scripthash = bitcoin.script_to_scripthash(script)
2019-10-23 17:09:41 +02:00
address = None
else:
raise Exception(f'unexpected txin_type to sweep: {txin_type}')
2018-10-01 05:16:03 +02:00
u = network.run_from_another_thread(network.listunspent_for_scripthash(scripthash))
for item in u:
if len(inputs) >= imax:
break
2019-10-23 17:09:41 +02:00
prevout_str = item['tx_hash'] + ':%d' % item['tx_pos']
prevout = TxOutpoint.from_str(prevout_str)
utxo = PartialTxInput(prevout=prevout)
utxo._trusted_value_sats = int(item['value'])
utxo._trusted_address = address
utxo.block_height = int(item['height'])
utxo.script_type = txin_type
utxo.pubkeys = [bfh(pubkey)]
utxo.num_sig = 1
if txin_type == 'p2wpkh-p2sh':
utxo.redeem_script = bfh(bitcoin.p2wpkh_nested_script(pubkey))
inputs.append(utxo)
2018-10-25 23:01:53 +02:00
def sweep_preparations(privkeys, network: 'Network', imax=100):
def find_utxos_for_privkey(txin_type, privkey, compressed):
pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed)
2019-10-23 17:09:41 +02:00
_append_utxos_to_inputs(inputs, network, pubkey, txin_type, imax)
keypairs[pubkey] = privkey, compressed
2019-10-23 17:09:41 +02:00
inputs = [] # type: List[PartialTxInput]
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
2019-10-23 17:09:41 +02:00
def sweep(privkeys, *, network: 'Network', config: 'SimpleConfig',
to_address: str, fee: int = None, imax=100,
locktime=None, tx_version=None) -> PartialTransaction:
2017-11-26 06:20:40 +01:00
inputs, keypairs = sweep_preparations(privkeys, network, imax)
2019-10-23 17:09:41 +02:00
total = sum(txin.value_sats() for txin in inputs)
if fee is None:
2019-10-23 17:09:41 +02:00
outputs = [PartialTxOutput(scriptpubkey=bfh(bitcoin.address_to_script(to_address)),
value=total)]
tx = PartialTransaction.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)))
2019-10-23 17:09:41 +02:00
outputs = [PartialTxOutput(scriptpubkey=bfh(bitcoin.address_to_script(to_address)),
value=total - fee)]
if locktime is None:
locktime = get_locktime_for_new_transaction(network)
2019-10-23 17:09:41 +02:00
tx = PartialTransaction.from_io(inputs, outputs, locktime=locktime, version=tx_version)
tx.set_rbf(True)
tx.sign(keypairs)
return tx
def get_locktime_for_new_transaction(network: 'Network') -> int:
# if no network or not up to date, just set locktime to zero
if not network:
return 0
chain = network.blockchain()
header = chain.header_at_tip()
if not header:
return 0
STALE_DELAY = 8 * 60 * 60 # in seconds
if header['timestamp'] + STALE_DELAY < time.time():
return 0
# discourage "fee sniping"
locktime = chain.height()
# sometimes pick locktime a bit further back, to help privacy
# of setups that need more time (offline/multisig/coinjoin/...)
if random.randint(0, 9) == 0:
locktime = max(0, locktime - random.randint(0, 99))
return locktime
2018-07-18 11:18:57 +02:00
class CannotBumpFee(Exception): pass
class InternalAddressCorruption(Exception):
def __str__(self):
2018-12-13 12:10:01 +01:00
return _("Wallet file corruption detected. "
"Please restore your wallet from seed, and compare the addresses in both files")
2018-06-14 22:36:54 +02:00
class TxWalletDetails(NamedTuple):
txid: Optional[str]
status: str
label: str
can_broadcast: bool
can_bump: bool
amount: Optional[int]
fee: Optional[int]
tx_mined_status: TxMinedInfo
mempool_depth_bytes: Optional[int]
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
LOGGING_SHORTCUT = 'w'
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
2016-01-23 22:05:08 +09:00
def __init__(self, storage: WalletStorage, *, config: SimpleConfig):
if not storage.is_ready_to_be_used_by_wallet():
raise Exception("storage not ready to be used by Abstract_Wallet")
self.config = config
assert self.config is not None, "config must not be None"
2019-07-03 08:46:00 +02:00
self.storage = storage
2019-03-04 12:53:04 +01:00
# load addresses needs to be called before constructor for sanity checks
2019-07-03 08:46:00 +02:00
self.storage.db.load_addresses(self.wallet_type)
2019-09-03 16:24:05 +02:00
self.keystore = None # type: Optional[KeyStore] # will be set by load_keystore
2019-07-03 08:46:00 +02:00
AddressSynchronizer.__init__(self, storage.db)
2018-07-19 18:16:23 +02:00
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', {})
2019-02-24 08:41:00 +01:00
self.frozen_addresses = set(storage.get('frozen_addresses', []))
self.frozen_coins = set(storage.get('frozen_coins', [])) # set of txid:vout strings
self.fiat_value = storage.get('fiat_value', {})
2018-04-11 19:26:42 +02:00
self.receive_requests = storage.get('payment_requests', {})
2019-08-11 14:47:06 +02:00
self.invoices = storage.get('invoices', {})
2019-09-13 15:00:34 +02:00
# convert invoices
2019-10-23 17:09:41 +02:00
# TODO invoices being these contextual dicts even internally,
# where certain keys are only present depending on values of other keys...
# it's horrible. we need to change this, at least for the internal representation,
# to something that can be typed.
2019-09-13 15:00:34 +02:00
for invoice_key, invoice in self.invoices.items():
2019-09-25 18:56:17 +02:00
if invoice.get('type') == PR_TYPE_ONCHAIN:
2019-10-23 17:09:41 +02:00
outputs = [PartialTxOutput.from_legacy_tuple(*output) for output in invoice.get('outputs')]
2019-09-13 15:00:34 +02:00
invoice['outputs'] = outputs
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)
self.contacts = Contacts(self.storage)
self._coin_price_cache = {}
# lightning
ln_xprv = self.storage.get('lightning_privkey2')
self.lnworker = LNWallet(self, ln_xprv) if ln_xprv else None
def has_lightning(self):
return bool(self.lnworker)
def init_lightning(self):
if self.storage.get('lightning_privkey2'):
return
if not is_using_fast_ecc():
raise Exception('libsecp256k1 library not available. '
'Verifying Lightning channels is too computationally expensive without libsecp256k1, aborting.')
# TODO derive this deterministically from wallet.keystore at keystore generation time
# probably along a hardened path ( lnd-equivalent would be m/1017'/coinType'/ )
seed = os.urandom(32)
node = BIP32Node.from_rootseed(seed, xtype='standard')
ln_xprv = node.to_xprv()
self.storage.put('lightning_privkey2', ln_xprv)
2019-10-14 11:18:57 +02:00
self.storage.write()
def remove_lightning(self):
if not self.storage.get('lightning_privkey2'):
return
if bool(self.lnworker.channels):
raise Exception('Error: This wallet has channels')
self.storage.put('lightning_privkey2', None)
self.storage.write()
2019-07-03 08:46:00 +02:00
def stop_threads(self):
super().stop_threads()
self.storage.write()
def set_up_to_date(self, b):
super().set_up_to_date(b)
if b: self.storage.write()
2019-07-03 13:40:42 +02:00
def clear_history(self):
super().clear_history()
self.storage.write()
2018-09-23 17:43:51 +02:00
def start_network(self, network):
AddressSynchronizer.start_network(self, network)
2019-05-06 12:26:53 +02:00
if self.lnworker:
network.maybe_init_lightning()
self.lnworker.start_network(network)
2018-09-23 17:43:51 +02:00
2018-07-19 18:16:23 +02:00
def load_and_cleanup(self):
self.load_keystore()
self.test_addresses_sanity()
super().load_and_cleanup()
2019-09-03 16:24:05 +02:00
def load_keystore(self) -> None:
raise NotImplementedError() # implemented by subclasses
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
2019-10-23 17:09:41 +02:00
def basename(self) -> str:
return os.path.basename(self.storage.path)
2018-03-09 03:18:53 +01:00
def test_addresses_sanity(self):
addrs = self.get_receiving_addresses()
if len(addrs) > 0:
addr = str(addrs[0])
if not bitcoin.is_address(addr):
neutered_addr = addr[:5] + '..' + addr[-2:]
raise WalletFileException(f'The addresses in this wallet are not bitcoin addresses.\n'
f'e.g. {neutered_addr} (length: {len(addr)})')
2018-03-09 03:18:53 +01:00
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 is not None:
2013-09-29 12:14:01 +02:00
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
2018-11-27 21:15:31 +01:00
def set_fiat_value(self, txid, ccy, text, fx, value_sat):
if not self.db.get_transaction(txid):
return
2018-11-26 21:21:02 +01:00
# since fx is inserting the thousands separator,
# and not util, also have fx remove it
text = fx.remove_thousands_separator(text)
2018-11-27 21:15:31 +01:00
def_fiat = self.default_fiat_value(txid, fx, value_sat)
2018-11-26 21:21:02 +01:00
formatted = fx.ccy_amount_str(def_fiat, commas=False)
def_fiat_rounded = Decimal(formatted)
reset = not text
if not reset:
try:
text_dec = Decimal(text)
text_dec_rounded = Decimal(fx.ccy_amount_str(text_dec, commas=False))
reset = text_dec_rounded == def_fiat_rounded
except:
# garbage. not resetting, but not saving either
return False
if reset:
d = self.fiat_value.get(ccy, {})
if d and txid in d:
d.pop(txid)
else:
2018-11-26 21:21:02 +01:00
# avoid saving empty dict
return True
2018-11-27 18:34:36 +01:00
else:
if ccy not in self.fiat_value:
self.fiat_value[ccy] = {}
2018-11-26 21:21:02 +01:00
self.fiat_value[ccy][txid] = text
self.storage.put('fiat_value', self.fiat_value)
2018-11-26 21:21:02 +01:00
return reset
def get_fiat_value(self, txid, ccy):
fiat_value = self.fiat_value.get(ccy, {}).get(txid)
try:
return Decimal(fiat_value)
except:
return
def is_mine(self, address) -> bool:
2019-03-04 12:53:04 +01:00
return bool(self.get_address_index(address))
2018-07-19 18:16:23 +02:00
def is_change(self, address) -> bool:
2016-07-02 08:58:56 +02:00
if not self.is_mine(address):
return False
2019-10-23 17:09:41 +02:00
return self.get_address_index(address)[0] == 1
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
2019-10-23 17:09:41 +02:00
def get_redeem_script(self, address: str) -> Optional[str]:
txin_type = self.get_txin_type(address)
if txin_type in ('p2pkh', 'p2wpkh', 'p2pk'):
return None
if txin_type == 'p2wpkh-p2sh':
pubkey = self.get_public_key(address)
return bitcoin.p2wpkh_nested_script(pubkey)
raise UnknownTxinType(f'unexpected txin_type {txin_type}')
def get_witness_script(self, address: str) -> Optional[str]:
return None
2019-10-23 17:09:41 +02:00
def get_txin_type(self, address: str) -> str:
"""Return script type of wallet address."""
raise NotImplementedError()
def export_private_key(self, address, password) -> str:
2013-11-12 11:14:16 +01:00
if self.is_watching_only():
raise Exception(_("This is a watching-only wallet"))
2019-06-12 18:27:13 +02:00
if not is_address(address):
raise Exception(f"Invalid bitcoin address: {address}")
if not self.is_mine(address):
raise Exception(_('Address not in wallet.') + f' {address}')
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)
serialized_privkey = bitcoin.serialize_privkey(pk, compressed, txin_type)
2019-10-23 17:09:41 +02:00
return serialized_privkey
2013-08-05 13:53:50 +02:00
def get_public_keys(self, address):
return [self.get_public_key(address)]
2019-10-23 17:09:41 +02:00
def get_public_keys_with_deriv_info(self, address: str) -> Dict[str, Tuple[KeyStore, Sequence[int]]]:
"""Returns a map: pubkey_hex -> (keystore, derivation_suffix)"""
return {}
2014-04-30 11:18:13 +02:00
def is_found(self):
return True
#return self.history.values() != [[]] * len(self.history)
2013-02-27 09:04:22 +01:00
def get_tx_info(self, tx) -> TxWalletDetails:
2016-06-08 11:06:51 +02:00
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 = ''
tx_hash = tx.txid()
tx_mined_status = self.get_tx_height(tx_hash)
2016-06-08 11:06:51 +02:00
if tx.is_complete():
if self.db.get_transaction(tx_hash):
2016-06-08 11:06:51 +02:00
label = self.get_label(tx_hash)
if tx_mined_status.height > 0:
if tx_mined_status.conf:
status = _("{} confirmations").format(tx_mined_status.conf)
2016-06-08 11:06:51 +02:00
else:
status = _('Not verified')
elif tx_mined_status.height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED):
2016-06-08 11:06:51 +02:00
status = _('Unconfirmed')
if fee is None:
2019-09-12 13:12:48 +02:00
fee = self.get_tx_fee(tx_hash)
if fee and self.network and self.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.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
can_bump = is_mine and not tx.is_final()
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
return TxWalletDetails(
txid=tx_hash,
status=status,
label=label,
can_broadcast=can_broadcast,
can_bump=can_bump,
amount=amount,
fee=fee,
tx_mined_status=tx_mined_status,
mempool_depth_bytes=exp_n,
)
2016-06-08 11:06:51 +02:00
2019-10-23 17:09:41 +02:00
def get_spendable_coins(self, domain, *, nonlocal_only=False) -> Sequence[PartialTxInput]:
confirmed_only = self.config.get('confirmed_only', False)
2019-02-24 08:41:00 +01:00
utxos = self.get_utxos(domain,
excluded_addresses=self.frozen_addresses,
mature_only=True,
confirmed_only=confirmed_only,
nonlocal_only=nonlocal_only)
utxos = [utxo for utxo in utxos if not self.is_frozen_coin(utxo)]
return utxos
2019-10-23 17:09:41 +02:00
def get_receiving_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence[str]:
raise NotImplementedError() # implemented by subclasses
2019-10-23 17:09:41 +02:00
def get_change_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence[str]:
raise NotImplementedError() # implemented by subclasses
2016-02-02 19:56:34 +01:00
def dummy_address(self):
# first receiving address
return self.get_receiving_addresses(slice_start=0, slice_stop=1)[0]
2016-02-02 19:56:34 +01:00
2013-04-12 14:29:11 +02:00
def get_frozen_balance(self):
2019-02-24 08:41:00 +01:00
if not self.frozen_coins: # shortcut
return self.get_balance(self.frozen_addresses)
c1, u1, x1 = self.get_balance()
c2, u2, x2 = self.get_balance(excluded_addresses=self.frozen_addresses,
excluded_coins=self.frozen_coins)
return c1-c2, u1-u2, x1-x2
2014-06-24 16:12:43 +03:00
2018-02-14 10:42:09 +01:00
def balance_at_timestamp(self, domain, target_timestamp):
2019-05-10 19:22:13 +02:00
# we assume that get_history returns items ordered by block height
# we also assume that block timestamps are monotonic (which is false...!)
2019-09-21 18:48:44 +02:00
h = self.get_history(domain=domain)
balance = 0
for hist_item in h:
balance = hist_item.balance
if hist_item.tx_mined_status.timestamp is None or hist_item.tx_mined_status.timestamp > target_timestamp:
return balance - hist_item.delta
2018-02-14 10:42:09 +01:00
# return last balance
return balance
2019-09-21 18:48:44 +02:00
def get_onchain_history(self, *, domain=None):
for hist_item in self.get_history(domain=domain):
2019-06-14 13:01:23 +02:00
yield {
2019-09-12 08:58:58 +02:00
'txid': hist_item.txid,
'fee_sat': hist_item.fee,
'height': hist_item.tx_mined_status.height,
'confirmations': hist_item.tx_mined_status.conf,
'timestamp': hist_item.tx_mined_status.timestamp,
'incoming': True if hist_item.delta>0 else False,
'bc_value': Satoshis(hist_item.delta),
'bc_balance': Satoshis(hist_item.balance),
'date': timestamp_to_datetime(hist_item.tx_mined_status.timestamp),
'label': self.get_label(hist_item.txid),
'txpos_in_block': hist_item.tx_mined_status.txpos,
2019-06-14 13:01:23 +02:00
}
2019-10-23 17:09:41 +02:00
def create_invoice(self, outputs: List[PartialTxOutput], message, pr, URI):
if '!' in (x.value for x in outputs):
amount = '!'
else:
amount = sum(x.value for x in outputs)
2019-09-08 11:59:03 +02:00
invoice = {
'type': PR_TYPE_ONCHAIN,
'message': message,
'outputs': outputs,
'amount': amount,
}
if pr:
invoice['bip70'] = pr.raw.hex()
invoice['time'] = pr.get_time()
invoice['exp'] = pr.get_expiration_date() - pr.get_time()
invoice['requestor'] = pr.get_requestor()
invoice['message'] = pr.get_memo()
elif URI:
timestamp = URI.get('time')
if timestamp: invoice['time'] = timestamp
exp = URI.get('exp')
if exp: invoice['exp'] = exp
if 'time' not in invoice:
invoice['time'] = int(time.time())
return invoice
2019-08-11 14:47:06 +02:00
def save_invoice(self, invoice):
invoice_type = invoice['type']
if invoice_type == PR_TYPE_LN:
2019-09-20 17:15:49 +02:00
key = invoice['rhash']
2019-09-08 11:59:03 +02:00
elif invoice_type == PR_TYPE_ONCHAIN:
key = bh2u(sha256(repr(invoice))[0:16])
invoice['id'] = key
invoice['txid'] = None
else:
raise Exception('Unsupported invoice type')
2019-09-20 17:15:49 +02:00
self.invoices[key] = invoice
self.storage.put('invoices', self.invoices)
self.storage.write()
2019-09-08 11:59:03 +02:00
def clear_invoices(self):
self.invoices = {}
self.storage.put('invoices', self.invoices)
self.storage.write()
2019-08-11 14:47:06 +02:00
def get_invoices(self):
out = [self.get_invoice(key) for key in self.invoices.keys()]
2019-10-23 05:30:16 +02:00
out = list(filter(None, out))
2019-08-11 14:47:06 +02:00
out.sort(key=operator.itemgetter('time'))
return out
def set_paid(self, key, txid):
if key not in self.invoices:
return
invoice = self.invoices[key]
assert invoice.get('type') == PR_TYPE_ONCHAIN
invoice['txid'] = txid
self.storage.put('invoices', self.invoices)
2019-09-20 17:15:49 +02:00
2019-08-11 14:47:06 +02:00
def get_invoice(self, key):
2019-09-20 17:15:49 +02:00
if key not in self.invoices:
return
item = copy.copy(self.invoices[key])
request_type = item.get('type')
if request_type == PR_TYPE_ONCHAIN:
item['status'] = PR_PAID if item.get('txid') is not None else PR_UNPAID
2019-10-07 05:29:34 +02:00
elif self.lnworker and request_type == PR_TYPE_LN:
item['status'] = self.lnworker.get_payment_status(bfh(item['rhash']))
2019-09-20 17:15:49 +02:00
else:
return
return item
2019-08-11 14:47:06 +02:00
2019-06-14 13:01:23 +02:00
@profiler
2019-09-21 18:48:44 +02:00
def get_full_history(self, fx=None, *, onchain_domain=None, include_lightning=True):
2019-06-14 13:01:23 +02:00
transactions = OrderedDictWithIndex()
2019-09-21 18:48:44 +02:00
onchain_history = self.get_onchain_history(domain=onchain_domain)
2019-06-14 13:01:23 +02:00
for tx_item in onchain_history:
txid = tx_item['txid']
transactions[txid] = tx_item
2019-09-21 18:48:44 +02:00
if self.lnworker and include_lightning:
lightning_history = self.lnworker.get_history()
else:
lightning_history = []
2019-06-14 13:01:23 +02:00
for i, tx_item in enumerate(lightning_history):
txid = tx_item.get('txid')
ln_value = Decimal(tx_item['amount_msat']) / 1000
if txid and txid in transactions:
item = transactions[txid]
item['label'] = tx_item['label']
item['ln_value'] = Satoshis(ln_value)
item['ln_balance_msat'] = tx_item['balance_msat']
2019-06-14 13:01:23 +02:00
else:
tx_item['lightning'] = True
tx_item['ln_value'] = Satoshis(ln_value)
tx_item['txpos'] = i # for sorting
key = tx_item['payment_hash'] if 'payment_hash' in tx_item else tx_item['type'] + tx_item['channel_id']
transactions[key] = tx_item
now = time.time()
balance = 0
2019-06-14 13:01:23 +02:00
for item in transactions.values():
# add on-chain and lightning values
value = Decimal(0)
if item.get('bc_value'):
value += item['bc_value'].value
if item.get('ln_value'):
value += item.get('ln_value').value
item['value'] = Satoshis(value)
balance += value
item['balance'] = Satoshis(balance)
2019-06-14 13:01:23 +02:00
if fx:
timestamp = item['timestamp'] or now
fiat_value = value / Decimal(bitcoin.COIN) * fx.timestamp_rate(timestamp)
item['fiat_value'] = Fiat(fiat_value, fx.ccy)
item['fiat_default'] = True
return transactions
@profiler
2019-06-14 13:01:23 +02:00
def get_detailed_history(self, from_timestamp=None, to_timestamp=None,
fx=None, show_addresses=False):
2019-06-14 13:01:23 +02:00
# History with capital gains, using utxo pricing
# FIXME: Lightning capital gains would requires FIFO
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)
now = time.time()
2019-06-14 13:01:23 +02:00
for item in self.get_onchain_history():
timestamp = item['timestamp']
if from_timestamp and (timestamp or now) < from_timestamp:
continue
if to_timestamp and (timestamp or now) >= to_timestamp:
continue
2019-06-14 13:01:23 +02:00
tx_hash = item['txid']
tx = self.db.get_transaction(tx_hash)
tx_fee = item['fee_sat']
item['fee'] = Satoshis(tx_fee) if tx_fee is not None else None
if show_addresses:
2019-10-23 17:09:41 +02:00
item['inputs'] = list(map(lambda x: x.to_json(), tx.inputs()))
item['outputs'] = list(map(lambda x: {'address': x.get_ui_address_str(), 'value': Satoshis(x.value)},
tx.outputs()))
# fixme: use in and out values
2019-06-14 13:01:23 +02:00
value = item['bc_value'].value
if value < 0:
expenditures += -value
else:
income += value
# fiat computations
if fx and fx.is_enabled() and fx.get_history_config():
2018-11-26 21:21:02 +01:00
fiat_fields = self.get_tx_item_fiat(tx_hash, value, fx, tx_fee)
fiat_value = fiat_fields['fiat_value'].value
item.update(fiat_fields)
if value < 0:
2018-11-26 21:21:02 +01:00
capital_gains += fiat_fields['capital_gain'].value
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:
2019-09-02 16:24:08 +03:00
b, v = out[0]['bc_balance'].value, out[0]['bc_value'].value
2018-02-21 21:09:07 +01:00
start_balance = None if b is None or v is None else b - v
2019-09-02 16:24:08 +03:00
end_balance = out[-1]['bc_balance'].value
2018-02-21 11:52:40 +01:00
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),
'incoming': Satoshis(income),
'outgoing': Satoshis(expenditures)
2018-02-21 11:52:40 +01:00
}
if fx and fx.is_enabled() and fx.get_history_config():
2019-06-14 13:01:23 +02:00
unrealized = self.unrealized_gains(None, fx.timestamp_rate, fx.ccy)
summary['fiat_currency'] = fx.ccy
summary['fiat_capital_gains'] = Fiat(capital_gains, fx.ccy)
summary['fiat_incoming'] = Fiat(fiat_income, fx.ccy)
summary['fiat_outgoing'] = Fiat(fiat_expenditures, fx.ccy)
summary['fiat_unrealized_gains'] = Fiat(unrealized, fx.ccy)
summary['fiat_start_balance'] = Fiat(fx.historical_value(start_balance, start_date), fx.ccy)
summary['fiat_end_balance'] = Fiat(fx.historical_value(end_balance, end_date), fx.ccy)
summary['fiat_start_value'] = Fiat(fx.historical_value(COIN, start_date), fx.ccy)
summary['fiat_end_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
}
2018-11-27 21:15:31 +01:00
def default_fiat_value(self, tx_hash, fx, value_sat):
return value_sat / Decimal(COIN) * self.price_at_timestamp(tx_hash, fx.timestamp_rate)
2018-11-26 21:21:02 +01:00
def get_tx_item_fiat(self, tx_hash, value, fx, tx_fee):
item = {}
fiat_value = self.get_fiat_value(tx_hash, fx.ccy)
fiat_default = fiat_value is None
fiat_rate = self.price_at_timestamp(tx_hash, fx.timestamp_rate)
fiat_value = fiat_value if fiat_value is not None else self.default_fiat_value(tx_hash, fx, value)
fiat_fee = tx_fee / Decimal(COIN) * fiat_rate if tx_fee is not None else None
item['fiat_currency'] = fx.ccy
2019-02-05 18:27:01 +01:00
item['fiat_rate'] = Fiat(fiat_rate, fx.ccy)
2018-11-26 21:21:02 +01:00
item['fiat_value'] = Fiat(fiat_value, fx.ccy)
item['fiat_fee'] = Fiat(fiat_fee, fx.ccy) if fiat_fee else None
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
item['acquisition_price'] = Fiat(acquisition_price, fx.ccy)
cg = liquidation_price - acquisition_price
item['capital_gain'] = Fiat(cg, fx.ccy)
return item
2019-10-23 17:09:41 +02:00
def get_label(self, tx_hash: str) -> str:
return self.labels.get(tx_hash, '') or self.get_default_label(tx_hash)
2019-10-23 17:09:41 +02:00
def get_default_label(self, tx_hash) -> str:
2019-09-11 21:47:44 +02:00
if not self.db.get_txi_addresses(tx_hash):
2016-01-04 15:40:57 +01:00
labels = []
2019-09-11 21:47:44 +02:00
for addr in self.db.get_txo_addresses(tx_hash):
2016-01-04 15:40:57 +01:00
label = self.labels.get(addr)
if label:
labels.append(label)
return ', '.join(labels)
return ''
def get_tx_status(self, tx_hash, tx_mined_info: TxMinedInfo):
2018-02-27 15:13:44 +01:00
extra = []
height = tx_mined_info.height
conf = tx_mined_info.conf
timestamp = tx_mined_info.timestamp
if height == TX_HEIGHT_FUTURE:
return 2, 'in %d blocks'%conf
if conf == 0:
tx = self.db.get_transaction(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')
2019-09-12 13:12:48 +02:00
fee = self.get_tx_fee(tx_hash)
2018-02-27 15:13:44 +01:00
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.config.has_fee_mempool():
exp_n = self.config.fee_to_depth(fee_per_byte)
2018-02-27 15:13:44 +01:00
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:
status = 2 # not SPV verified
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
2018-11-09 17:56:42 +01:00
def get_unconfirmed_base_tx_for_batching(self) -> Optional[Transaction]:
candidate = None
for hist_item in self.get_history():
2018-11-09 17:56:42 +01:00
# tx should not be mined yet
if hist_item.tx_mined_status.conf > 0: continue
# conservative future proofing of code: only allow known unconfirmed types
if hist_item.tx_mined_status.height not in (TX_HEIGHT_UNCONFIRMED,
TX_HEIGHT_UNCONF_PARENT,
TX_HEIGHT_LOCAL):
continue
2018-11-09 17:56:42 +01:00
# tx should be "outgoing" from wallet
if hist_item.delta >= 0:
continue
tx = self.db.get_transaction(hist_item.txid)
if not tx:
continue
2018-11-09 17:56:42 +01:00
# is_mine outputs should not be spent yet
# to avoid cancelling our own dependent transactions
2018-11-09 22:47:41 +01:00
txid = tx.txid()
2019-03-02 19:16:39 +01:00
if any([self.is_mine(o.address) and self.db.get_spent_outpoint(txid, output_idx)
2018-11-09 22:47:41 +01:00
for output_idx, o in enumerate(tx.outputs())]):
continue
2018-11-09 20:04:06 +01:00
# all inputs should be is_mine
if not all([self.is_mine(self.get_txin_address(txin)) for txin in tx.inputs()]):
continue
2018-11-09 17:56:42 +01:00
# prefer txns already in mempool (vs local)
if hist_item.tx_mined_status.height == TX_HEIGHT_LOCAL:
2018-11-09 17:56:42 +01:00
candidate = tx
continue
# tx must have opted-in for RBF
if tx.is_final(): continue
return tx
return candidate
def get_change_addresses_for_new_transaction(self, preferred_change_addr=None) -> List[str]:
change_addrs = []
if preferred_change_addr:
if isinstance(preferred_change_addr, (list, tuple)):
change_addrs = list(preferred_change_addr)
else:
change_addrs = [preferred_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
else:
# if there are none, take one randomly from the last few
addrs = self.get_change_addresses(slice_start=-self.gap_limit_for_change)
change_addrs = [random.choice(addrs)] if addrs else []
for addr in change_addrs:
assert is_address(addr), f"not valid bitcoin address: {addr}"
# note that change addresses are not necessarily ismine
# in which case this is a no-op
self.check_address(addr)
max_change = self.max_change_outputs if self.multiple_change else 1
return change_addrs[:max_change]
2019-10-23 17:09:41 +02:00
def make_unsigned_transaction(self, *, coins: Sequence[PartialTxInput],
outputs: List[PartialTxOutput], fee=None,
change_addr: str = None, is_sweep=False) -> PartialTransaction:
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.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
2019-10-23 17:09:41 +02:00
if fee is None and self.config.fee_per_kb() is None:
2017-12-30 01:10:20 +01:00
raise NoDynamicFeeEstimates()
for item in coins:
2018-06-14 15:11:20 +02:00
self.add_input_info(item)
2015-11-29 12:41:54 +09:00
# Fee estimator
2019-10-23 17:09:41 +02:00
if fee is None:
fee_estimator = self.config.estimate_fee
2019-10-23 17:09:41 +02:00
elif isinstance(fee, Number):
fee_estimator = lambda size: fee
elif callable(fee):
fee_estimator = fee
2017-12-18 22:26:29 +01:00
else:
2019-10-23 17:09:41 +02:00
raise Exception(f'Invalid argument fee: {fee}')
2016-12-31 16:29:18 +01:00
if i_max is None:
# Let the coin chooser select the coins to spend
coin_chooser = coinchooser.get_coin_chooser(self.config)
# If there is an unconfirmed RBF tx, merge with it
2018-11-09 17:56:42 +01:00
base_tx = self.get_unconfirmed_base_tx_for_batching()
if self.config.get('batch_rbf', False) and base_tx:
2019-06-20 21:53:24 +02:00
# make sure we don't try to spend change from the tx-to-be-replaced:
2019-10-23 17:09:41 +02:00
coins = [c for c in coins if c.prevout.txid.hex() != base_tx.txid()]
2018-11-09 20:04:06 +01:00
is_local = self.get_tx_height(base_tx.txid()).height == TX_HEIGHT_LOCAL
2019-10-23 17:09:41 +02:00
base_tx = PartialTransaction.from_tx(base_tx)
base_tx.add_info_from_wallet(self)
2018-11-09 20:04:06 +01:00
base_tx_fee = base_tx.get_fee()
relayfeerate = Decimal(self.relayfee()) / 1000
2018-11-09 20:04:06 +01:00
original_fee_estimator = fee_estimator
def fee_estimator(size: Union[int, float, Decimal]) -> int:
size = Decimal(size)
2018-11-09 20:04:06 +01:00
lower_bound = base_tx_fee + round(size * relayfeerate)
lower_bound = lower_bound if not is_local else 0
return int(max(lower_bound, original_fee_estimator(size)))
txi = base_tx.inputs()
2018-11-09 17:56:42 +01:00
txo = list(filter(lambda o: not self.is_change(o.address), base_tx.outputs()))
old_change_addrs = [o.address for o in base_tx.outputs() if self.is_change(o.address)]
else:
txi = []
txo = []
old_change_addrs = []
# change address. if empty, coin_chooser will set it
change_addrs = self.get_change_addresses_for_new_transaction(change_addr or old_change_addrs)
2019-10-23 17:09:41 +02:00
tx = coin_chooser.make_tx(coins=coins,
inputs=txi,
outputs=list(outputs) + txo,
change_addrs=change_addrs,
fee_estimator_vb=fee_estimator,
dust_threshold=self.dust_threshold())
2016-12-31 16:29:18 +01:00
else:
# "spend max" branch
# note: This *will* spend inputs with negative effective value (if there are any).
# Given as the user is spending "max", and so might be abandoning the wallet,
# try to include all UTXOs, otherwise leftover might remain in the UTXO set
# forever. see #5433
# note: Actually it might be the case that not all UTXOs from the wallet are
# being spent if the user manually selected UTXOs.
2019-10-23 17:09:41 +02:00
sendable = sum(map(lambda c: c.value_sats(), coins))
outputs[i_max].value = 0
tx = PartialTransaction.from_io(list(coins), list(outputs))
2016-12-31 16:29:18 +01:00
fee = fee_estimator(tx.estimated_size())
amount = sendable - tx.output_value() - fee
if amount < 0:
raise NotEnoughFunds()
2019-10-23 17:09:41 +02:00
outputs[i_max].value = amount
tx = PartialTransaction.from_io(list(coins), list(outputs))
2014-09-07 18:45:06 +02:00
2017-04-19 11:55:31 +02:00
# Timelock tx to current height.
tx.locktime = get_locktime_for_new_transaction(self.network)
2019-10-23 17:09:41 +02:00
tx.add_info_from_wallet(self)
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
2019-10-23 17:09:41 +02:00
def mktx(self, *, outputs: List[PartialTxOutput], password, fee=None, change_addr=None,
domain=None, rbf=False, nonlocal_only=False, tx_version=None) -> PartialTransaction:
coins = self.get_spendable_coins(domain, nonlocal_only=nonlocal_only)
2019-10-23 17:09:41 +02:00
tx = self.make_unsigned_transaction(coins=coins,
outputs=outputs,
fee=fee,
change_addr=change_addr)
tx.set_rbf(rbf)
2019-01-28 15:11:03 +01:00
if tx_version is not None:
tx.version = tx_version
self.sign_transaction(tx, password)
2013-09-04 10:33:14 +02:00
return tx
2019-02-24 08:41:00 +01:00
def is_frozen_address(self, addr: str) -> bool:
return addr in self.frozen_addresses
2019-10-23 17:09:41 +02:00
def is_frozen_coin(self, utxo: PartialTxInput) -> bool:
prevout_str = utxo.prevout.to_str()
return prevout_str in self.frozen_coins
2019-02-24 08:41:00 +01:00
def set_frozen_state_of_addresses(self, addrs, freeze: bool):
"""Set frozen state of the addresses to FREEZE, True or False"""
if all(self.is_mine(addr) for addr in addrs):
2019-02-24 08:41:00 +01:00
# FIXME take lock?
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
2019-10-23 17:09:41 +02:00
def set_frozen_state_of_coins(self, utxos: Sequence[PartialTxInput], freeze: bool):
2019-02-24 08:41:00 +01:00
"""Set frozen state of the utxos to FREEZE, True or False"""
2019-10-23 17:09:41 +02:00
utxos = {utxo.prevout.to_str() for utxo in utxos}
2019-02-24 08:41:00 +01:00
# FIXME take lock?
if freeze:
self.frozen_coins |= set(utxos)
else:
self.frozen_coins -= set(utxos)
self.storage.put('frozen_coins', list(self.frozen_coins))
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:
2019-04-26 18:52:26 +02:00
self.logger.info("waiting for network...")
2015-10-28 10:36:44 +01:00
wait_for_network()
2019-04-26 18:52:26 +02:00
self.logger.info("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')
2019-09-09 01:34:29 +02:00
def address_is_old(self, address: str, *, req_conf: int = 3) -> bool:
"""Returns whether address has any history that is deeply confirmed."""
2019-09-09 01:34:29 +02:00
max_conf = -1
h = self.db.get_addr_history(address)
needs_spv_check = not self.config.get("skipmerklecheck", False)
2014-06-24 16:47:58 +02:00
for tx_hash, tx_height in h:
if needs_spv_check:
tx_age = self.get_tx_height(tx_hash).conf
2014-06-24 16:47:58 +02:00
else:
if tx_height <= 0:
tx_age = 0
else:
tx_age = self.get_local_height() - tx_height + 1
2019-09-09 01:34:29 +02:00
max_conf = max(max_conf, tx_age)
return max_conf >= req_conf
2014-06-24 16:47:58 +02:00
2019-10-23 17:09:41 +02:00
def bump_fee(self, *, tx: Transaction, new_fee_rate) -> PartialTransaction:
"""Increase the miner fee of 'tx'.
'new_fee_rate' is the target min rate in sat/vbyte
"""
2016-05-20 10:38:48 +02:00
if tx.is_final():
2018-06-14 22:36:54 +02:00
raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('transaction is final'))
new_fee_rate = quantize_feerate(new_fee_rate) # strip excess precision
old_tx_size = tx.estimated_size()
old_txid = tx.txid()
assert old_txid
2019-09-12 12:26:49 +02:00
old_fee = self.get_tx_fee(old_txid)
if old_fee is None:
raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('current fee unknown'))
old_fee_rate = old_fee / old_tx_size # sat/vbyte
if new_fee_rate <= old_fee_rate:
raise CannotBumpFee(_('Cannot bump fee') + ': ' + _("The new fee rate needs to be higher than the old fee rate."))
try:
# method 1: keep all inputs, keep all not is_mine outputs,
# allow adding new inputs
tx_new = self._bump_fee_through_coinchooser(
tx=tx, new_fee_rate=new_fee_rate)
method_used = 1
except CannotBumpFee:
# method 2: keep all inputs, no new inputs are added,
# allow decreasing and removing outputs (change is decreased first)
# This is less "safe" as it might end up decreasing e.g. a payment to a merchant;
# but e.g. if the user has sent "Max" previously, this is the only way to RBF.
tx_new = self._bump_fee_through_decreasing_outputs(
tx=tx, new_fee_rate=new_fee_rate)
method_used = 2
target_min_fee = new_fee_rate * tx_new.estimated_size()
actual_fee = tx_new.get_fee()
if actual_fee + 1 < target_min_fee:
raise Exception(f"bump_fee fee target was not met (method: {method_used}). "
f"got {actual_fee}, expected >={target_min_fee}. "
f"target rate was {new_fee_rate}")
tx_new.locktime = get_locktime_for_new_transaction(self.network)
return tx_new
2019-10-23 17:09:41 +02:00
def _bump_fee_through_coinchooser(self, *, tx: Transaction, new_fee_rate) -> PartialTransaction:
tx = PartialTransaction.from_tx(tx)
tx.add_info_from_wallet(self)
old_inputs = list(tx.inputs())
old_outputs = list(tx.outputs())
# change address
old_change_addrs = [o.address for o in old_outputs if self.is_change(o.address)]
change_addrs = self.get_change_addresses_for_new_transaction(old_change_addrs)
# which outputs to keep?
if old_change_addrs:
fixed_outputs = list(filter(lambda o: not self.is_change(o.address), old_outputs))
else:
if all(self.is_mine(o.address) for o in old_outputs):
# all outputs are is_mine and none of them are change.
# we bail out as it's unclear what the user would want!
# the coinchooser bump fee method is probably not a good idea in this case
raise CannotBumpFee(_('Cannot bump fee') + ': all outputs are non-change is_mine')
old_not_is_mine = list(filter(lambda o: not self.is_mine(o.address), old_outputs))
if old_not_is_mine:
fixed_outputs = old_not_is_mine
else:
fixed_outputs = old_outputs
if not fixed_outputs:
raise CannotBumpFee(_('Cannot bump fee') + ': could not figure out which outputs to keep')
coins = self.get_spendable_coins(None)
for item in coins:
self.add_input_info(item)
def fee_estimator(size):
return self.config.estimate_fee_for_feerate(fee_per_kb=new_fee_rate*1000, size=size)
coin_chooser = coinchooser.get_coin_chooser(self.config)
try:
2019-10-23 17:09:41 +02:00
return coin_chooser.make_tx(coins=coins,
inputs=old_inputs,
outputs=fixed_outputs,
change_addrs=change_addrs,
fee_estimator_vb=fee_estimator,
dust_threshold=self.dust_threshold())
except NotEnoughFunds as e:
raise CannotBumpFee(e)
2019-10-23 17:09:41 +02:00
def _bump_fee_through_decreasing_outputs(self, *, tx: Transaction, new_fee_rate) -> PartialTransaction:
tx = PartialTransaction.from_tx(tx)
tx.add_info_from_wallet(self)
inputs = tx.inputs()
2019-10-23 17:09:41 +02:00
outputs = list(tx.outputs())
2016-10-21 12:59:55 +02:00
# use own outputs
2019-06-19 21:59:49 +02:00
s = list(filter(lambda o: self.is_mine(o.address), 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
2019-06-19 21:59:49 +02:00
s = filter(lambda o: o.address != x_fee_address, s)
if not s:
raise CannotBumpFee(_('Cannot bump fee') + ': no outputs at all??')
2017-07-06 16:03:21 +02:00
2016-10-21 12:59:55 +02:00
# prioritize low value outputs, to get rid of dust
2019-06-19 21:59:49 +02:00
s = sorted(s, key=lambda o: o.value)
2016-10-21 12:59:55 +02:00
for o in s:
target_fee = int(round(tx.estimated_size() * new_fee_rate))
delta = target_fee - tx.get_fee()
2016-10-21 12:59:55 +02:00
i = outputs.index(o)
2018-07-31 18:24:37 +02:00
if o.value - delta >= self.dust_threshold():
new_output_value = o.value - delta
assert isinstance(new_output_value, int)
2019-10-23 17:09:41 +02:00
outputs[i].value = new_output_value
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
# note: delta might be negative now, in which case
# the value of the next output will be increased
2016-10-21 12:59:55 +02:00
if delta > 0:
2018-06-14 22:36:54 +02:00
raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('could not find suitable outputs'))
2019-10-23 17:09:41 +02:00
return PartialTransaction.from_io(inputs, outputs)
2016-05-20 10:38:48 +02:00
2019-10-23 17:09:41 +02:00
def cpfp(self, tx: Transaction, fee: int) -> Optional[PartialTransaction]:
txid = tx.txid()
for i, o in enumerate(tx.outputs()):
2018-07-31 18:24:37 +02:00
address, value = o.address, o.value
2019-10-23 17:09:41 +02:00
if self.is_mine(address):
break
else:
return
coins = self.get_addr_utxo(address)
2019-10-23 17:09:41 +02:00
item = coins.get(TxOutpoint.from_str(txid+':%d'%i))
2017-10-07 12:52:52 +02:00
if not item:
return
self.add_input_info(item)
inputs = [item]
2018-11-07 14:48:33 +01:00
out_address = self.get_unused_address() or address
2019-10-23 17:09:41 +02:00
outputs = [PartialTxOutput.from_address_and_value(out_address, value - fee)]
locktime = get_locktime_for_new_transaction(self.network)
2019-10-23 17:09:41 +02:00
return PartialTransaction.from_io(inputs, outputs, locktime=locktime)
2019-10-23 17:09:41 +02:00
def _add_input_sig_info(self, txin: PartialTxInput, address: str) -> None:
raise NotImplementedError() # implemented by subclasses
2019-10-23 17:09:41 +02:00
def _add_input_utxo_info(self, txin: PartialTxInput, address: str) -> None:
if Transaction.is_segwit_input(txin):
if txin.witness_utxo is None:
received, spent = self.get_addr_io(address)
2019-10-23 17:09:41 +02:00
item = received.get(txin.prevout.to_str())
if item:
2019-10-23 17:09:41 +02:00
txin_value = item[1]
txin_value_bytes = txin_value.to_bytes(8, byteorder="little", signed=True)
txin.witness_utxo = txin_value_bytes + bfh(bitcoin.address_to_script(address))
else: # legacy input
if txin.utxo is None:
# note: for hw wallets, for legacy inputs, ignore_network_issues used to be False
txin.utxo = self.get_input_tx(txin.prevout.txid.hex(), ignore_network_issues=True)
txin.ensure_there_is_only_one_utxo()
def _learn_derivation_path_for_address_from_txinout(self, txinout: Union[PartialTxInput, PartialTxOutput],
address: str) -> bool:
"""Tries to learn the derivation path for an address (potentially beyond gap limit)
using data available in given txin/txout.
Returns whether the address was found to be is_mine.
"""
return False # implemented by subclasses
def add_input_info(self, txin: PartialTxInput) -> None:
address = self.get_txin_address(txin)
if not self.is_mine(address):
is_mine = self._learn_derivation_path_for_address_from_txinout(txin, address)
if not is_mine:
return
# set script_type first, as later checks might rely on it:
txin.script_type = self.get_txin_type(address)
self._add_input_utxo_info(txin, address)
txin.num_sig = self.m if isinstance(self, Multisig_Wallet) else 1
if txin.redeem_script is None:
try:
redeem_script_hex = self.get_redeem_script(address)
txin.redeem_script = bfh(redeem_script_hex) if redeem_script_hex else None
except UnknownTxinType:
pass
if txin.witness_script is None:
try:
witness_script_hex = self.get_witness_script(address)
txin.witness_script = bfh(witness_script_hex) if witness_script_hex else None
except UnknownTxinType:
pass
self._add_input_sig_info(txin, address)
2019-09-11 21:47:44 +02:00
def can_sign(self, tx: Transaction) -> bool:
2019-10-23 17:09:41 +02:00
if not isinstance(tx, PartialTransaction):
return False
2016-07-02 08:58:56 +02:00
if tx.is_complete():
return False
# add info to inputs if we can; otherwise we might return a false negative:
2019-10-23 17:09:41 +02:00
tx.add_info_from_wallet(self)
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
2019-09-03 14:20:00 +02:00
def get_input_tx(self, tx_hash, *, ignore_network_issues=False) -> Optional[Transaction]:
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.
tx = self.db.get_transaction(tx_hash)
2016-09-01 13:52:49 +02:00
if not tx and self.network:
2018-03-03 02:39:49 +01:00
try:
2018-10-30 19:07:37 +01:00
raw_tx = self.network.run_from_another_thread(
self.network.get_transaction(tx_hash, timeout=10))
except NetworkException as e:
self.logger.info(f'got network error getting input txn. err: {repr(e)}. txid: {tx_hash}. '
f'if you are intentionally offline, consider using the --offline flag')
if not ignore_network_issues:
2018-03-03 02:39:49 +01:00
raise e
2018-10-30 19:07:37 +01:00
else:
tx = Transaction(raw_tx)
2016-07-02 08:58:56 +02:00
return tx
2014-04-30 11:18:13 +02:00
2019-10-23 17:09:41 +02:00
def add_output_info(self, txout: PartialTxOutput) -> None:
address = txout.address
if not self.is_mine(address):
is_mine = self._learn_derivation_path_for_address_from_txinout(txout, address)
if not is_mine:
return
txout.script_type = self.get_txin_type(address)
txout.is_mine = True
txout.is_change = self.is_change(address)
if isinstance(self, Multisig_Wallet):
txout.num_sig = self.m
if isinstance(self, Deterministic_Wallet):
if not txout.pubkeys or len(txout.pubkeys) != len(txout.bip32_paths):
pubkey_deriv_info = self.get_public_keys_with_deriv_info(address)
txout.pubkeys = sorted([bfh(pk) for pk in list(pubkey_deriv_info)])
for pubkey_hex in pubkey_deriv_info:
ks, der_suffix = pubkey_deriv_info[pubkey_hex]
xfp_bytes = bfh(ks.get_root_fingerprint())
der_prefix = bip32.convert_bip32_path_to_list_of_uint32(ks.get_derivation_prefix())
der_full = der_prefix + list(der_suffix)
txout.bip32_paths[bfh(pubkey_hex)] = (xfp_bytes, der_full)
if txout.redeem_script is None:
try:
redeem_script_hex = self.get_redeem_script(address)
txout.redeem_script = bfh(redeem_script_hex) if redeem_script_hex else None
except UnknownTxinType:
pass
if txout.witness_script is None:
try:
witness_script_hex = self.get_witness_script(address)
txout.witness_script = bfh(witness_script_hex) if witness_script_hex else None
except UnknownTxinType:
pass
def sign_transaction(self, tx: Transaction, password) -> Optional[PartialTransaction]:
if self.is_watching_only():
return
2019-10-23 17:09:41 +02:00
if not isinstance(tx, PartialTransaction):
return
tx.add_info_from_wallet(self)
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 try_detecting_internal_addresses_corruption(self):
pass
2018-12-13 12:10:01 +01:00
def check_address(self, addr):
pass
2018-12-13 12:10:01 +01:00
def check_returned_address(func):
def wrapper(self, *args, **kwargs):
addr = func(self, *args, **kwargs)
2018-12-13 12:10:01 +01:00
self.check_address(addr)
return addr
return wrapper
2016-07-02 08:58:56 +02:00
def get_unused_addresses(self):
domain = self.get_receiving_addresses()
2019-06-10 14:05:02 +02:00
in_use = [k for k in self.receive_requests.keys() if self.get_request_status(k)[0] != PR_EXPIRED]
return [addr for addr in domain if not self.db.get_addr_history(addr)
2019-06-10 14:05:02 +02:00
and addr not in in_use]
2018-12-13 12:10:01 +01:00
@check_returned_address
2016-07-02 08:58:56 +02:00
def get_unused_address(self):
addrs = self.get_unused_addresses()
if addrs:
return addrs[0]
2018-12-13 12:10:01 +01:00
@check_returned_address
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:
if not self.db.get_addr_history(addr):
if addr not in self.receive_requests.keys():
return addr
else:
choice = addr
return choice
def create_new_address(self, for_change=False):
raise Exception("this wallet cannot generate new addresses")
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.db.get_verified_tx(txid)
if info:
conf = local_height - info.height + 1
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
2019-06-10 14:05:02 +02:00
def get_request_URI(self, addr):
req = self.receive_requests[addr]
message = self.labels.get(addr, '')
amount = req['amount']
extra_query_params = {}
if req.get('time'):
extra_query_params['time'] = str(int(req.get('time')))
if req.get('exp'):
extra_query_params['exp'] = str(int(req.get('exp')))
if req.get('name') and req.get('sig'):
sig = bfh(req.get('sig'))
sig = bitcoin.base_encode(sig, base=58)
extra_query_params['name'] = req['name']
extra_query_params['sig'] = sig
uri = create_bip21_uri(addr, amount, message, extra_query_params=extra_query_params)
return str(uri)
2019-09-20 17:15:49 +02:00
def get_request_status(self, address):
r = self.receive_requests.get(address)
if r is None:
return PR_UNKNOWN
amount = r.get('amount', 0) or 0
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
2019-06-10 14:05:02 +02:00
paid, conf = self.get_payment_status(address, amount)
if not paid:
if expiration is not None and time.time() > timestamp + expiration:
status = PR_EXPIRED
else:
status = PR_UNPAID
else:
status = PR_PAID
return status, conf
2015-06-02 11:36:06 +02:00
def get_request(self, key):
2019-09-20 17:15:49 +02:00
req = self.receive_requests.get(key)
if not req:
return
2019-09-20 17:15:49 +02:00
req = copy.copy(req)
2019-10-07 17:24:49 +02:00
_type = req.get('type')
if _type == PR_TYPE_ONCHAIN:
2019-09-20 17:15:49 +02:00
addr = req['address']
req['URI'] = self.get_request_URI(addr)
status, conf = self.get_request_status(addr)
req['status'] = status
if conf is not None:
req['confirmations'] = conf
2019-10-07 17:24:49 +02:00
elif self.lnworker and _type == PR_TYPE_LN:
req['status'] = self.lnworker.get_payment_status(bfh(key))
2019-09-20 17:15:49 +02:00
else:
return
# add URL if we are running a payserver
2019-09-22 17:32:22 +02:00
if self.config.get('run_payserver'):
host = self.config.get('payserver_host', 'localhost')
2019-09-22 17:32:22 +02:00
port = self.config.get('payserver_port', 8002)
root = self.config.get('payserver_root', '/r')
use_ssl = bool(self.config.get('ssl_keyfile'))
protocol = 'https' if use_ssl else 'http'
base = '%s://%s:%d'%(protocol, host, port)
req['view_url'] = base + root + '/pay?id=' + key
if use_ssl and 'URI' in req:
request_url = base + '/bip70/' + key + '.bip70'
req['bip70_url'] = request_url
return req
2019-06-10 14:05:02 +02:00
def receive_tx_callback(self, tx_hash, tx, tx_height):
super().receive_tx_callback(tx_hash, tx, tx_height)
for txo in tx.outputs():
addr = self.get_txout_address(txo)
if addr in self.receive_requests:
status, conf = self.get_request_status(addr)
self.network.trigger_callback('payment_received', self, addr, status)
def make_payment_request(self, addr, amount, message, expiration):
2015-06-08 12:51:45 +02:00
timestamp = int(time.time())
2018-10-25 22:28:24 +02:00
_id = bh2u(sha256d(addr + "%d"%timestamp))[0:10]
2019-09-20 17:15:49 +02:00
return {
'type': PR_TYPE_ONCHAIN,
'time':timestamp,
'amount':amount,
'exp':expiration,
'address':addr,
'memo':message,
'id':_id,
2019-10-23 17:09:41 +02:00
'outputs': [PartialTxOutput.from_address_and_value(addr, amount)],
2019-09-20 17:15:49 +02:00
}
2015-07-22 09:06:03 +02:00
def sign_payment_request(self, key, alias, alias_addr, password):
req = self.receive_requests.get(key)
2019-10-23 17:09:41 +02:00
alias_privkey = self.export_private_key(alias_addr, password)
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):
2019-09-20 17:15:49 +02:00
if req['type'] == PR_TYPE_ONCHAIN:
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.'))
key = addr
message = req['memo']
elif req['type'] == PR_TYPE_LN:
key = req['rhash']
message = req['message']
else:
raise Exception('Unknown request type')
amount = req.get('amount')
2019-09-20 17:15:49 +02:00
self.receive_requests[key] = req
self.storage.put('payment_requests', self.receive_requests)
2019-09-20 17:15:49 +02:00
self.set_label(key, message) # should be a default label
2015-06-08 13:21:13 +02:00
return req
2015-06-02 09:18:39 +02:00
2019-08-22 18:48:52 +02:00
def delete_request(self, key):
""" lightning or on-chain """
if key in self.receive_requests:
self.remove_payment_request(key)
2019-08-22 18:48:52 +02:00
elif self.lnworker:
self.lnworker.delete_payment(key)
2019-08-22 18:48:52 +02:00
2019-08-11 14:47:06 +02:00
def delete_invoice(self, key):
""" lightning or on-chain """
if key in self.invoices:
self.invoices.pop(key)
self.storage.put('invoices', self.invoices)
elif self.lnworker:
self.lnworker.delete_payment(key)
2019-08-11 14:47:06 +02:00
def remove_payment_request(self, addr):
2015-06-02 09:18:39 +02:00
if addr not in self.receive_requests:
return False
self.receive_requests.pop(addr)
self.storage.put('payment_requests', self.receive_requests)
2015-06-02 09:18:39 +02:00
return True
def get_sorted_requests(self):
2019-06-10 14:05:02 +02:00
""" sorted by timestamp """
out = [self.get_request(x) for x in self.receive_requests.keys()]
2019-10-07 05:29:34 +02:00
out = [x for x in out if x is not None]
2019-06-10 14:05:02 +02:00
out.sort(key=operator.itemgetter('time'))
return out
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()
2019-09-04 13:31:49 +02:00
def get_available_storage_encryption_version(self) -> StorageEncryptionVersion:
"""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):
2019-09-04 13:31:49 +02:00
return StorageEncryptionVersion.XPUB_PASSWORD
else:
2019-09-04 13:31:49 +02:00
return StorageEncryptionVersion.USER_PASSWORD
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:
2019-09-04 13:31:49 +02:00
enc_version = StorageEncryptionVersion.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)
2019-05-03 03:10:31 +02:00
def decrypt_message(self, pubkey, message, password) -> bytes:
addr = self.pubkeys_to_address(pubkey)
index = self.get_address_index(addr)
return self.keystore.decrypt_message(index, message, password)
2019-10-23 17:09:41 +02:00
def txin_value(self, txin: TxInput) -> Optional[int]:
if isinstance(txin, PartialTxInput):
v = txin.value_sats()
if v: return v
txid = txin.prevout.txid.hex()
prev_n = txin.prevout.out_idx
2019-09-11 21:47:44 +02:00
for addr in self.db.get_txo_addresses(txid):
d = self.db.get_txo_addr(txid, addr)
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)
2019-10-23 17:09:41 +02:00
ap = sum(self.coin_price(coin.prevout.txid.hex(), price_func, ccy, self.txin_value(coin)) for coin in coins)
lp = sum([coin.value_sats() 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
2019-09-11 21:47:44 +02:00
for addr in self.db.get_txi_addresses(txid):
d = self.db.get_txi_addr(txid, addr)
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 clear_coin_price_cache(self):
self._coin_price_cache = {}
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
2019-09-11 21:47:44 +02:00
if self.db.get_txi_addresses(txid):
result = self.average_price(txid, price_func, ccy) * txin_value/Decimal(COIN)
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):
2019-09-03 16:24:05 +02:00
# overridden for TrustedCoin wallets
2018-07-02 14:23:14 +02:00
return False
def is_watching_only(self) -> bool:
raise NotImplementedError()
2019-09-03 16:24:05 +02:00
def get_keystore(self) -> Optional[KeyStore]:
return self.keystore
2016-07-02 08:58:56 +02:00
2019-09-03 16:24:05 +02:00
def get_keystores(self) -> Sequence[KeyStore]:
return [self.keystore] if self.keystore else []
2017-10-15 10:14:55 +02:00
2019-09-03 16:24:05 +02:00
class Simple_Wallet(Abstract_Wallet):
# wallet with a single keystore
2017-10-15 10:14:55 +02:00
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, *, config):
Abstract_Wallet.__init__(self, storage, config=config)
2016-07-02 08:58:56 +02:00
def is_watching_only(self):
return self.keystore is None
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
2019-03-04 12:53:04 +01:00
def save_keystore(self):
self.storage.put('keystore', self.keystore.dump())
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
2019-03-04 12:53:04 +01:00
return self.db.get_imported_addresses()
def get_receiving_addresses(self, **kwargs):
return self.get_addresses()
def get_change_addresses(self, **kwargs):
return []
2016-07-02 08:58:56 +02:00
2018-12-14 22:50:25 +01:00
def import_addresses(self, addresses: List[str], *,
write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]:
good_addr = [] # type: List[str]
bad_addr = [] # type: List[Tuple[str, str]]
for address in addresses:
if not bitcoin.is_address(address):
bad_addr.append((address, _('invalid address')))
continue
2019-03-04 12:53:04 +01:00
if self.db.has_imported_address(address):
bad_addr.append((address, _('address already in wallet')))
continue
good_addr.append(address)
2019-03-04 12:53:04 +01:00
self.db.add_imported_address(address, {})
self.add_address(address)
2019-03-04 12:53:04 +01:00
if write_to_disk:
self.storage.write()
return good_addr, bad_addr
def import_address(self, address: str) -> str:
good_addr, bad_addr = self.import_addresses([address])
if good_addr and good_addr[0] == address:
return address
else:
raise BitcoinException(str(bad_addr[0][1]))
2016-07-02 08:58:56 +02:00
2016-08-17 10:39:30 +02:00
def delete_address(self, address):
2019-03-04 12:53:04 +01:00
if not self.db.has_imported_address(address):
2016-08-17 10:39:30 +02:00
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 in self.db.get_history():
details = self.db.get_addr_history(addr)
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.db.remove_addr_history(address)
for tx_hash in transactions_to_remove:
self.remove_transaction(tx_hash)
self.db.remove_tx_fee(tx_hash)
self.db.remove_verified_tx(tx_hash)
self.unverified_tx.pop(tx_hash, None)
self.db.remove_transaction(tx_hash)
self.set_label(address, None)
self.remove_payment_request(address)
2019-02-24 08:41:00 +01:00
self.set_frozen_state_of_addresses([address], False)
2017-10-10 11:38:30 +02:00
pubkey = self.get_public_key(address)
2019-03-04 12:53:04 +01:00
self.db.remove_imported_address(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:
2019-03-04 12:53:04 +01:00
if self.db.has_imported_address(addr2):
2018-03-22 07:27:18 +01:00
break
else:
self.keystore.delete_imported_key(pubkey)
self.save_keystore()
2016-08-17 10:39:30 +02:00
self.storage.write()
def is_mine(self, address) -> bool:
2019-03-04 12:53:04 +01:00
return self.db.has_imported_address(address)
2019-10-23 17:09:41 +02:00
def get_address_index(self, address) -> Optional[str]:
# returns None if address is not mine
return self.get_public_key(address)
2019-10-23 17:09:41 +02:00
def get_public_key(self, address) -> Optional[str]:
2019-03-04 12:53:04 +01:00
x = self.db.get_imported_address(address)
return x.get('pubkey') if x else None
2018-12-14 22:50:25 +01:00
def import_private_keys(self, keys: List[str], password: Optional[str], *,
write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]:
good_addr = [] # type: List[str]
bad_keys = [] # type: List[Tuple[str, str]]
for key in keys:
try:
txin_type, pubkey = self.keystore.import_privkey(key, password)
except Exception as e:
bad_keys.append((key, _('invalid private key') + f': {e}'))
continue
if txin_type not in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'):
bad_keys.append((key, _('not implemented type') + f': {txin_type}'))
continue
addr = bitcoin.pubkey_to_address(txin_type, pubkey)
good_addr.append(addr)
2019-10-23 17:09:41 +02:00
self.db.add_imported_address(addr, {'type':txin_type, 'pubkey':pubkey})
self.add_address(addr)
self.save_keystore()
if write_to_disk:
self.storage.write()
return good_addr, bad_keys
def import_private_key(self, key: str, password: Optional[str]) -> str:
good_addr, bad_keys = self.import_private_keys([key], password=password)
if good_addr:
return good_addr[0]
else:
raise BitcoinException(str(bad_keys[0][1]))
2016-07-02 08:58:56 +02:00
def get_txin_type(self, address):
2019-03-04 12:53:04 +01:00
return self.db.get_imported_address(address).get('type', 'address')
2019-10-23 17:09:41 +02:00
def _add_input_sig_info(self, txin, address):
assert self.is_mine(address)
if txin.script_type in ('unknown', 'address'):
return
2019-10-23 17:09:41 +02:00
elif txin.script_type in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'):
pubkey = self.get_public_key(address)
if not pubkey:
return
txin.pubkeys = [bfh(pubkey)]
else:
2019-10-23 17:09:41 +02:00
raise Exception(f'Unexpected script type: {txin.script_type}. '
f'Imported wallets are not implemented to handle this.')
def pubkeys_to_address(self, pubkey):
2019-03-04 12:53:04 +01:00
for addr in self.db.get_imported_addresses():
if self.db.get_imported_address(addr)['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, *, config):
2019-10-23 17:09:41 +02:00
self._ephemeral_addr_to_addr_index = {} # type: Dict[str, Sequence[int]]
Abstract_Wallet.__init__(self, storage, config=config)
2016-07-02 08:58:56 +02:00
self.gap_limit = storage.get('gap_limit', 20)
# generate addresses now. note that without libsecp this might block
# for a few seconds!
self.synchronize()
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
out = self.get_receiving_addresses()
2018-07-19 18:16:23 +02:00
out += self.get_change_addresses()
return out
def get_receiving_addresses(self, *, slice_start=None, slice_stop=None):
return self.db.get_receiving_addresses(slice_start=slice_start, slice_stop=slice_stop)
2014-06-24 16:12:43 +03:00
def get_change_addresses(self, *, slice_start=None, slice_stop=None):
return self.db.get_change_addresses(slice_start=slice_start, slice_stop=slice_stop)
2014-05-01 18:58:24 +02:00
@profiler
def try_detecting_internal_addresses_corruption(self):
if not is_using_fast_ecc():
2019-04-26 18:52:26 +02:00
self.logger.info("internal address corruption test skipped due to missing libsecp256k1")
return
addresses_all = self.get_addresses()
# sample 1: first few
addresses_sample1 = addresses_all[:10]
# sample2: a few more randomly selected
addresses_rand = addresses_all[10:]
addresses_sample2 = random.sample(addresses_rand, min(len(addresses_rand), 10))
for addr_found in addresses_sample1 + addresses_sample2:
2018-12-13 12:10:01 +01:00
self.check_address(addr_found)
2018-12-13 12:10:01 +01:00
def check_address(self, addr):
if addr and self.is_mine(addr):
if addr != self.derive_address(*self.get_address_index(addr)):
raise InternalAddressCorruption()
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'''
2019-03-04 12:53:04 +01:00
if value >= self.min_acceptable_gap():
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)
2019-03-04 12:53:04 +01:00
self.storage.write()
2014-04-30 11:18:13 +02:00
return True
else:
return False
def num_unused_trailing_addresses(self, addresses):
k = 0
for addr in addresses[::-1]:
if self.db.get_addr_history(addr):
break
k += 1
2014-04-30 11:18:13 +02:00
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 addr in addresses[0:-k]:
if self.db.get_addr_history(addr):
2016-07-02 08:58:56 +02:00
n = 0
else:
n += 1
nmax = max(nmax, n)
2014-04-30 11:18:13 +02:00
return nmax + 1
def derive_address(self, for_change, n):
x = self.derive_pubkeys(for_change, n)
2018-12-13 12:10:01 +01:00
return self.pubkeys_to_address(x)
2019-10-23 17:09:41 +02:00
def get_public_keys_with_deriv_info(self, address: str):
der_suffix = self.get_address_index(address)
der_suffix = [int(x) for x in der_suffix]
return {k.derive_pubkey(*der_suffix): (k, der_suffix)
for k in self.get_keystores()}
def _add_input_sig_info(self, txin, address):
assert self.is_mine(address)
pubkey_deriv_info = self.get_public_keys_with_deriv_info(address)
txin.pubkeys = sorted([bfh(pk) for pk in list(pubkey_deriv_info)])
for pubkey_hex in pubkey_deriv_info:
ks, der_suffix = pubkey_deriv_info[pubkey_hex]
xfp_bytes = bfh(ks.get_root_fingerprint())
der_prefix = bip32.convert_bip32_path_to_list_of_uint32(ks.get_derivation_prefix())
der_full = der_prefix + list(der_suffix)
txin.bip32_paths[bfh(pubkey_hex)] = (xfp_bytes, der_full)
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:
2019-03-04 12:53:04 +01:00
n = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses()
address = self.derive_address(for_change, n)
2019-03-04 12:53:04 +01:00
self.db.add_change_address(address) if for_change else self.db.add_receiving_address(address)
2018-02-12 23:20:58 +01:00
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:
num_addr = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses()
if num_addr < limit:
self.create_new_address(for_change)
continue
if for_change:
last_few_addresses = self.get_change_addresses(slice_start=-limit)
else:
last_few_addresses = self.get_receiving_addresses(slice_start=-limit)
if any(map(self.address_is_old, last_few_addresses)):
self.create_new_address(for_change)
else:
break
2014-04-30 11:18:13 +02:00
2019-09-09 01:34:29 +02:00
@AddressSynchronizer.with_local_height_cached
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)
limit = self.gap_limit_for_change if is_change else self.gap_limit
if i < limit:
return False
slice_start = max(0, i - limit)
slice_stop = max(0, i)
if is_change:
prev_addresses = self.get_change_addresses(slice_start=slice_start, slice_stop=slice_stop)
else:
prev_addresses = self.get_receiving_addresses(slice_start=slice_start, slice_stop=slice_stop)
for addr in prev_addresses:
if self.db.get_addr_history(addr):
return False
return True
2019-10-23 17:09:41 +02:00
def get_address_index(self, address) -> Optional[Sequence[int]]:
return self.db.get_address_index(address) or self._ephemeral_addr_to_addr_index.get(address)
def _learn_derivation_path_for_address_from_txinout(self, txinout, address):
for ks in self.get_keystores():
pubkey, der_suffix = ks.find_my_pubkey_in_txinout(txinout, only_der_suffix=True)
if der_suffix is not None:
self._ephemeral_addr_to_addr_index[address] = list(der_suffix)
return True
return False
2018-01-30 00:59:12 +01:00
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, *, config):
Deterministic_Wallet.__init__(self, storage, config=config)
def get_public_key(self, address):
sequence = self.get_address_index(address)
2019-10-23 17:09:41 +02:00
pubkey = self.derive_pubkeys(*sequence)
return pubkey
2017-01-16 09:48:38 +01:00
def load_keystore(self):
self.keystore = load_keystore(self.storage, 'keystore')
try:
2018-10-25 22:20:33 +02:00
xtype = bip32.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
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, *, config):
2015-06-26 14:29:26 +02:00
self.wallet_type = storage.get('wallet_type')
2016-08-21 11:58:15 +02:00
self.m, self.n = multisig_type(self.wallet_type)
Deterministic_Wallet.__init__(self, storage, config=config)
2015-06-26 14:29:26 +02:00
def get_public_keys(self, address):
2019-10-23 17:09:41 +02:00
return list(self.get_public_keys_with_deriv_info(address))
2017-09-18 08:52:06 +02:00
def pubkeys_to_address(self, pubkeys):
2019-10-23 17:09:41 +02:00
redeem_script = self.pubkeys_to_scriptcode(pubkeys)
return bitcoin.redeem_script_to_address(self.txin_type, redeem_script)
2017-09-01 14:15:54 +02:00
2019-10-23 17:09:41 +02:00
def pubkeys_to_scriptcode(self, pubkeys: Sequence[str]) -> str:
2017-01-16 09:48:38 +01:00
return transaction.multisig_script(sorted(pubkeys), self.m)
2014-04-06 21:38:53 +02:00
def get_redeem_script(self, address):
2019-10-23 17:09:41 +02:00
txin_type = self.get_txin_type(address)
pubkeys = self.get_public_keys(address)
scriptcode = self.pubkeys_to_scriptcode(pubkeys)
if txin_type == 'p2sh':
return scriptcode
elif txin_type == 'p2wsh-p2sh':
return bitcoin.p2wsh_nested_script(scriptcode)
elif txin_type == 'p2wsh':
return None
raise UnknownTxinType(f'unexpected txin_type {txin_type}')
def get_witness_script(self, address):
txin_type = self.get_txin_type(address)
pubkeys = self.get_public_keys(address)
2019-10-23 17:09:41 +02:00
scriptcode = self.pubkeys_to_scriptcode(pubkeys)
if txin_type == 'p2sh':
return None
elif txin_type in ('p2wsh-p2sh', 'p2wsh'):
return scriptcode
raise UnknownTxinType(f'unexpected txin_type {txin_type}')
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/']
2018-10-25 22:20:33 +02:00
xtype = bip32.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
2019-09-04 13:31:49 +02:00
return StorageEncryptionVersion.USER_PASSWORD
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 all([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()))
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: WalletStorage, *, config: SimpleConfig):
wallet_type = storage.get('wallet_type')
2016-07-02 08:58:56 +02:00
WalletClass = Wallet.wallet_class(wallet_type)
wallet = WalletClass(storage, config=config)
2015-12-31 09:51:27 +09:00
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))
def create_new_wallet(*, path, config: SimpleConfig, passphrase=None, password=None,
2019-09-04 20:15:54 +02:00
encrypt_file=True, seed_type=None, gap_limit=None) -> dict:
"""Create a new wallet"""
storage = WalletStorage(path)
if storage.file_exists():
raise Exception("Remove the existing wallet first!")
seed = Mnemonic('en').make_seed(seed_type)
k = keystore.from_seed(seed, passphrase)
storage.put('keystore', k.dump())
storage.put('wallet_type', 'standard')
if gap_limit is not None:
storage.put('gap_limit', gap_limit)
wallet = Wallet(storage, config=config)
wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file)
wallet.synchronize()
msg = "Please keep your seed in a safe place; if you lose it, you will not be able to restore your wallet."
wallet.storage.write()
return {'seed': seed, 'wallet': wallet, 'msg': msg}
def restore_wallet_from_text(text, *, path, config: SimpleConfig,
passphrase=None, password=None, encrypt_file=True,
2019-09-04 20:15:54 +02:00
gap_limit=None) -> dict:
"""Restore a wallet from text. Text can be a seed phrase, a master
public key, a master private key, a list of bitcoin addresses
or bitcoin private keys."""
storage = WalletStorage(path)
if storage.file_exists():
raise Exception("Remove the existing wallet first!")
text = text.strip()
if keystore.is_address_list(text):
wallet = Imported_Wallet(storage, config=config)
addresses = text.split()
good_inputs, bad_inputs = wallet.import_addresses(addresses, write_to_disk=False)
# FIXME tell user about bad_inputs
if not good_inputs:
raise Exception("None of the given addresses can be imported")
elif keystore.is_private_key_list(text, allow_spaces_inside_key=False):
k = keystore.Imported_KeyStore({})
storage.put('keystore', k.dump())
wallet = Imported_Wallet(storage, config=config)
keys = keystore.get_private_keys(text, allow_spaces_inside_key=False)
good_inputs, bad_inputs = wallet.import_private_keys(keys, None, write_to_disk=False)
# FIXME tell user about bad_inputs
if not good_inputs:
raise Exception("None of the given privkeys can be imported")
else:
if keystore.is_master_key(text):
k = keystore.from_master_key(text)
elif keystore.is_seed(text):
k = keystore.from_seed(text, passphrase)
else:
raise Exception("Seed or key not recognized")
storage.put('keystore', k.dump())
storage.put('wallet_type', 'standard')
if gap_limit is not None:
storage.put('gap_limit', gap_limit)
wallet = Wallet(storage, config=config)
assert not storage.file_exists(), "file was created too soon! plaintext keys might have been written to disk"
wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file)
wallet.synchronize()
2019-09-04 20:15:54 +02:00
msg = ("This wallet was restored offline. It may contain more addresses than displayed. "
"Start a daemon and use load_wallet to sync its history.")
wallet.storage.write()
return {'wallet': wallet, 'msg': msg}