diff --git a/jmbitcoin/jmbitcoin/secp256k1_main.py b/jmbitcoin/jmbitcoin/secp256k1_main.py index 931b424..e91fc59 100644 --- a/jmbitcoin/jmbitcoin/secp256k1_main.py +++ b/jmbitcoin/jmbitcoin/secp256k1_main.py @@ -10,7 +10,7 @@ import coincurve as secp256k1 #Required only for PoDLE calculation: N = 115792089237316195423570985008687907852837564279074904382605163141518161494337 -BTC_P2PK_VBYTE = {"mainnet": b'\x00', "testnet": b'\x6f'} +BTC_P2PK_VBYTE = {"mainnet": b'\x00', "testnet": b'\x6f', "regtest": 100} BTC_P2SH_VBYTE = {"mainnet": b'\x05', "testnet": b'\xc4'} #Standard prefix for Bitcoin message signing. diff --git a/jmclient/jmclient/__init__.py b/jmclient/jmclient/__init__.py index 04d5aa4..c00fe7e 100644 --- a/jmclient/jmclient/__init__.py +++ b/jmclient/jmclient/__init__.py @@ -13,7 +13,7 @@ from .old_mnemonic import mn_decode, mn_encode from .taker import Taker, P2EPTaker from .wallet import (Mnemonic, estimate_tx_fee, WalletError, BaseWallet, ImportWalletMixin, BIP39WalletMixin, BIP32Wallet, BIP49Wallet, LegacyWallet, - SegwitWallet, SegwitLegacyWallet, UTXOManager, + SegwitWallet, SegwitLegacyWallet, FidelityBondMixin, UTXOManager, WALLET_IMPLEMENTATIONS, compute_tx_locktime) from .storage import (Argon2Hash, Storage, StorageError, RetryableStorageError, StoragePasswordError, VolatileStorage) diff --git a/jmclient/jmclient/cryptoengine.py b/jmclient/jmclient/cryptoengine.py index e537b8f..24ccfe9 100644 --- a/jmclient/jmclient/cryptoengine.py +++ b/jmclient/jmclient/cryptoengine.py @@ -5,10 +5,11 @@ from collections import OrderedDict import struct import jmbitcoin as btc -from .configure import get_network +from .configure import get_network, jm_single -TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH, TYPE_P2SH_M_N = range(4) +TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH, TYPE_P2SH_M_N, TYPE_TIMELOCK_P2WSH, \ + TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS = range(6) NET_MAINNET, NET_TESTNET = range(2) NET_MAP = {'mainnet': NET_MAINNET, 'testnet': NET_TESTNET} WIF_PREFIX_MAP = {'mainnet': b'\x80', 'testnet': b'\xef'} @@ -283,8 +284,61 @@ class BTC_P2WPKH(BTCEngine): return btc.sign(btc.serialize(tx), index, privkey, hashcode=hashcode, amount=amount, native=True) +class BTC_Timelocked_P2WSH(BTCEngine): + + """ + In this class many instances of "privkey" or "pubkey" are actually tuples + of (privkey, timelock) or (pubkey, timelock) + """ + + @classproperty + def VBYTE(cls): + #slight hack here, network can be either "mainnet" or "testnet" + #but we need to distinguish between actual testnet and regtest + if get_network() == "mainnet": + return btc.BTC_P2PK_VBYTE["mainnet"] + else: + if jm_single().config.get("BLOCKCHAIN", "blockchain_source")\ + == "regtest": + return btc.BTC_P2PK_VBYTE["regtest"] + else: + assert get_network() == "testnet" + return btc.BTC_P2PK_VBYTE["testnet"] + + @classmethod + def privkey_to_script(cls, privkey_locktime): + privkey, locktime = privkey_locktime + pub = cls.privkey_to_pubkey(privkey) + return cls.pubkey_to_script((pub, locktime)) + + @classmethod + def pubkey_to_script(cls, pubkey_locktime): + redeem_script = cls.pubkey_to_script_code(pubkey_locktime) + return btc.redeem_script_to_p2wsh_script(redeem_script) + + @classmethod + def pubkey_to_script_code(cls, pubkey_locktime): + pubkey, locktime = pubkey_locktime + return btc.mk_freeze_script(pubkey, locktime) + + @classmethod + def privkey_to_wif(cls, privkey_locktime): + priv, locktime = privkey_locktime + return btc.bin_to_b58check(priv, cls.WIF_PREFIX) + + @classmethod + def sign_transaction(cls, tx, index, privkey, amount, + hashcode=btc.SIGHASH_ALL, **kwargs): + raise Exception("not implemented yet") + + @classmethod + def sign_transaction(cls, tx, index, privkey, amount, + hashcode=btc.SIGHASH_ALL, **kwargs): + raise RuntimeError("Cannot spend from watch-only wallets") + ENGINES = { TYPE_P2PKH: BTC_P2PKH, TYPE_P2SH_P2WPKH: BTC_P2SH_P2WPKH, - TYPE_P2WPKH: BTC_P2WPKH + TYPE_P2WPKH: BTC_P2WPKH, + TYPE_TIMELOCK_P2WSH: BTC_Timelocked_P2WSH } diff --git a/jmclient/jmclient/wallet.py b/jmclient/jmclient/wallet.py index 3778c4a..3f1dae5 100644 --- a/jmclient/jmclient/wallet.py +++ b/jmclient/jmclient/wallet.py @@ -7,6 +7,7 @@ import numbers import random from binascii import hexlify, unhexlify from datetime import datetime +from calendar import timegm from copy import deepcopy from mnemonic import Mnemonic as MnemonicParent from hashlib import sha256 @@ -20,7 +21,8 @@ from .blockchaininterface import INF_HEIGHT from .support import select_gradual, select_greedy, select_greediest, \ select from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH,\ - TYPE_P2WPKH, ENGINES + TYPE_P2WPKH, TYPE_TIMELOCK_P2WSH, TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS,\ + ENGINES from .support import get_random_bytes from . import mn_encode, mn_decode import jmbitcoin as btc @@ -559,7 +561,7 @@ class BaseWallet(object): returns: tuple (mixdepth, type, index) - type is one of 0, 1, 'imported' + type is one of 0, 1, 'imported', 2, 3 """ raise NotImplementedError() @@ -976,27 +978,28 @@ class BaseWallet(object): def rewind_wallet_indices(self, used_indices, saved_indices): for md in used_indices: - for address_type in (self.ADDRESS_TYPE_EXTERNAL, - self.ADDRESS_TYPE_INTERNAL): + for address_type in range(min(len(used_indices[md]), len(saved_indices[md]))): index = max(used_indices[md][address_type], saved_indices[md][address_type]) self.set_next_index(md, address_type, index, force=True) + def _get_default_used_indices(self): + return {x: [0, 0] for x in range(self.max_mixdepth + 1)} + def get_used_indices(self, addr_gen): """ Returns a dict of max used indices for each branch in the wallet, from the given addresses addr_gen, assuming that they are known to the wallet. """ - - indices = {x: [0, 0] for x in range(self.max_mixdepth + 1)} + indices = self._get_default_used_indices() for addr in addr_gen: if not self.is_known_addr(addr): continue md, address_type, index = self.get_details( self.addr_to_path(addr)) - if address_type not in (self.ADDRESS_TYPE_EXTERNAL, - self.ADDRESS_TYPE_INTERNAL): + if address_type not in (self.BIP32_EXT_ID, self.BIP32_INT_ID, + FidelityBondMixin.BIP32_TIMELOCK_ID, FidelityBondMixin.BIP32_BURN_ID): assert address_type == 'imported' continue indices[md][address_type] = max(indices[md][address_type], index + 1) @@ -1379,6 +1382,10 @@ class BIP32Wallet(BaseWallet): def _derive_bip32_master_key(cls, seed): return cls._ENGINE.derive_bip32_master_key(seed) + @classmethod + def _get_supported_address_types(cls): + return (cls.BIP32_EXT_ID, cls.BIP32_INT_ID) + def get_script_from_path(self, path): if not self._is_my_bip32_path(path): raise WalletError("unable to get script for unknown key path") @@ -1387,11 +1394,14 @@ class BIP32Wallet(BaseWallet): if not 0 <= md <= self.max_mixdepth: raise WalletError("Mixdepth outside of wallet's range.") - assert address_type in (self.BIP32_EXT_ID, self.BIP32_INT_ID) + assert address_type in self._get_supported_address_types() current_index = self._index_cache[md][address_type] - if index == current_index: + if index == current_index \ + and address_type != FidelityBondMixin.BIP32_TIMELOCK_ID: + #special case for timelocked addresses because for them the + #concept of a "next address" cant be used return self.get_new_script_override_disable(md, address_type) priv, engine = self._get_priv_from_path(path) @@ -1481,9 +1491,16 @@ class BIP32Wallet(BaseWallet): def get_new_script_override_disable(self, mixdepth, address_type): # This is called by get_script_from_path and calls back there. We need to # ensure all conditions match to avoid endless recursion. + index = self.get_index_cache_and_increment(mixdepth, address_type) + return self.get_script_and_update_map(mixdepth, address_type, index) + + def get_index_cache_and_increment(self, mixdepth, address_type): index = self._index_cache[mixdepth][address_type] self._index_cache[mixdepth][address_type] += 1 - path = self.get_path(mixdepth, address_type, index) + return index + + def get_script_and_update_map(self, *args): + path = self.get_path(*args) script = self.get_script_from_path(path) self._script_map[script] = path return script @@ -1571,8 +1588,6 @@ class LegacyWallet(ImportWalletMixin, BIP32Wallet): def _get_bip32_base_path(self): return self._key_ident, 0 - - class BIP32PurposedWallet(BIP32Wallet): """ A class to encapsulate cases like BIP44, 49 and 84, all of which are derivatives @@ -1595,6 +1610,174 @@ class BIP32PurposedWallet(BIP32Wallet): return path[len(self._get_bip32_base_path())] - 2**31 +class FidelityBondMixin(object): + BIP32_TIMELOCK_ID = 2 + BIP32_BURN_ID = 3 + + """ + Explaination of time number + + incrementing time numbers (0, 1, 2, 3, 4... + will produce datetimes + suitable for timelocking (1st january, 1st april, 1st july .... + this greatly reduces the number of possible timelock values + and is helpful for recovery of funds because the wallet can search + the only addresses corresponding to timenumbers which are far fewer + + For example, if TIMENUMBER_UNIT = 2 (i.e. every time number is two months) + then there are 6 timelocks per year so just 600 possible + addresses per century per pubkey. Easily searchable when recovering a + wallet from seed phrase. Therefore the user doesn't need to store any + dates, the seed phrase is sufficent for recovery. + """ + #should be a factor of 12, the number of months in a year + TIMENUMBER_UNIT = 1 + + # all timelocks are 1st of the month at midnight + TIMELOCK_DAY_AND_SHORTER = (1, 0, 0, 0, 0) + TIMELOCK_EPOCH_YEAR = 2020 + TIMELOCK_EPOCH_MONTH = 1 #january + MONTHS_IN_YEAR = 12 + + TIMELOCK_ERA_YEARS = 30 + TIMENUMBERS_PER_PUBKEY = TIMELOCK_ERA_YEARS * MONTHS_IN_YEAR // TIMENUMBER_UNIT + + """ + As each pubkey corresponds to hundreds of addresses, to reduce load the + given gap limit will be reduced by this factor. Also these timelocked + addresses are never handed out to takers so there wont be a problem of + having many used addresses with no transactions on them. + """ + TIMELOCK_GAP_LIMIT_REDUCTION_FACTOR = 6 + + #only one mixdepth will have fidelity bonds in it + FIDELITY_BOND_MIXDEPTH = 0 + + @classmethod + def _time_number_to_timestamp(cls, timenumber): + """ + converts a time number to a unix timestamp + """ + year = cls.TIMELOCK_EPOCH_YEAR + (timenumber*cls.TIMENUMBER_UNIT) // cls.MONTHS_IN_YEAR + month = cls.TIMELOCK_EPOCH_MONTH + (timenumber*cls.TIMENUMBER_UNIT) % cls.MONTHS_IN_YEAR + return timegm(datetime(year, month, *cls.TIMELOCK_DAY_AND_SHORTER).timetuple()) + + @classmethod + def timestamp_to_time_number(cls, timestamp): + """ + converts a datetime object to a time number + """ + dt = datetime.utcfromtimestamp(timestamp) + if (dt.month - cls.TIMELOCK_EPOCH_MONTH) % cls.TIMENUMBER_UNIT != 0: + raise ValueError() + day_and_shorter_tuple = (dt.day, dt.hour, dt.minute, dt.second, dt.microsecond) + if day_and_shorter_tuple != cls.TIMELOCK_DAY_AND_SHORTER: + raise ValueError() + timenumber = (dt.year - cls.TIMELOCK_EPOCH_YEAR)*(cls.MONTHS_IN_YEAR // + cls.TIMENUMBER_UNIT) + ((dt.month - cls.TIMELOCK_EPOCH_MONTH) // cls.TIMENUMBER_UNIT) + if timenumber < 0 or timenumber > cls.TIMENUMBERS_PER_PUBKEY: + raise ValueError("datetime out of range") + return timenumber + + @classmethod + def _is_timelocked_path(cls, path): + return len(path) > 4 and path[4] == cls.BIP32_TIMELOCK_ID + + def get_xpub_from_fidelity_bond_master_pub_key(cls, mpk): + if mpk.startswith(cls._BIP32_PUBKEY_PREFIX): + return mpk[len(cls._BIP32_PUBKEY_PREFIX):] + else: + return False + + def _get_key_ident(self): + first_path = self.get_path(0, 0) + priv, engine = self._get_priv_from_path(first_path) + pub = engine.privkey_to_pubkey(priv) + return sha256(sha256(pub).digest()).digest()[:3] + + def _populate_script_map(self): + super(FidelityBondMixin, self)._populate_script_map() + for md in self._index_cache: + address_type = self.BIP32_TIMELOCK_ID + for i in range(self._index_cache[md][address_type]): + for timenumber in range(self.TIMENUMBERS_PER_PUBKEY): + path = self.get_path(md, address_type, i, timenumber) + script = self.get_script_from_path(path) + self._script_map[script] = path + + @classmethod + def _get_supported_address_types(cls): + return (cls.BIP32_EXT_ID, cls.BIP32_INT_ID, cls.BIP32_TIMELOCK_ID, cls.BIP32_BURN_ID) + + def _get_priv_from_path(self, path): + if self._is_timelocked_path(path): + key_path = path[:-1] + locktime = path[-1] + engine = ENGINES[TYPE_TIMELOCK_P2WSH] + privkey = engine.derive_bip32_privkey(self._master_key, key_path) + return (privkey, locktime), engine + else: + return super(FidelityBondMixin, self)._get_priv_from_path(path) + + def get_path(self, mixdepth=None, address_type=None, index=None, timenumber=None): + if address_type == None or address_type in (self.BIP32_EXT_ID, self.BIP32_INT_ID, + self.BIP32_BURN_ID) or index == None: + return super(FidelityBondMixin, self).get_path(mixdepth, address_type, index) + elif address_type == self.BIP32_TIMELOCK_ID: + assert timenumber != None + timestamp = self._time_number_to_timestamp(timenumber) + return tuple(chain(self._get_bip32_export_path(mixdepth, address_type), + (index, timestamp))) + else: + assert 0 + + """ + We define a new serialization of the bip32 path to include bip65 timelock addresses + Previously it was m/44'/1'/0'/3/0 + For a timelocked address it will be m/44'/1'/0'/3/0:13245432 + The timelock will be in unix format and added to the end with a colon ":" character + refering to the pubkey plus the timelock value which together are needed to create the address + """ + def get_path_repr(self, path): + if self._is_timelocked_path(path) and len(path) == 7: + return super(FidelityBondMixin, self).get_path_repr(path[:-1]) +\ + ":" + str(path[-1]) + else: + return super(FidelityBondMixin, self).get_path_repr(path) + + def path_repr_to_path(self, pathstr): + if pathstr.find(":") == -1: + return super(FidelityBondMixin, self).path_repr_to_path(pathstr) + else: + colon_chunks = pathstr.split(":") + if len(colon_chunks) != 2: + raise WalletError("Not a valid wallet timelock path: {}".format(pathstr)) + return tuple(chain( + super(FidelityBondMixin, self).path_repr_to_path(colon_chunks[0]), + (int(colon_chunks[1]),) + )) + + def get_details(self, path): + if self._is_timelocked_path(path): + return self._get_mixdepth_from_path(path), path[-3], path[-2] + else: + return super(FidelityBondMixin, self).get_details(path) + + def _get_default_used_indices(self): + return {x: [0, 0, 0, 0] for x in range(self.max_mixdepth + 1)} + + def get_script(self, mixdepth, address_type, index, timenumber=None): + path = self.get_path(mixdepth, address_type, index, timenumber) + return self.get_script_from_path(path) + + def get_addr(self, mixdepth, address_type, index, timenumber=None): + script = self.get_script(mixdepth, address_type, index, timenumber) + return self.script_to_addr(script) + +#class FidelityBondWatchonlyWallet(ImportWalletMixin, BIP39WalletMixin, FidelityBondMixin): + + + class BIP49Wallet(BIP32PurposedWallet): _PURPOSE = 2**31 + 49 _ENGINE = ENGINES[TYPE_P2SH_P2WPKH] @@ -1609,8 +1792,12 @@ class SegwitLegacyWallet(ImportWalletMixin, BIP39WalletMixin, BIP49Wallet): class SegwitWallet(ImportWalletMixin, BIP39WalletMixin, BIP84Wallet): TYPE = TYPE_P2WPKH +class SegwitLegacyWalletFidelityBonds(FidelityBondMixin, SegwitLegacyWallet): + TYPE = TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS + WALLET_IMPLEMENTATIONS = { LegacyWallet.TYPE: LegacyWallet, SegwitLegacyWallet.TYPE: SegwitLegacyWallet, - SegwitWallet.TYPE: SegwitWallet + SegwitWallet.TYPE: SegwitWallet, + SegwitLegacyWalletFidelityBonds.TYPE: SegwitLegacyWalletFidelityBonds } diff --git a/jmclient/jmclient/wallet_service.py b/jmclient/jmclient/wallet_service.py index 1d75ae9..c21fc70 100644 --- a/jmclient/jmclient/wallet_service.py +++ b/jmclient/jmclient/wallet_service.py @@ -15,6 +15,7 @@ from jmclient.configure import jm_single, get_log from jmclient.output import fmt_tx_data from jmclient.blockchaininterface import (INF_HEIGHT, BitcoinCoreInterface, BitcoinCoreNoHistoryInterface) +from jmclient.wallet import FidelityBondMixin from jmbase.support import jmprint, EXIT_SUCCESS """Wallet service @@ -735,6 +736,22 @@ class WalletService(Service): for path in self.yield_imported_paths(md): addresses.add(self.get_address_from_path(path)) + if isinstance(self.wallet, FidelityBondMixin): + md = FidelityBondMixin.FIDELITY_BOND_MIXDEPTH + address_type = FidelityBondMixin.BIP32_TIMELOCK_ID + saved_indices[md] += [0] + next_unused = self.get_next_unused_index(md, address_type) + for index in range(next_unused): + for timenumber in range(FidelityBondMixin.TIMENUMBERS_PER_PUBKEY): + addresses.add(self.get_addr(md, address_type, index, timenumber)) + for index in range(self.gap_limit // FidelityBondMixin.TIMELOCK_GAP_LIMIT_REDUCTION_FACTOR): + index += next_unused + assert self.wallet.get_index_cache_and_increment(md, address_type) == index + for timenumber in range(FidelityBondMixin.TIMENUMBERS_PER_PUBKEY): + self.wallet.get_script_and_update_map(md, address_type, index, timenumber) + addresses.add(self.get_addr(md, address_type, index, timenumber)) + self.wallet.set_next_index(md, address_type, next_unused) + return addresses, saved_indices def collect_addresses_gap(self, gap_limit=None): @@ -748,6 +765,17 @@ class WalletService(Service): addresses.add(self.get_new_addr(md, address_type)) self.set_next_index(md, address_type, old_next) + if isinstance(self.wallet, FidelityBondMixin): + md = FidelityBondMixin.FIDELITY_BOND_MIXDEPTH + address_type = FidelityBondMixin.BIP32_TIMELOCK_ID + old_next = self.get_next_unused_index(md, address_type) + for ii in range(gap_limit // FidelityBondMixin.TIMELOCK_GAP_LIMIT_REDUCTION_FACTOR): + index = self.wallet.get_index_cache_and_increment(md, address_type) + for timenumber in range(FidelityBondMixin.TIMENUMBERS_PER_PUBKEY): + self.wallet.get_script_and_update_map(md, address_type, index, timenumber) + addresses.add(self.get_addr(md, address_type, index, timenumber)) + self.set_next_index(md, address_type, old_next) + return addresses def get_external_addr(self, mixdepth): diff --git a/jmclient/jmclient/wallet_utils.py b/jmclient/jmclient/wallet_utils.py index 6a1d4a7..7ff37cc 100644 --- a/jmclient/jmclient/wallet_utils.py +++ b/jmclient/jmclient/wallet_utils.py @@ -5,6 +5,7 @@ import sys import sqlite3 import binascii from datetime import datetime +from calendar import timegm from optparse import OptionParser from numbers import Integral from collections import Counter @@ -12,12 +13,13 @@ from itertools import islice from jmclient import (get_network, WALLET_IMPLEMENTATIONS, Storage, podle, jm_single, BitcoinCoreInterface, WalletError, VolatileStorage, StoragePasswordError, is_segwit_mode, SegwitLegacyWallet, - LegacyWallet, SegwitWallet, is_native_segwit_mode, load_program_config, - add_base_options, check_regtest) + LegacyWallet, SegwitWallet, FidelityBondMixin, is_native_segwit_mode, + load_program_config, add_base_options, check_regtest) from jmclient.wallet_service import WalletService from jmbase.support import get_password, jmprint, EXIT_FAILURE, EXIT_ARGERROR -from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH +from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH, \ + TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS from .output import fmt_utxo import jmbitcoin as btc @@ -42,8 +44,9 @@ def get_wallettool_parser(): '(dumpprivkey) Export a single private key, specify an hd wallet path\n' '(signmessage) Sign a message with the private key from an address in \n' 'the wallet. Use with -H and specify an HD wallet path for the address.\n' - '(freeze) Freeze or un-freeze a specific utxo. Specify mixdepth with -m.') - parser = OptionParser(usage='usage: %prog [options] [wallet file] [method]', + '(freeze) Freeze or un-freeze a specific utxo. Specify mixdepth with -m.\n' + '(gettimelockaddress) Obtain a timelocked address. Argument is locktime value as yyyy-mm. For example `2021-03`') + parser = OptionParser(usage='usage: %prog [options] [wallet file] [method] [args..]', description=description) add_base_options(parser) parser.add_option('-p', @@ -167,7 +170,9 @@ class WalletViewEntry(WalletViewBase): super(WalletViewEntry, self).__init__(wallet_path_repr, serclass=serclass, custom_separator=custom_separator) self.account = account - assert address_type in [0, 1, -1] + assert address_type in [SegwitWallet.BIP32_EXT_ID, + SegwitWallet.BIP32_INT_ID, -1, FidelityBondMixin.BIP32_TIMELOCK_ID, + FidelityBondMixin.BIP32_BURN_ID] self.address_type = address_type assert isinstance(aindex, Integral) assert aindex >= 0 @@ -219,7 +224,9 @@ class WalletViewBranch(WalletViewBase): serclass=serclass, custom_separator=custom_separator) self.account = account - assert address_type in [0, 1, -1] + assert address_type in [SegwitWallet.BIP32_EXT_ID, + SegwitWallet.BIP32_INT_ID, -1, FidelityBondMixin.BIP32_TIMELOCK_ID, + FidelityBondMixin.BIP32_BURN_ID] self.address_type = address_type if xpub: assert xpub.startswith('xpub') or xpub.startswith('tpub') @@ -441,11 +448,42 @@ def wallet_display(wallet_service, showprivkey, displayall=False, entrylist.append(WalletViewEntry( wallet_service.get_path_repr(path), m, address_type, k, addr, [balance, balance], priv=privkey, used=used)) - wallet_service.set_next_index(m, address_type, unused_index) path = wallet_service.get_path_repr(wallet_service.get_path(m, address_type)) branchlist.append(WalletViewBranch(path, m, address_type, entrylist, xpub=xpub_key)) + + if m == FidelityBondMixin.FIDELITY_BOND_MIXDEPTH and \ + isinstance(wallet_service.wallet, FidelityBondMixin): + address_type = FidelityBondMixin.BIP32_TIMELOCK_ID + unused_index = wallet_service.get_next_unused_index(m, address_type) + timelocked_gaplimit = (wallet_service.wallet.gap_limit + // FidelityBondMixin.TIMELOCK_GAP_LIMIT_REDUCTION_FACTOR) + entrylist = [] + for k in range(unused_index + timelocked_gaplimit): + for timenumber in range(FidelityBondMixin.TIMENUMBERS_PER_PUBKEY): + path = wallet_service.get_path(m, address_type, k, timenumber) + addr = wallet_service.get_address_from_path(path) + timelock = datetime.utcfromtimestamp(path[-1]) + + balance = sum([utxodata["value"] for utxo, utxodata in + iteritems(utxos[m]) if path == utxodata["path"]]) + status = timelock.strftime("%Y-%m-%d") + " [" + ( + "LOCKED" if datetime.now() < timelock else "UNLOCKED") + "]" + privkey = "" + if showprivkey: + privkey = wallet_service.get_wif_path(path) + if displayall or balance > 0: + entrylist.append(WalletViewEntry( + wallet_service.get_path_repr(path), m, address_type, k, + addr, [balance, balance], priv=privkey, used=status)) + + #TODO fidelity bond master pub key is this, although it should include burner too + xpub_key = wallet_service.get_bip32_pub_export(m, address_type) + path = wallet_service.get_path_repr(wallet_service.get_path(m, address_type)) + branchlist.append(WalletViewBranch(path, m, address_type, entrylist, + xpub=xpub_key)) + ipb = get_imported_privkey_branch(wallet_service, m, showprivkey) if ipb: branchlist.append(ipb) @@ -499,11 +537,19 @@ def cli_get_mnemonic_extension(): "info") return input("Enter mnemonic extension: ") +def cli_do_support_fidelity_bonds(): + uin = input("Would you like this wallet to support fidelity bonds? " + "write 'n' if you don't know what this is (y/n): ") + if len(uin) == 0 or uin[0] != 'y': + jmprint("Not supporting fidelity bonds", "info") + return False + else: + return True def wallet_generate_recover_bip39(method, walletspath, default_wallet_name, display_seed_callback, enter_seed_callback, enter_wallet_password_callback, enter_wallet_file_name_callback, enter_if_use_seed_extension, - enter_seed_extension_callback, mixdepth=DEFAULT_MIXDEPTH): + enter_seed_extension_callback, enter_do_support_fidelity_bonds, mixdepth=DEFAULT_MIXDEPTH): entropy = None mnemonic_extension = None if method == "generate": @@ -537,7 +583,10 @@ def wallet_generate_recover_bip39(method, walletspath, default_wallet_name, wallet_name = default_wallet_name wallet_path = os.path.join(walletspath, wallet_name) - wallet = create_wallet(wallet_path, password, mixdepth, + support_fidelity_bonds = enter_do_support_fidelity_bonds() + wallet_cls = get_wallet_cls(get_configured_wallet_type(support_fidelity_bonds)) + + wallet = create_wallet(wallet_path, password, mixdepth, wallet_cls, entropy=entropy, entropy_extension=mnemonic_extension) mnemonic, mnext = wallet.get_mnemonic_words() @@ -555,7 +604,7 @@ def wallet_generate_recover(method, walletspath, default_wallet_name, cli_display_user_words, cli_user_mnemonic_entry, cli_get_wallet_passphrase_check, cli_get_wallet_file_name, cli_do_use_mnemonic_extension, cli_get_mnemonic_extension, - mixdepth=mixdepth) + cli_do_support_fidelity_bonds, mixdepth=mixdepth) entropy = None if method == 'recover': @@ -1035,29 +1084,54 @@ def wallet_freezeutxo(wallet, md, display_callback=None, info_callback=None): .format(fmt_utxo((txid, index)))) return "Done" -def get_wallet_type(): + + +def wallet_gettimelockaddress(wallet_service, locktime_string): + if not isinstance(wallet_service.wallet, FidelityBondMixin): + jmprint("Error: not a fidelity bond wallet", "error") + return "" + + m = FidelityBondMixin.FIDELITY_BOND_MIXDEPTH + address_type = FidelityBondMixin.BIP32_TIMELOCK_ID + index = wallet_service.get_next_unused_index(m, address_type) + lock_datetime = datetime.strptime(locktime_string, "%Y-%m") + timenumber = FidelityBondMixin.timestamp_to_time_number(timegm( + lock_datetime.timetuple())) + + path = wallet_service.get_path(m, address_type, index, timenumber) + jmprint("path = " + wallet_service.get_path_repr(path), "info") + jmprint("Coins sent to this address will be not be spendable until " + + lock_datetime.strftime("%B %Y") + ". Full date: " + + str(lock_datetime)) + addr = wallet_service.get_address_from_path(path) + return addr + +def get_configured_wallet_type(support_fidelity_bonds): + configured_type = TYPE_P2PKH if is_segwit_mode(): if is_native_segwit_mode(): - return TYPE_P2WPKH - return TYPE_P2SH_P2WPKH - return TYPE_P2PKH + configured_type = TYPE_P2WPKH + else: + configured_type = TYPE_P2SH_P2WPKH + if not support_fidelity_bonds: + return configured_type -def get_wallet_cls(wtype=None): - if wtype is None: - wtype = get_wallet_type() + if configured_type == TYPE_P2SH_P2WPKH: + return TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS + else: + raise ValueError("Fidelity bonds not supported with the configured " + "options of segwit and native. Edit joinmarket.cfg") +def get_wallet_cls(wtype): cls = WALLET_IMPLEMENTATIONS.get(wtype) - if not cls: raise WalletError("No wallet implementation found for type {}." "".format(wtype)) return cls - -def create_wallet(path, password, max_mixdepth, wallet_cls=None, **kwargs): +def create_wallet(path, password, max_mixdepth, wallet_cls, **kwargs): storage = Storage(path, password, create=True) - wallet_cls = wallet_cls or get_wallet_cls() wallet_cls.initialize(storage, get_network(), max_mixdepth=max_mixdepth, **kwargs) storage.save() @@ -1195,11 +1269,12 @@ def wallet_tool_main(wallet_root_path): wallet_root_path = os.path.join(jm_single().datadir, wallet_root_path) noseed_methods = ['generate', 'recover'] methods = ['display', 'displayall', 'summary', 'showseed', 'importprivkey', - 'history', 'showutxos', 'freeze'] + 'history', 'showutxos', 'freeze', 'gettimelockaddress'] methods.extend(noseed_methods) noscan_methods = ['showseed', 'importprivkey', 'dumpprivkey', 'signmessage'] readonly_methods = ['display', 'displayall', 'summary', 'showseed', - 'history', 'showutxos', 'dumpprivkey', 'signmessage'] + 'history', 'showutxos', 'dumpprivkey', 'signmessage', + 'gettimelockaddress'] if len(args) < 1: parser.error('Needs a wallet file or method') @@ -1273,9 +1348,17 @@ def wallet_tool_main(wallet_root_path): map_key_type(options.key_type)) return "Key import completed." elif method == "signmessage": + if len(args) < 3: + jmprint('Must provide message to sign', "error") + sys.exit(EXIT_ARGERROR) return wallet_signmessage(wallet_service, options.hd_path, args[2]) elif method == "freeze": return wallet_freezeutxo(wallet_service, options.mixdepth) + elif method == "gettimelockaddress": + if len(args) < 3: + jmprint('Must have locktime value yyyy-mm. For example 2021-03', "error") + sys.exit(EXIT_ARGERROR) + return wallet_gettimelockaddress(wallet_service, args[2]) else: parser.error("Unknown wallet-tool method: " + method) sys.exit(EXIT_ARGERROR) diff --git a/scripts/joinmarket-qt.py b/scripts/joinmarket-qt.py index a3bea2b..6764f88 100644 --- a/scripts/joinmarket-qt.py +++ b/scripts/joinmarket-qt.py @@ -1808,7 +1808,8 @@ class JMMainWindow(QMainWindow): enter_wallet_password_callback=self.getPassword, enter_wallet_file_name_callback=self.getWalletFileName, enter_if_use_seed_extension=self.promptUseMnemonicExtension, - enter_seed_extension_callback=self.promptInputMnemonicExtension) + enter_seed_extension_callback=self.promptInputMnemonicExtension, + enter_do_support_fidelity_bonds=lambda: False) if not success: JMQtMessageBox(self, "Failed to create new wallet file.",