diff --git a/CHANGELOG b/CHANGELOG index 4f58d87f..1a066191 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +RELEASE 0.6.15 - Small bugfixes, documentation updates +====================================================== +* Some small bugfixes +* New properties for WalletKey class for multisig wallets +* Add Bcoin documentation, add FAQ, update other documentation +* Add dockerfile for Linux Mint + RELEASE 0.6.14 - Update installation instruction, docker & bugfixes =================================================================== * Update installation instructions diff --git a/README.rst b/README.rst index 135ecf54..5e91f9da 100644 --- a/README.rst +++ b/README.rst @@ -4,14 +4,17 @@ Python Bitcoin Library Bitcoin cryptocurrency Library writen in Python. Allows you to create a fully functional Bitcoin wallet with a single line of code. -Use this library to create and manage transactions, addresses/keys, wallets, mnemonic password phrases and blocks with -simple and straightforward Python code. +Use this library to create and manage transactions, addresses/keys, wallets, mnemonic password phrases +and blocks with simple and straightforward Python code. You can use this library at a high level and create and manage wallets from the command line or at a low level and create your own custom made transactions, scripts, keys or wallets. The BitcoinLib connects to various service providers automatically to update wallets, transaction and -blockchain information. +blockchain information. You can also connect to a local +`Bitcoin `_ or +`Bcoin node `_. + .. image:: https://github.com/1200wd/bitcoinlib/actions/workflows/unittests.yaml/badge.svg :target: https://github.com/1200wd/bitcoinlib/actions/workflows/unittests.yaml @@ -30,7 +33,7 @@ blockchain information. Install ------- -Installed required packages +Install required packages on Ubuntu or related Linux systems: .. code-block:: bash @@ -42,9 +45,11 @@ Then install using pip $ pip install bitcoinlib -For more detailed installation instructions, how to install on other systems or troubleshooting please read https://bitcoinlib.readthedocs.io/en/latest/source/_static/manuals.install.html +Check out the `more detailed installation instructions `_ to read how to install on other systems or for +troubleshooting. -If you are using docker you can check some Dockerfiles to create images in the docker directory. +If you are using docker you can check some Dockerfiles to create images in the +`docker `_ directory. Documentation ------------- @@ -65,7 +70,7 @@ Example: Create wallet and generate new address (key) to receive bitcoins >>> from bitcoinlib.wallets import Wallet >>> w = Wallet.create('Wallet1') >>> w.get_key().address - '1Fo7STj6LdRhUuD1AiEsHpH65pXzraGJ9j' + 'bc1qk25wwkvz3am9smmm3372xct5s7cwf0hmnq8szj' Now send a small transaction to your wallet and use the scan() method to update transactions and UTXO's @@ -79,7 +84,7 @@ If successful a transaction ID is returned .. code-block:: pycon - >>> t = w.send_to('1PWXhWvUH3bcDWn6Fdq3xhMRPfxRXTjAi1', '0.001 BTC', offline=False) + >>> t = w.send_to('bc1qemtr8ywkzg483g8m34ukz2l4pl3730776vzq54', '0.001 BTC', offline=False) 'b7feea5e7c79d4f6f343b5ca28fa2a1fcacfe9a2b7f44f3d2fd8d6c2d82c4078' >>> t.info # Shows transaction information and send results @@ -87,15 +92,19 @@ If successful a transaction ID is returned More examples ------------- -Checkout the documentation page https://bitcoinlib.readthedocs.io/en/latest/ or take a look at some -more examples at https://github.com/1200wd/bitcoinlib/tree/master/examples +You can find many more examples in the `documentation `_ +for instance about the `Wallet.create() `_ method. + +There are many working examples on how to create wallets, specific transactions, encrypted databases, parse the +blockchain, connect to specific service providers in the `examples directory `_ in the source code of this library. +Some more specific examples can be found on the `Coineva website `_. Contact ------- -If you have any questions, encounter a problem or want to share an idea, please use Github Discussions -https://github.com/1200wd/bitcoinlib/discussions +If you have any questions, encounter a problem or want to share an idea, please use `Github Discussions +`_ Implements the following Bitcoin Improvement Proposals @@ -115,13 +124,12 @@ Implements the following Bitcoin Improvement Proposals Future / Roadmap ---------------- -- Support advanced scripts - Fully support timelocks -- Support for lightning network +- Support Taproot and Schnorr signatures +- Support advanced scripts - Support for Trezor wallet or other hardware wallets - Allow to scan full blockchain - Integrate simple SPV client -- Support Schnorr signatures Disclaimer diff --git a/bitcoinlib/config/VERSION b/bitcoinlib/config/VERSION index 5c858bde..7e3c84c3 100644 --- a/bitcoinlib/config/VERSION +++ b/bitcoinlib/config/VERSION @@ -1 +1 @@ -0.6.14 \ No newline at end of file +0.6.15 \ No newline at end of file diff --git a/bitcoinlib/services/bitcoind.py b/bitcoinlib/services/bitcoind.py index f8196e88..464941f4 100644 --- a/bitcoinlib/services/bitcoind.py +++ b/bitcoinlib/services/bitcoind.py @@ -53,10 +53,13 @@ class BitcoindClient(BaseClient): """ @staticmethod - def from_config(configfile=None, network='bitcoin', *args): + @deprecated + def from_config(configfile=None, network='bitcoin', **kwargs): """ Read settings from bitcoind config file + Obsolete: does not work anymore, passwords are not stored in bitcoin config, only hashed password. + :param configfile: Path to config file. Leave empty to look in default places :type: str :param network: Bitcoin mainnet or testnet. Default is bitcoin mainnet @@ -113,7 +116,7 @@ def from_config(configfile=None, network='bitcoin', *args): server = _read_from_config(config, 'rpc', 'externalip', server) url = "http://%s:%s@%s:%s" % (config.get('rpc', 'rpcuser'), config.get('rpc', 'rpcpassword'), server, port) - return BitcoindClient(network, url, *args) + return BitcoindClient(network, url, **kwargs) def __init__(self, network='bitcoin', base_url='', denominator=100000000, *args): """ @@ -129,6 +132,7 @@ def __init__(self, network='bitcoin', base_url='', denominator=100000000, *args) if isinstance(network, Network): network = network.name if not base_url: + _logger.warning("Please provide rpc connection url to bitcoind node") bdc = self.from_config('', network) base_url = bdc.base_url network = bdc.network @@ -332,12 +336,9 @@ def getinfo(self): from pprint import pprint - # 1. Connect by specifying connection URL - # base_url = 'http://bitcoinrpc:passwd@host:8332' - # bdc = BitcoindClient(base_url=base_url) - - # 2. Or connect using default settings or settings from config file - bdc = BitcoindClient() + # Connect by specifying connection URL + base_url = 'http://bitcoinrpc:passwd@host:8332' + bdc = BitcoindClient(base_url=base_url) print("\n=== SERVERINFO ===") pprint(bdc.proxy.getnetworkinfo()) diff --git a/bitcoinlib/services/services.py b/bitcoinlib/services/services.py index 630f298f..f8d6a9d8 100644 --- a/bitcoinlib/services/services.py +++ b/bitcoinlib/services/services.py @@ -55,7 +55,7 @@ class Service(object): def __init__(self, network=DEFAULT_NETWORK, min_providers=1, max_providers=1, providers=None, timeout=TIMEOUT_REQUESTS, cache_uri=None, ignore_priority=False, exclude_providers=None, - max_errors=SERVICE_MAX_ERRORS, strict=True): + max_errors=SERVICE_MAX_ERRORS, strict=True, wallet_name=None): """ Create a service object for the specified network. By default, the object connect to 1 service provider, but you can specify a list of providers or a minimum or maximum number of providers. @@ -131,6 +131,7 @@ def __init__(self, network=DEFAULT_NETWORK, min_providers=1, max_providers=1, pr self._blockcount = None self.cache = None self.cache_uri = cache_uri + self.wallet_name = wallet_name try: self.cache = Cache(self.network, db_uri=cache_uri) except Exception as e: @@ -166,8 +167,13 @@ def _provider_execute(self, method, *arguments): continue client = getattr(services, self.providers[sp]['provider']) providerclient = getattr(client, self.providers[sp]['client_class']) + + base_url = self.providers[sp]['url'] + if 'bitcoind' in sp and self.wallet_name is not None: + base_url = f"{base_url}/wallet/{self.wallet_name}" + pc_instance = providerclient( - self.network, self.providers[sp]['url'], self.providers[sp]['denominator'], + self.network, base_url, self.providers[sp]['denominator'], self.providers[sp]['api_key'], self.providers[sp]['provider_coin_id'], self.providers[sp]['network_overrides'], self.timeout, self._blockcount, self.strict) if not hasattr(pc_instance, method): diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index b9943639..66b0d533 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -58,8 +58,7 @@ def wallets_list(db_uri=None, include_cosigners=False, db_password=None): :type db_uri: str :param include_cosigners: Child wallets for multisig wallets are for internal use only and are skipped by default :type include_cosigners: bool - :param db_password: Password to use for encrypted database. Requires the installation of sqlcipher (see - documentation). + :param db_password: Password to use for encrypted database. Requires the installation of sqlcipher (see documentation). :type db_password: str :return dict: Dictionary of wallets defined in database @@ -109,7 +108,7 @@ def wallet_exists(wallet, db_uri=None, db_password=None): def wallet_create_or_open( name, keys='', owner='', network=None, account_id=0, purpose=None, scheme='bip32', sort_keys=True, password='', witness_type=None, encoding=None, multisig=None, sigs_required=None, cosigner_id=None, - key_path=None, anti_fee_sniping=True, db_uri=None, db_cache_uri=None, db_password=None): + key_path=None, db_uri=None, db_cache_uri=None, db_password=None): """ Create a wallet with specified options if it doesn't exist, otherwise just open @@ -124,8 +123,7 @@ def wallet_create_or_open( else: return Wallet.create(name, keys, owner, network, account_id, purpose, scheme, sort_keys, password, witness_type, encoding, multisig, sigs_required, cosigner_id, - key_path, anti_fee_sniping, db_uri=db_uri, db_cache_uri=db_cache_uri, - db_password=db_password) + key_path, db_uri=db_uri, db_cache_uri=db_cache_uri, db_password=db_password) def wallet_delete(wallet, db_uri=None, force=False, db_password=None): @@ -302,9 +300,9 @@ class WalletKey(object): """ @staticmethod - def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0, purpose=84, parent_id=0, + def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0, purpose=44, parent_id=0, path='m', key_type=None, encoding=None, witness_type=DEFAULT_WITNESS_TYPE, multisig=False, - cosigner_id=None, new_key_id=None): + cosigner_id=None): """ Create WalletKey from a HDKey object or key. @@ -312,7 +310,7 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0 >>> w = wallet_create_or_open('hdwalletkey_test') >>> wif = 'xprv9s21ZrQH143K2mcs9jcK4EjALbu2z1N9qsMTUG1frmnXM3NNCSGR57yLhwTccfNCwdSQEDftgjCGm96P29wGGcbBsPqZH85iqpoHA7LrqVy' - >>> wk = WalletKey.from_key('import_key', w.wallet_id, w.session, wif) + >>> wk = WalletKey.from_key('import_key', w.wallet_id, w._session, wif) >>> wk.address '1MwVEhGq6gg1eeSrEdZom5bHyPqXtJSnPg' >>> wk # doctest:+ELLIPSIS @@ -332,7 +330,7 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0 :type network: str :param change: Use 0 for normal key, and 1 for change key (for returned payments) :type change: int - :param purpose: BIP0044 purpose field, default is 84 + :param purpose: BIP0044 purpose field, default is 44 :type purpose: int :param parent_id: Key ID of parent, default is 0 (no parent) :type parent_id: int @@ -348,8 +346,6 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0 :type multisig: bool :param cosigner_id: Set this if you would like to create keys for other cosigners. :type cosigner_id: int - :param new_key_id: Key ID in database (DbKey.id), use to directly insert key in database without checks and without commiting. Mainly for internal usage, to significantly increase speed when inserting multiple keys. - :type new_key_id: int :return WalletKey: WalletKey object """ @@ -361,7 +357,6 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0 network = k.network.name elif network != k.network.name: raise WalletError("Specified network and key network should be the same") - witness_type = k.witness_type elif isinstance(key, Address): k = key key_is_address = True @@ -372,78 +367,65 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0 else: if network is None: network = DEFAULT_NETWORK - k = HDKey(import_key=key, network=network, witness_type=witness_type) + k = HDKey(import_key=key, network=network) if not encoding and witness_type: encoding = get_encoding_from_witness(witness_type) script_type = script_type_default(witness_type, multisig) - if not new_key_id: - key_id_max = session.query(func.max(DbKey.id)).scalar() - new_key_id = key_id_max + 1 if key_id_max else None - commit = True - else: - commit = False - if not key_is_address: + keyexists = session.query(DbKey).\ + filter(DbKey.wallet_id == wallet_id, + DbKey.wif == k.wif(witness_type=witness_type, multisig=multisig, is_private=True)).first() + if keyexists: + _logger.warning("Key already exists in this wallet. Key ID: %d" % keyexists.id) + return WalletKey(keyexists.id, session, k) + if key_type != 'single' and k.depth != len(path.split('/'))-1: if path == 'm' and k.depth > 1: path = "M" - address = k.address(encoding=encoding, script_type=script_type) - if commit: - keyexists = session.query(DbKey).\ - filter(DbKey.wallet_id == wallet_id, - DbKey.wif == k.wif(witness_type=witness_type, multisig=multisig, is_private=True)).first() - if keyexists: - _logger.warning("Key already exists in this wallet. Key ID: %d" % keyexists.id) - return WalletKey(keyexists.id, session, k) - - wk = session.query(DbKey).filter( - DbKey.wallet_id == wallet_id, - or_(DbKey.public == k.public_byte, - DbKey.wif == k.wif(witness_type=witness_type, multisig=multisig, is_private=False), - DbKey.address == address)).first() - if wk: - wk.wif = k.wif(witness_type=witness_type, multisig=multisig, is_private=True) - wk.is_private = True - wk.private = k.private_byte - wk.public = k.public_byte - wk.path = path - session.commit() - return WalletKey(wk.id, session, k) - - address_index = k.child_index % 0x80000000 - nk = DbKey(id=new_key_id, name=name[:80], wallet_id=wallet_id, public=k.public_byte, private=k.private_byte, purpose=purpose, - account_id=account_id, depth=k.depth, change=change, address_index=address_index, + address = k.address(encoding=encoding, script_type=script_type) + wk = session.query(DbKey).filter( + DbKey.wallet_id == wallet_id, + or_(DbKey.public == k.public_byte, + DbKey.wif == k.wif(witness_type=witness_type, multisig=multisig, is_private=False), + DbKey.address == address)).first() + if wk: + wk.wif = k.wif(witness_type=witness_type, multisig=multisig, is_private=True) + wk.is_private = True + wk.private = k.private_byte + wk.public = k.public_byte + wk.path = path + session.commit() + return WalletKey(wk.id, session, k) + + nk = DbKey(name=name[:80], wallet_id=wallet_id, public=k.public_byte, private=k.private_byte, purpose=purpose, + account_id=account_id, depth=k.depth, change=change, address_index=k.child_index, wif=k.wif(witness_type=witness_type, multisig=multisig, is_private=True), address=address, parent_id=parent_id, compressed=k.compressed, is_private=k.is_private, path=path, - key_type=key_type, network_name=network, encoding=encoding, cosigner_id=cosigner_id, - witness_type=witness_type) + key_type=key_type, network_name=network, encoding=encoding, cosigner_id=cosigner_id) else: keyexists = session.query(DbKey).\ filter(DbKey.wallet_id == wallet_id, DbKey.address == k.address).first() if keyexists: - _logger.warning("Key %s with ID %s already exists" % (k.address, keyexists.id)) + _logger.warning("Key with ID %s already exists" % keyexists.id) return WalletKey(keyexists.id, session, k) - nk = DbKey(id=new_key_id, name=name[:80], wallet_id=wallet_id, purpose=purpose, + nk = DbKey(name=name[:80], wallet_id=wallet_id, purpose=purpose, account_id=account_id, depth=k.depth, change=change, address=k.address, parent_id=parent_id, compressed=k.compressed, is_private=False, path=path, - key_type=key_type, network_name=network, encoding=encoding, cosigner_id=cosigner_id, - witness_type=witness_type) + key_type=key_type, network_name=network, encoding=encoding, cosigner_id=cosigner_id) - if commit: - session.merge(DbNetwork(name=network)) + session.merge(DbNetwork(name=network)) session.add(nk) - if commit: - session.commit() + session.commit() return WalletKey(nk.id, session, k) def _commit(self): try: - self.session.commit() + self._session.commit() except Exception: - self.session.rollback() + self._session.rollback() raise def __init__(self, key_id, session, hdkey_object=None): @@ -459,7 +441,7 @@ def __init__(self, key_id, session, hdkey_object=None): """ - self.session = session + self._session = session wk = session.query(DbKey).filter_by(id=key_id).first() if wk: self._dbkey = wk @@ -494,13 +476,9 @@ def __init__(self, key_id, session, hdkey_object=None): self.encoding = wk.encoding self.cosigner_id = wk.cosigner_id self.used = wk.used - self.witness_type = wk.witness_type else: raise WalletError("Key with id %s not found" % key_id) - def __del__(self): - self.session.close() - def __repr__(self): return "" % (self.key_id, self.name, self.wif, self.path) @@ -694,7 +672,7 @@ def from_txid(cls, hdwallet, txid): :return WalletClass: """ - sess = hdwallet.session + sess = hdwallet._session # If txid is unknown add it to database, else update db_tx_query = sess.query(DbTransaction). \ filter(DbTransaction.wallet_id == hdwallet.wallet_id, DbTransaction.txid == to_bytes(txid)) @@ -840,7 +818,7 @@ def send(self, offline=False): # Update db: Update spent UTXO's, add transaction to database for inp in self.inputs: txid = inp.prev_txid - utxos = self.hdwallet.session.query(DbTransactionOutput).join(DbTransaction).\ + utxos = self.hdwallet._session.query(DbTransactionOutput).join(DbTransaction).\ filter(DbTransaction.txid == txid, DbTransactionOutput.output_n == inp.output_n_int, DbTransactionOutput.spent.is_(False)).all() @@ -859,7 +837,7 @@ def store(self): :return int: Transaction index number """ - sess = self.hdwallet.session + sess = self.hdwallet._session # If txid is unknown add it to database, else update db_tx_query = sess.query(DbTransaction). \ filter(DbTransaction.wallet_id == self.hdwallet.wallet_id, DbTransaction.txid == bytes.fromhex(self.txid)) @@ -876,8 +854,7 @@ def store(self): wallet_id=self.hdwallet.wallet_id, txid=bytes.fromhex(self.txid), block_height=self.block_height, size=self.size, confirmations=self.confirmations, date=self.date, fee=self.fee, status=self.status, input_total=self.input_total, output_total=self.output_total, network_name=self.network.name, - raw=self.rawtx, verified=self.verified, account_id=self.account_id, locktime=self.locktime, - version=self.version_int, coinbase=self.coinbase, index=self.index) + raw=self.rawtx, verified=self.verified, account_id=self.account_id) sess.add(new_tx) self.hdwallet._commit() txidn = new_tx.id @@ -893,7 +870,6 @@ def store(self): db_tx.network_name = self.network.name if self.network.name else db_tx.name db_tx.raw = self.rawtx if self.rawtx else db_tx.raw db_tx.verified = self.verified - db_tx.locktime = self.locktime self.hdwallet._commit() assert txidn @@ -1017,7 +993,7 @@ def delete(self): :return int: Number of deleted transactions """ - session = self.hdwallet.session + session = self.hdwallet._session txid = bytes.fromhex(self.txid) tx_query = session.query(DbTransaction).filter_by(txid=txid) tx = tx_query.scalar() @@ -1045,8 +1021,8 @@ class Wallet(object): @classmethod def _create(cls, name, key, owner, network, account_id, purpose, scheme, parent_id, sort_keys, - witness_type, encoding, multisig, sigs_required, cosigner_id, key_path, - anti_fee_sniping, db_uri, db_cache_uri, db_password): + witness_type, encoding, multisig, sigs_required, cosigner_id, key_path, db_uri, db_cache_uri, + db_password): db = Db(db_uri, db_password) session = db.session @@ -1086,7 +1062,7 @@ def _create(cls, name, key, owner, network, account_id, purpose, scheme, parent_ new_wallet = DbWallet(name=name, owner=owner, network_name=network, purpose=purpose, scheme=scheme, sort_keys=sort_keys, witness_type=witness_type, parent_id=parent_id, encoding=encoding, multisig=multisig, multisig_n_required=sigs_required, cosigner_id=cosigner_id, - key_path=key_path, anti_fee_sniping=anti_fee_sniping) + key_path=key_path) session.add(new_wallet) session.commit() new_wallet_id = new_wallet.id @@ -1101,7 +1077,7 @@ def _create(cls, name, key, owner, network, account_id, purpose, scheme, parent_ session.commit() w = cls(new_wallet_id, db_uri=db_uri, db_cache_uri=db_cache_uri, main_key_object=mk.key()) - w.key_for_path([], account_id=account_id, cosigner_id=cosigner_id, change=0, address_index=0) + w.key_for_path([0, 0], account_id=account_id, cosigner_id=cosigner_id) else: # scheme == 'single': if not key: key = HDKey(network=network, depth=key_depth) @@ -1117,16 +1093,15 @@ def _create(cls, name, key, owner, network, account_id, purpose, scheme, parent_ def _commit(self): try: - self.session.commit() + self._session.commit() except Exception: - self.session.rollback() + self._session.rollback() raise @classmethod def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0, scheme='bip32', sort_keys=True, password='', witness_type=None, encoding=None, multisig=None, sigs_required=None, - cosigner_id=None, key_path=None, anti_fee_sniping=True, db_uri=None, db_cache_uri=None, - db_password=None): + cosigner_id=None, key_path=None, db_uri=None, db_cache_uri=None, db_password=None): """ Create Wallet and insert in database. Generate masterkey or import key when specified. @@ -1180,8 +1155,7 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 :type sort_keys: bool :param password: Password to protect passphrase, only used if a passphrase is supplied in the 'key' argument. :type password: str - :param witness_type: Specify witness type, default is 'segwit', for native segregated witness - wallet. Use 'legacy' for an old-style wallets or 'p2sh-segwit' for legacy compatible wallets + :param witness_type: Specify witness type, default is 'legacy'. Use 'segwit' for native segregated witness wallet, or 'p2sh-segwit' for legacy compatible wallets :type witness_type: str :param encoding: Encoding used for address generation: base58 or bech32. Default is derive from wallet and/or witness type :type encoding: str @@ -1197,8 +1171,6 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 * All keys must be hardened, except for change, address_index or cosigner_id * Max length of path is 8 levels :type key_path: list, str - :param anti_fee_sniping: Set default locktime in transactions as current block height + 1 to avoid fee-sniping. Default is True, which will make the network more secure. You could disable it to avoid transaction fingerprinting. - :type anti_fee_sniping: boolean :param db_uri: URI of the database for wallets, wallet transactions and keys :type db_uri: str :param db_cache_uri: URI of the cache database. If not specified the default cache database is used when using sqlite, for other databasetypes the cache database is merged with the wallet database (db_uri) @@ -1251,14 +1223,14 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 # If key consists of several words assume it is a passphrase and convert it to a HDKey object if isinstance(key, str) and len(key.split(" ")) > 1: if not network: - network = DEFAULT_NETWORK - key = HDKey.from_seed(Mnemonic().to_seed(key, password), network=network, witness_type=witness_type) + raise WalletError("Please specify network when using passphrase to create a key") + key = HDKey.from_seed(Mnemonic().to_seed(key, password), network=network) else: try: if isinstance(key, WalletKey): key = key._hdkey_object else: - key = HDKey(key, password=password, witness_type=witness_type, network=network) + key = HDKey(key, password=password, network=network) except BKeyError: try: scheme = 'single' @@ -1275,7 +1247,7 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 network = DEFAULT_NETWORK if witness_type is None: witness_type = DEFAULT_WITNESS_TYPE - if network in ['dogecoin', 'dogecoin_testnet'] and witness_type != 'legacy': + if network in ['dash', 'dash_testnet', 'dogecoin', 'dogecoin_testnet'] and witness_type != 'legacy': raise WalletError("Segwit is not supported for %s wallets" % network.capitalize()) elif network in ('dogecoin', 'dogecoin_testnet') and witness_type not in ('legacy', 'p2sh-segwit'): raise WalletError("Pure segwit addresses are not supported for Dogecoin wallets. " @@ -1286,7 +1258,16 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 key_path = ['m'] purpose = 0 else: - key_path, purpose, encoding = get_key_structure_data(witness_type, multisig, purpose, encoding) + ks = [k for k in WALLET_KEY_STRUCTURES if k['witness_type'] == witness_type and + k['multisig'] == multisig and k['purpose'] is not None] + if len(ks) > 1: + raise WalletError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for " + "witness_type - multisig combination") + if ks and not purpose: + purpose = ks[0]['purpose'] + if ks and not encoding: + encoding = ks[0]['encoding'] + key_path = ks[0]['key_path'] else: if purpose is None: purpose = 0 @@ -1317,8 +1298,7 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 hdpm = cls._create(name, key, owner=owner, network=network, account_id=account_id, purpose=purpose, scheme=scheme, parent_id=None, sort_keys=sort_keys, witness_type=witness_type, encoding=encoding, multisig=multisig, sigs_required=sigs_required, cosigner_id=cosigner_id, - anti_fee_sniping=anti_fee_sniping, key_path=main_key_path, db_uri=db_uri, - db_cache_uri=db_cache_uri, db_password=db_password) + key_path=main_key_path, db_uri=db_uri, db_cache_uri=db_cache_uri, db_password=db_password) if multisig: wlt_cos_id = 0 @@ -1336,14 +1316,13 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 purpose=hdpm.purpose, scheme=scheme, parent_id=hdpm.wallet_id, sort_keys=sort_keys, witness_type=hdpm.witness_type, encoding=encoding, multisig=True, sigs_required=None, cosigner_id=wlt_cos_id, key_path=c_key_path, - anti_fee_sniping=anti_fee_sniping, db_uri=db_uri, db_cache_uri=db_cache_uri, - db_password=db_password) + db_uri=db_uri, db_cache_uri=db_cache_uri, db_password=db_password) hdpm.cosigner.append(w) wlt_cos_id += 1 - # hdpm._dbwallet = hdpm.session.query(DbWallet).filter(DbWallet.id == hdpm.wallet_id) + # hdpm._dbwallet = hdpm._session.query(DbWallet).filter(DbWallet.id == hdpm.wallet_id) # hdpm._dbwallet.update({DbWallet.cosigner_id: hdpm.cosigner_id}) # hdpm._dbwallet.update({DbWallet.key_path: hdpm.key_path}) - # hdpm.session.commit() + # hdpm._session.commit() return hdpm @@ -1366,17 +1345,18 @@ def __init__(self, wallet, db_uri=None, db_cache_uri=None, session=None, main_ke :type main_key_object: HDKey """ - self._session = None - self._engine = None if session: self._session = session - self._db_password = db_password + else: + dbinit = Db(db_uri=db_uri, password=db_password) + self._session = dbinit.session + self._engine = dbinit.engine self.db_uri = db_uri self.db_cache_uri = db_cache_uri if isinstance(wallet, int) or wallet.isdigit(): - db_wlt = self.session.query(DbWallet).filter_by(id=wallet).scalar() + db_wlt = self._session.query(DbWallet).filter_by(id=wallet).scalar() else: - db_wlt = self.session.query(DbWallet).filter_by(name=wallet).scalar() + db_wlt = self._session.query(DbWallet).filter_by(name=wallet).scalar() if db_wlt: self._dbwallet = db_wlt self.wallet_id = db_wlt.id @@ -1391,12 +1371,12 @@ def __init__(self, wallet, db_uri=None, db_cache_uri=None, session=None, main_ke self.main_key = None self._default_account_id = db_wlt.default_account_id self.multisig_n_required = db_wlt.multisig_n_required - co_sign_wallets = self.session.query(DbWallet).\ + co_sign_wallets = self._session.query(DbWallet).\ filter(DbWallet.parent_id == self.wallet_id).order_by(DbWallet.name).all() self.cosigner = [Wallet(w.id, db_uri=db_uri, db_cache_uri=db_cache_uri) for w in co_sign_wallets] self.sort_keys = db_wlt.sort_keys if db_wlt.main_key_id: - self.main_key = WalletKey(self.main_key_id, session=self.session, hdkey_object=main_key_object) + self.main_key = WalletKey(self.main_key_id, session=self._session, hdkey_object=main_key_object) if self._default_account_id is None: self._default_account_id = 0 if self.main_key: @@ -1423,20 +1403,19 @@ def __init__(self, wallet, db_uri=None, db_cache_uri=None, session=None, main_ke self.depth_public_master = self.key_path.index(hardened_keys[-1]) self.key_depth = len(self.key_path) - 1 self.last_updated = None - self.anti_fee_sniping = db_wlt.anti_fee_sniping else: raise WalletError("Wallet '%s' not found, please specify correct wallet ID or name." % wallet) def __exit__(self, exception_type, exception_value, traceback): try: - self.session.close() + self._session.close() self._engine.dispose() except Exception: pass def __del__(self): try: - self.session.close() + self._session.close() self._engine.dispose() except Exception: pass @@ -1474,7 +1453,7 @@ def _get_account_defaults(self, network=None, account_id=None, key_id=None): network = self.network.name if account_id is None and network == self.network.name: account_id = self.default_account_id - qr = self.session.query(DbKey).\ + qr = self._session.query(DbKey).\ filter_by(wallet_id=self.wallet_id, purpose=self.purpose, depth=self.depth_public_master, network_name=network) if account_id is not None: @@ -1497,7 +1476,7 @@ def default_account_id(self): @default_account_id.setter def default_account_id(self, value): self._default_account_id = value - self._dbwallet = self.session.query(DbWallet).filter(DbWallet.id == self.wallet_id). \ + self._dbwallet = self._session.query(DbWallet).filter(DbWallet.id == self.wallet_id). \ update({DbWallet.default_account_id: value}) self._commit() @@ -1523,7 +1502,7 @@ def owner(self, value): """ self._owner = value - self._dbwallet = self.session.query(DbWallet).filter(DbWallet.id == self.wallet_id).\ + self._dbwallet = self._session.query(DbWallet).filter(DbWallet.id == self.wallet_id).\ update({DbWallet.owner: value}) self._commit() @@ -1551,22 +1530,14 @@ def name(self, value): if wallet_exists(value, db_uri=self.db_uri): raise WalletError("Wallet with name '%s' already exists" % value) self._name = value - self.session.query(DbWallet).filter(DbWallet.id == self.wallet_id).update({DbWallet.name: value}) + self._session.query(DbWallet).filter(DbWallet.id == self.wallet_id).update({DbWallet.name: value}) self._commit() - @property - def session(self): - if not self._session: - dbinit = Db(db_uri=self.db_uri, password=self._db_password) - self._session = dbinit.session - self._engine = dbinit.engine - return self._session - def default_network_set(self, network): if not isinstance(network, Network): network = Network(network) self.network = network - self.session.query(DbWallet).filter(DbWallet.id == self.wallet_id).\ + self._session.query(DbWallet).filter(DbWallet.id == self.wallet_id).\ update({DbWallet.network_name: network.name}) self._commit() @@ -1594,6 +1565,7 @@ def import_master_key(self, hdkey, name='Masterkey (imported)'): if (self.main_key.depth != 1 and self.main_key.depth != 3 and self.main_key.depth != 4) or \ self.main_key.key_type != 'bip32': raise WalletError("Current main key is not a valid BIP32 public master key") + # pm = self.public_master() if not (self.network.name == self.main_key.network.name == hdkey.network.name): raise WalletError("Network of Wallet class, main account key and the imported private key must use " "the same network") @@ -1601,19 +1573,18 @@ def import_master_key(self, hdkey, name='Masterkey (imported)'): raise WalletError("This key does not correspond to current public master key") hdkey.key_type = 'bip32' - # ks = [k for k in WALLET_KEY_STRUCTURES if - # k['witness_type'] == self.witness_type and k['multisig'] == self.multisig and k['purpose'] is not None] - # if len(ks) > 1: - # raise WalletError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for " - # "witness_type - multisig combination") - # self.key_path = ks[0]['key_path'] - self.key_path, _, _ = get_key_structure_data(self.witness_type, self.multisig) + ks = [k for k in WALLET_KEY_STRUCTURES if + k['witness_type'] == self.witness_type and k['multisig'] == self.multisig and k['purpose'] is not None] + if len(ks) > 1: + raise WalletError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for " + "witness_type - multisig combination") + self.key_path = ks[0]['key_path'] self.main_key = WalletKey.from_key( - key=hdkey, name=name, session=self.session, wallet_id=self.wallet_id, network=network, + key=hdkey, name=name, session=self._session, wallet_id=self.wallet_id, network=network, account_id=account_id, purpose=self.purpose, key_type='bip32', witness_type=self.witness_type) self.main_key_id = self.main_key.key_id self._key_objects.update({self.main_key_id: self.main_key}) - self.session.query(DbWallet).filter(DbWallet.id == self.wallet_id).\ + self._session.query(DbWallet).filter(DbWallet.id == self.wallet_id).\ update({DbWallet.main_key_id: self.main_key_id}) for key in self.keys(is_private=False): @@ -1625,7 +1596,7 @@ def import_master_key(self, hdkey, name='Masterkey (imported)'): self._commit() return self.main_key - def import_key(self, key, account_id=0, name='', network=None, purpose=84, key_type=None): + def import_key(self, key, account_id=0, name='', network=None, purpose=44, key_type=None): """ Add new single key to wallet. @@ -1637,7 +1608,7 @@ def import_key(self, key, account_id=0, name='', network=None, purpose=84, key_t :type name: str :param network: Network name, method will try to extract from key if not specified. Raises warning if network could not be detected :type network: str - :param purpose: BIP44 definition used, default is 84 (segwit) + :param purpose: BIP definition used, default is BIP44 :type purpose: int :param key_type: Key type of imported key, can be single. Unrelated to wallet, bip32, bip44 or master for new or extra master key import. Default is 'single' :type key_type: str @@ -1664,7 +1635,7 @@ def import_key(self, key, account_id=0, name='', network=None, purpose=84, key_t if network not in self.network_list(): raise WalletError("Network %s not available in this wallet, please create an account for this " "network first." % network) - hdkey = HDKey(key, network=network, key_type=key_type, witness_type=self.witness_type) + hdkey = HDKey(key, network=network, key_type=key_type) if not self.multisig: if self.main_key and self.main_key.depth == self.depth_public_master and \ @@ -1679,7 +1650,7 @@ def import_key(self, key, account_id=0, name='', network=None, purpose=84, key_t if key_type == 'single': # Create path for unrelated import keys hdkey.depth = self.key_depth - last_import_key = self.session.query(DbKey).filter(DbKey.path.like("import_key_%")).\ + last_import_key = self._session.query(DbKey).filter(DbKey.path.like("import_key_%")).\ order_by(DbKey.path.desc()).first() if last_import_key: ik_path = "import_key_" + str(int(last_import_key.path[-5:]) + 1).zfill(5) @@ -1690,7 +1661,7 @@ def import_key(self, key, account_id=0, name='', network=None, purpose=84, key_t mk = WalletKey.from_key( key=hdkey, name=name, wallet_id=self.wallet_id, network=network, key_type=key_type, - account_id=account_id, purpose=purpose, session=self.session, path=ik_path, + account_id=account_id, purpose=purpose, session=self._session, path=ik_path, witness_type=self.witness_type) self._key_objects.update({mk.key_id: mk}) if mk.key_id == self.main_key.key_id: @@ -1704,21 +1675,23 @@ def import_key(self, key, account_id=0, name='', network=None, purpose=84, key_t return w.import_master_key(hdkey) raise WalletError("Unknown key: Can only import a private key for a known public key in multisig wallets") - def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, network, address_index, - witness_type): + def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, network, address_index): if self.sort_keys: public_keys.sort(key=lambda pubk: pubk.key_public) public_key_list = [pubk.key_public for pubk in public_keys] public_key_ids = [str(x.key_id) for x in public_keys] + # Calculate redeemscript and address and add multisig key to database + # redeemscript = serialize_multisig_redeemscript(public_key_list, n_required=self.multisig_n_required) + # todo: pass key object, reuse key objects redeemscript = Script(script_types=['multisig'], keys=public_key_list, sigs_required=self.multisig_n_required).serialize() script_type = 'p2sh' - if witness_type == 'p2sh-segwit': + if self.witness_type == 'p2sh-segwit': script_type = 'p2sh_p2wsh' - address = Address(redeemscript, script_type=script_type, network=network, witness_type=witness_type) - already_found_key = self.session.query(DbKey).filter_by(wallet_id=self.wallet_id, + address = Address(redeemscript, encoding=self.encoding, script_type=script_type, network=network) + already_found_key = self._session.query(DbKey).filter_by(wallet_id=self.wallet_id, address=address.address).first() if already_found_key: return self.key(already_found_key.id) @@ -1727,43 +1700,20 @@ def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, if not name: name = "Multisig Key " + '/'.join(public_key_ids) - new_key_id = (self.session.query(func.max(DbKey.id)).scalar() or 0) + 1 - multisig_key = DbKey(id=new_key_id, + multisig_key = DbKey( name=name[:80], wallet_id=self.wallet_id, purpose=self.purpose, account_id=account_id, depth=depth, change=change, address_index=address_index, parent_id=0, is_private=False, path=path, public=address.hash_bytes, wif='multisig-%s' % address, address=address.address, cosigner_id=cosigner_id, - key_type='multisig', witness_type=witness_type, network_name=network) - self.session.add(multisig_key) + key_type='multisig', network_name=network) + self._session.add(multisig_key) self._commit() for child_id in public_key_ids: - self.session.add(DbKeyMultisigChildren(key_order=public_key_ids.index(child_id), parent_id=multisig_key.id, + self._session.add(DbKeyMultisigChildren(key_order=public_key_ids.index(child_id), parent_id=multisig_key.id, child_id=int(child_id))) self._commit() return self.key(multisig_key.id) - def new_key(self, name='', account_id=None, change=0, cosigner_id=None, witness_type=None, network=None): - """ - def new_key(self, name='', account_id=None, change=0, cosigner_id=None, witness_type=None, network=None): - - :param name: Key name. Does not have to be unique but if you use it at reference you might chooce to enforce this. If not specified 'Key #' with a unique sequence number will be used - :type name: str - :param account_id: Account ID. Default is last used or created account ID. - :type account_id: int - :param change: Change (1) or payments (0). Default is 0 - :type change: int - :param cosigner_id: Cosigner ID for key path - :type cosigner_id: int - :param witness_type: Use to create key with different witness_type - :type witness_type: str - :param network: Network name. Leave empty for default network - :type network: str - - :return WalletKey: - """ - return self.new_keys(name, account_id, change, cosigner_id, witness_type, 1, network)[0] - - def new_keys(self, name='', account_id=None, change=0, cosigner_id=None, witness_type=None, - number_of_keys=1, network=None): + def new_key(self, name='', account_id=None, change=0, cosigner_id=None, network=None): """ Create a new HD Key derived from this wallet's masterkey. An account will be created for this wallet with index 0 if there is no account defined yet. @@ -1780,18 +1730,15 @@ def new_keys(self, name='', account_id=None, change=0, cosigner_id=None, witness :type change: int :param cosigner_id: Cosigner ID for key path :type cosigner_id: int - :param witness_type: Use to create key with different witness_type - :type witness_type: str - :param number_of_keys: Number of keys to generate. Use positive integer - :type number_of_keys: int - :param network: Network name. Leave empty for default network + :param network: Network name. Leave empty for default network :type network: str - :return list of WalletKey: + :return WalletKey: """ if self.scheme == 'single': - return [self.main_key] + return self.main_key + network, account_id, _ = self._get_account_defaults(network, account_id) if network != self.network.name and "coin_type'" not in self.key_path: raise WalletError("Multiple networks not supported by wallet key structure") @@ -1802,27 +1749,23 @@ def new_keys(self, name='', account_id=None, change=0, cosigner_id=None, witness if self.cosigner_id is None: raise WalletError("Missing Cosigner ID value, cannot create new key") cosigner_id = self.cosigner_id - witness_type = self.witness_type if not witness_type else witness_type - purpose = self.purpose - if witness_type != self.witness_type: - _, purpose, encoding = get_key_structure_data(witness_type, self.multisig) address_index = 0 - if not((self.multisig and cosigner_id is not None and - (len(self.cosigner) > cosigner_id and self.cosigner[cosigner_id].key_path == 'm' or - self.cosigner[cosigner_id].key_path == ['m']))): - prevkey = self.session.query(DbKey).\ - filter_by(wallet_id=self.wallet_id, purpose=purpose, network_name=network, account_id=account_id, - witness_type=witness_type, change=change, cosigner_id=cosigner_id, depth=self.key_depth).\ + if self.multisig and cosigner_id is not None and (len(self.cosigner) > cosigner_id and self.cosigner[cosigner_id].key_path == 'm' or self.cosigner[cosigner_id].key_path == ['m']): + req_path = [] + else: + prevkey = self._session.query(DbKey).\ + filter_by(wallet_id=self.wallet_id, purpose=self.purpose, network_name=network, account_id=account_id, + change=change, cosigner_id=cosigner_id, depth=self.key_depth).\ order_by(DbKey.address_index.desc()).first() if prevkey: address_index = prevkey.address_index + 1 + req_path = [change, address_index] - return self.keys_for_path([], name=name, account_id=account_id, witness_type=witness_type, network=network, - cosigner_id=cosigner_id, address_index=address_index, number_of_keys=number_of_keys, - change=change) + return self.key_for_path(req_path, name=name, account_id=account_id, network=network, + cosigner_id=cosigner_id, address_index=address_index) - def new_key_change(self, name='', account_id=None, witness_type=None, network=None): + def new_key_change(self, name='', account_id=None, network=None): """ Create new key to receive change for a transaction. Calls :func:`new_key` method with change=1. @@ -1836,7 +1779,7 @@ def new_key_change(self, name='', account_id=None, witness_type=None, network=No :return WalletKey: """ - return self.new_key(name=name, account_id=account_id, witness_type=witness_type, network=network, change=1) + return self.new_key(name=name, account_id=account_id, network=network, change=1) def scan_key(self, key): """ @@ -1905,7 +1848,7 @@ def scan(self, scan_gap_limit=5, account_id=None, change=None, rescan_used=False self.transactions_update_confirmations() # Check unconfirmed transactions - db_txs = self.session.query(DbTransaction). \ + db_txs = self._session.query(DbTransaction). \ filter(DbTransaction.wallet_id == self.wallet_id, DbTransaction.network_name == network, DbTransaction.confirmations == 0).all() for db_tx in db_txs: @@ -1923,11 +1866,7 @@ def scan(self, scan_gap_limit=5, account_id=None, change=None, rescan_used=False keys_to_scan = [self.key(k.id) for k in self.keys_addresses()[counter:counter+scan_gap_limit]] counter += scan_gap_limit else: - keys_to_scan = [] - for witness_type in self.witness_types(network=network): - keys_to_scan += self.get_keys(account_id, witness_type, network, - number_of_keys=scan_gap_limit, change=chg) - + keys_to_scan = self.get_keys(account_id, network, number_of_keys=scan_gap_limit, change=chg) n_highest_updated = 0 for key in keys_to_scan: if key.key_id in keys_ignore: @@ -1943,8 +1882,7 @@ def scan(self, scan_gap_limit=5, account_id=None, change=None, rescan_used=False if not n_highest_updated: break - def _get_key(self, account_id=None, witness_type=None, network=None, cosigner_id=None, number_of_keys=1, change=0, - as_list=False): + def _get_key(self, account_id=None, network=None, cosigner_id=None, number_of_keys=1, change=0, as_list=False): network, account_id, _ = self._get_account_defaults(network, account_id) if cosigner_id is None: cosigner_id = self.cosigner_id @@ -1952,37 +1890,33 @@ def _get_key(self, account_id=None, witness_type=None, network=None, cosigner_id raise WalletError("Cosigner ID (%d) can not be greater then number of cosigners for this wallet (%d)" % (cosigner_id, len(self.cosigner))) - witness_type = witness_type if witness_type else self.witness_type - last_used_qr = self.session.query(DbKey.id).\ + last_used_qr = self._session.query(DbKey.id).\ filter_by(wallet_id=self.wallet_id, account_id=account_id, network_name=network, cosigner_id=cosigner_id, - used=True, change=change, depth=self.key_depth, witness_type=witness_type).\ + used=True, change=change, depth=self.key_depth).\ order_by(DbKey.id.desc()).first() last_used_key_id = 0 if last_used_qr: last_used_key_id = last_used_qr.id - dbkey = (self.session.query(DbKey.id). + dbkey = self._session.query(DbKey).\ filter_by(wallet_id=self.wallet_id, account_id=account_id, network_name=network, cosigner_id=cosigner_id, - used=False, change=change, depth=self.key_depth, witness_type=witness_type). - filter(DbKey.id > last_used_key_id). - order_by(DbKey.id.asc()).all()) + used=False, change=change, depth=self.key_depth).filter(DbKey.id > last_used_key_id).\ + order_by(DbKey.id.desc()).all() + key_list = [] if self.scheme == 'single' and len(dbkey): number_of_keys = len(dbkey) if number_of_keys > len(dbkey) else number_of_keys - key_list = [self.key(key_id[0]) for key_id in dbkey] - - if len(key_list) > number_of_keys: - key_list = key_list[:number_of_keys] - else: - new_keys = self.new_keys(account_id=account_id, change=change, cosigner_id=cosigner_id, - witness_type=witness_type, network=network, - number_of_keys=number_of_keys - len(key_list)) - key_list += new_keys - + for i in range(number_of_keys): + if dbkey: + dk = dbkey.pop() + nk = self.key(dk.id) + else: + nk = self.new_key(account_id=account_id, change=change, cosigner_id=cosigner_id, network=network) + key_list.append(nk) if as_list: return key_list else: return key_list[0] - def get_key(self, account_id=None, witness_type=None, network=None, cosigner_id=None, change=0): + def get_key(self, account_id=None, network=None, cosigner_id=None, change=0): """ Get a unused key / address or create a new one with :func:`new_key` if there are no unused keys. Returns a key from this wallet which has no transactions linked to it. @@ -1997,8 +1931,6 @@ def get_key(self, account_id=None, witness_type=None, network=None, cosigner_id= :param account_id: Account ID. Default is last used or created account ID. :type account_id: int - :param witness_type: Use to create key with specific witness_type - :type witness_type: str :param network: Network name. Leave empty for default network :type network: str :param cosigner_id: Cosigner ID for key path @@ -2008,9 +1940,9 @@ def get_key(self, account_id=None, witness_type=None, network=None, cosigner_id= :return WalletKey: """ - return self._get_key(account_id, witness_type, network, cosigner_id, change=change, as_list=False) + return self._get_key(account_id, network, cosigner_id, change=change, as_list=False) - def get_keys(self, account_id=None, witness_type=None, network=None, cosigner_id=None, number_of_keys=1, change=0): + def get_keys(self, account_id=None, network=None, cosigner_id=None, number_of_keys=1, change=0): """ Get a list of unused keys / addresses or create a new ones with :func:`new_key` if there are no unused keys. Returns a list of keys from this wallet which has no transactions linked to it. @@ -2019,8 +1951,6 @@ def get_keys(self, account_id=None, witness_type=None, network=None, cosigner_id :param account_id: Account ID. Default is last used or created account ID. :type account_id: int - :param witness_type: Use to create key with specific witness_type - :type witness_type: str :param network: Network name. Leave empty for default network :type network: str :param cosigner_id: Cosigner ID for key path @@ -2034,34 +1964,30 @@ def get_keys(self, account_id=None, witness_type=None, network=None, cosigner_id """ if self.scheme == 'single': raise WalletError("Single wallet has only one (master)key. Use get_key() or main_key() method") - return self._get_key(account_id, witness_type, network, cosigner_id, number_of_keys, change, as_list=True) + return self._get_key(account_id, network, cosigner_id, number_of_keys, change, as_list=True) - def get_key_change(self, account_id=None, witness_type=None, network=None): + def get_key_change(self, account_id=None, network=None): """ Get a unused change key or create a new one if there are no unused keys. Wrapper for the :func:`get_key` method :param account_id: Account ID. Default is last used or created account ID. :type account_id: int - :param witness_type: Use to create key with specific witness_type - :type witness_type: str :param network: Network name. Leave empty for default network :type network: str :return WalletKey: """ - return self._get_key(account_id, witness_type, network, change=1, as_list=False) + return self._get_key(account_id=account_id, network=network, change=1, as_list=False) - def get_keys_change(self, account_id=None, witness_type=None, network=None, number_of_keys=1): + def get_keys_change(self, account_id=None, network=None, number_of_keys=1): """ Get a unused change key or create a new one if there are no unused keys. Wrapper for the :func:`get_key` method :param account_id: Account ID. Default is last used or created account ID. :type account_id: int - :param witness_type: Use to create key with specific witness_type - :type witness_type: str :param network: Network name. Leave empty for default network :type network: str :param number_of_keys: Number of keys to return. Default is 1 @@ -2070,9 +1996,10 @@ def get_keys_change(self, account_id=None, witness_type=None, network=None, numb :return list of WalletKey: """ - return self._get_key(account_id, witness_type, network, change=1, number_of_keys=number_of_keys, as_list=True) + return self._get_key(account_id=account_id, network=network, change=1, number_of_keys=number_of_keys, + as_list=True) - def new_account(self, name='', account_id=None, witness_type=None, network=None): + def new_account(self, name='', account_id=None, network=None): """ Create a new account with a child key for payments and 1 for change. @@ -2082,8 +2009,6 @@ def new_account(self, name='', account_id=None, witness_type=None, network=None) :type name: str :param account_id: Account ID. Default is last accounts ID + 1 :type account_id: int - :param witness_type: Use to create key with specific witness_type - :type witness_type: str :param network: Network name. Leave empty for default network :type network: str @@ -2096,7 +2021,7 @@ def new_account(self, name='', account_id=None, witness_type=None, network=None) raise WalletError("A master private key of depth 0 is needed to create new accounts (depth: %d)" % self.main_key.depth) if "account'" not in self.key_path: - raise WalletError("Accounts are not supported for this wallet. Account level not found in key path %s" % + raise WalletError("Accounts are not supported for this wallet. Account not found in key path %s" % self.key_path) if network is None: network = self.network.name @@ -2109,25 +2034,21 @@ def new_account(self, name='', account_id=None, witness_type=None, network=None) raise WalletError("Can not create new account for network %s with same BIP44 cointype: %s" % (network, duplicate_cointypes)) - witness_type = witness_type if witness_type else self.witness_type # Determine account_id and name if account_id is None: account_id = 0 - qr = self.session.query(DbKey). \ - filter_by(wallet_id=self.wallet_id, witness_type=witness_type, network_name=network). \ + qr = self._session.query(DbKey). \ + filter_by(wallet_id=self.wallet_id, purpose=self.purpose, network_name=network). \ order_by(DbKey.account_id.desc()).first() if qr: account_id = qr.account_id + 1 - if self.keys(account_id=account_id, depth=self.depth_public_master, witness_type=witness_type, - network=network): + if self.keys(account_id=account_id, depth=self.depth_public_master, network=network): raise WalletError("Account with ID %d already exists for this wallet" % account_id) acckey = self.key_for_path([], level_offset=self.depth_public_master-self.key_depth, account_id=account_id, - name=name, witness_type=witness_type, network=network) - self.key_for_path([], witness_type=witness_type, network=network, account_id=account_id, change=0, - address_index=0) - self.key_for_path([], witness_type=witness_type, network=network, account_id=account_id, change=1, - address_index=0) + name=name, network=network) + self.key_for_path([0, 0], network=network, account_id=account_id) + self.key_for_path([1, 0], network=network, account_id=account_id) return acckey def path_expand(self, path, level_offset=None, account_id=None, cosigner_id=0, address_index=None, change=0, @@ -2166,39 +2087,7 @@ def path_expand(self, path, level_offset=None, account_id=None, cosigner_id=0, a witness_type=self.witness_type, network=network) def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosigner_id=None, - address_index=0, change=0, witness_type=None, network=None, recreate=False): - """ - Wrapper for the keys_for_path method. Returns a single wallet key. - - :param path: Part of key path, i.e. [0, 0] for [change=0, address_index=0] - :type path: list, str - :param level_offset: Just create part of path, when creating keys. For example -2 means create path with the last 2 items (change, address_index) or 1 will return the master key 'm' - :type level_offset: int - :param name: Specify key name for latest/highest key in structure - :type name: str - :param account_id: Account ID - :type account_id: int - :param cosigner_id: ID of cosigner - :type cosigner_id: int - :param address_index: Index of key, normally provided to 'path' argument - :type address_index: int - :param change: Change key = 1 or normal = 0, normally provided to 'path' argument - :type change: int - :param witness_type: Use to create key with different witness_type - :type witness_type: str - :param network: Network name. Leave empty for default network - :type network: str - :param recreate: Recreate key, even if already found in wallet. Can be used to update public key with private key info - :type recreate: bool - - :return WalletKey: - """ - return self.keys_for_path(path, level_offset, name, account_id, cosigner_id, address_index, change, - witness_type, network, recreate, 1)[0] - - def keys_for_path(self, path, level_offset=None, name=None, account_id=None, cosigner_id=None, - address_index=0, change=0, witness_type=None, network=None, recreate=False, - number_of_keys=1): + address_index=0, change=0, network=None, recreate=False): """ Return key for specified path. Derive all wallet keys in path if they not already exists @@ -2234,67 +2123,45 @@ def keys_for_path(self, path, level_offset=None, name=None, account_id=None, cos :type address_index: int :param change: Change key = 1 or normal = 0, normally provided to 'path' argument :type change: int - :param witness_type: Use to create key with different witness_type - :type witness_type: str :param network: Network name. Leave empty for default network :type network: str :param recreate: Recreate key, even if already found in wallet. Can be used to update public key with private key info :type recreate: bool - :param number_of_keys: Number of keys to create, use to create keys in bulk fast - :type number_of_keys: int - :return list of WalletKey: + :return WalletKey: """ - if number_of_keys == 0: - return [] network, account_id, _ = self._get_account_defaults(network, account_id) cosigner_id = cosigner_id if cosigner_id is not None else self.cosigner_id level_offset_key = level_offset if level_offset and self.main_key and level_offset > 0: level_offset_key = level_offset - self.main_key.depth - witness_type = witness_type if witness_type else self.witness_type - if ((not self.main_key or not self.main_key.is_private or self.main_key.depth != 0) and - self.witness_type != witness_type) and not self.multisig: - raise WalletError("This wallet has no private key, cannot use multiple witness types") + key_path = self.key_path - purpose = self.purpose - encoding = self.encoding - if witness_type != self.witness_type: - _, purpose, encoding = get_key_structure_data(witness_type, self.multisig) if self.multisig and cosigner_id is not None and len(self.cosigner) > cosigner_id: key_path = self.cosigner[cosigner_id].key_path fullpath = path_expand(path, key_path, level_offset_key, account_id=account_id, cosigner_id=cosigner_id, - purpose=purpose, address_index=address_index, change=change, - witness_type=witness_type, network=network) + purpose=self.purpose, address_index=address_index, change=change, + witness_type=self.witness_type, network=network) if self.multisig and self.cosigner: public_keys = [] for wlt in self.cosigner: if wlt.scheme == 'single': - wk = [wlt.main_key] + wk = wlt.main_key else: - wk = wlt.keys_for_path(path, level_offset=level_offset, account_id=account_id, name=name, - cosigner_id=cosigner_id, network=network, recreate=recreate, - witness_type=witness_type, number_of_keys=number_of_keys, change=change, - address_index=address_index) + wk = wlt.key_for_path(path, level_offset=level_offset, account_id=account_id, name=name, + cosigner_id=cosigner_id, network=network, recreate=recreate) public_keys.append(wk) - keys_to_add = [public_keys] - if type(public_keys[0]) is list: - keys_to_add = list(zip(*public_keys)) - new_ms_keys = [] - for ms_key_cosigners in keys_to_add: - new_ms_keys.append(self._new_key_multisig(list(ms_key_cosigners), name, account_id, change, cosigner_id, - network, address_index, witness_type)) - return new_ms_keys if new_ms_keys else None - - # Check for closest ancestor in wallet + return self._new_key_multisig(public_keys, name, account_id, change, cosigner_id, network, address_index) + + # Check for closest ancestor in wallet\ wpath = fullpath if self.main_key.depth and fullpath and fullpath[0] != 'M': wpath = ["M"] + fullpath[self.main_key.depth + 1:] dbkey = None while wpath and not dbkey: - qr = self.session.query(DbKey).filter_by(path=normalize_path('/'.join(wpath)), wallet_id=self.wallet_id) + qr = self._session.query(DbKey).filter_by(path=normalize_path('/'.join(wpath)), wallet_id=self.wallet_id) if recreate: qr = qr.filter_by(is_private=True) dbkey = qr.first() @@ -2305,80 +2172,40 @@ def keys_for_path(self, path, level_offset=None, name=None, account_id=None, cos else: topkey = self.key(dbkey.id) - if topkey.network != network and topkey.path.split('/') == fullpath: - raise WalletError("Cannot create new keys for network %s, no private masterkey found" % network) - # Key already found in db, return key - if dbkey and dbkey.path == normalize_path('/'.join(fullpath)) and not recreate and number_of_keys == 1: - return [topkey] + if dbkey and dbkey.path == normalize_path('/'.join(fullpath)) and not recreate: + return topkey else: - if dbkey and dbkey.path == normalize_path('/'.join(fullpath)) and not recreate and number_of_keys > 1: - new_keys = [topkey] - else: - # Create 1 or more keys add them to wallet - new_keys = [] - - nkey = None + # Create 1 or more keys add them to wallet + nk = None parent_id = topkey.key_id ck = topkey.key() - ck.witness_type = witness_type - ck.encoding = encoding newpath = topkey.path n_items = len(str(dbkey.path).split('/')) for lvl in fullpath[n_items:]: ck = ck.subkey_for_path(lvl, network=network) newpath += '/' + lvl if not account_id: - account_id = 0 if ("account'" not in self.key_path or - self.key_path.index("account'") >= len(fullpath)) \ + account_id = 0 if "account'" not in self.key_path or self.key_path.index("account'") >= len( + fullpath) \ else int(fullpath[self.key_path.index("account'")][:-1]) - change_pos = [self.key_path.index(chg) for chg in ["change", "change'"] if chg in self.key_path] - change = None if not change_pos or change_pos[0] >= len(fullpath) else ( - int(fullpath[change_pos[0]].strip("'"))) + change = None if "change" not in self.key_path or self.key_path.index("change") >= len(fullpath) \ + else int(fullpath[self.key_path.index("change")]) if name and len(fullpath) == len(newpath.split('/')): key_name = name else: key_name = "%s %s" % (self.key_path[len(newpath.split('/'))-1], lvl) key_name = key_name.replace("'", "").replace("_", " ") - nkey = WalletKey.from_key(key=ck, name=key_name, wallet_id=self.wallet_id, account_id=account_id, - change=change, purpose=purpose, path=newpath, parent_id=parent_id, - encoding=encoding, witness_type=witness_type, - cosigner_id=cosigner_id, network=network, session=self.session) - self._key_objects.update({nkey.key_id: nkey}) - parent_id = nkey.key_id - if nkey: - new_keys.append(nkey) - if len(new_keys) < number_of_keys: - parent_id = new_keys[0].parent_id - if parent_id not in self._key_objects: - self.key(parent_id) - topkey = self._key_objects[new_keys[0].parent_id] - parent_key = topkey.key() - new_key_id = self.session.query(DbKey.id).order_by(DbKey.id.desc()).first()[0] + 1 - hardened_child = False - if fullpath[-1].endswith("'"): - hardened_child = True - keys_to_add = [str(k_id) for k_id in range(int(fullpath[-1].strip("'")) + len(new_keys), - int(fullpath[-1].strip("'")) + number_of_keys)] - - for key_idx in keys_to_add: - new_key_id += 1 - if hardened_child: - key_idx = "%s'" % key_idx - ck = parent_key.subkey_for_path(key_idx, network=network) - key_name = 'address index %s' % key_idx.strip("'") - newpath = '/'.join(newpath.split('/')[:-1] + [key_idx]) - new_keys.append(WalletKey.from_key( - key=ck, name=key_name, wallet_id=self.wallet_id, account_id=account_id, - change=change, purpose=purpose, path=newpath, parent_id=parent_id, - encoding=encoding, witness_type=witness_type, new_key_id=new_key_id, - cosigner_id=cosigner_id, network=network, session=self.session)) - self.session.commit() - - return new_keys + nk = WalletKey.from_key(key=ck, name=key_name, wallet_id=self.wallet_id, account_id=account_id, + change=change, purpose=self.purpose, path=newpath, parent_id=parent_id, + encoding=self.encoding, witness_type=self.witness_type, + cosigner_id=cosigner_id, network=network, session=self._session) + self._key_objects.update({nk.key_id: nk}) + parent_id = nk.key_id + return nk def keys(self, account_id=None, name=None, key_id=None, change=None, depth=None, used=None, is_private=None, - has_balance=None, is_active=None, witness_type=None, network=None, include_private=False, as_dict=False): + has_balance=None, is_active=None, network=None, include_private=False, as_dict=False): """ Search for keys in database. Include 0 or more of account_id, name, key_id, change and depth. @@ -2407,8 +2234,6 @@ def keys(self, account_id=None, name=None, key_id=None, change=None, depth=None, :type has_balance: bool :param is_active: Hide inactive keys. Only include active keys with either a balance or which are unused, default is None (show all) :type is_active: bool - :param witness_type: Filter by witness_type - :type witness_type: str :param network: Network name filter :type network: str :param include_private: Include private key information in dictionary @@ -2419,11 +2244,9 @@ def keys(self, account_id=None, name=None, key_id=None, change=None, depth=None, :return list of DbKey: List of Keys """ - qr = self.session.query(DbKey).filter_by(wallet_id=self.wallet_id).order_by(DbKey.id) + qr = self._session.query(DbKey).filter_by(wallet_id=self.wallet_id).order_by(DbKey.id) if network is not None: qr = qr.filter(DbKey.network_name == network) - if witness_type is not None: - qr = qr.filter(DbKey.witness_type == witness_type) if account_id is not None: qr = qr.filter(DbKey.account_id == account_id) if self.scheme == 'bip32' and depth is None: @@ -2638,7 +2461,9 @@ def key(self, term): """ dbkey = None - qr = self.session.query(DbKey).filter_by(wallet_id=self.wallet_id) + qr = self._session.query(DbKey).filter_by(wallet_id=self.wallet_id) + if self.purpose: + qr = qr.filter_by(purpose=self.purpose) if isinstance(term, numbers.Number): dbkey = qr.filter_by(id=term).scalar() if not dbkey: @@ -2651,7 +2476,7 @@ def key(self, term): if dbkey.id in self._key_objects.keys(): return self._key_objects[dbkey.id] else: - hdwltkey = WalletKey(key_id=dbkey.id, session=self.session) + hdwltkey = WalletKey(key_id=dbkey.id, session=self._session) self._key_objects.update({dbkey.id: hdwltkey}) return hdwltkey else: @@ -2674,7 +2499,7 @@ def account(self, account_id): if "account'" not in self.key_path: raise WalletError("Accounts are not supported for this wallet. Account not found in key path %s" % self.key_path) - qr = self.session.query(DbKey).\ + qr = self._session.query(DbKey).\ filter_by(wallet_id=self.wallet_id, purpose=self.purpose, network_name=self.network.name, account_id=account_id, depth=3).scalar() if not qr: @@ -2703,28 +2528,6 @@ def accounts(self, network=None): accounts = [self.default_account_id] return list(dict.fromkeys(accounts)) - def witness_types(self, account_id=None, network=None): - """ - Get witness types in use by this wallet. For example 'legacy', 'segwit', 'p2sh-segwit' - - :param account_id: Account ID. Leave empty for default account - :type account_id: int - :param network: Network name filter. Default filter is DEFAULT_NETWORK - :type network: str - - :return list of str: - """ - - # network, account_id, _ = self._get_account_defaults(network, account_id) - qr = self.session.query(DbKey.witness_type).filter_by(wallet_id=self.wallet_id) - if network is not None: - qr = qr.filter(DbKey.network_name == network) - if account_id is not None: - qr = qr.filter(DbKey.account_id == account_id) - qr = qr.group_by(DbKey.witness_type).all() - return [x[0] for x in qr] - - def networks(self, as_dict=False): """ Get list of networks used by this wallet @@ -2737,7 +2540,7 @@ def networks(self, as_dict=False): nw_list = [self.network] if self.multisig and self.cosigner: - keys_qr = self.session.query(DbKey.network_name).\ + keys_qr = self._session.query(DbKey.network_name).\ filter_by(wallet_id=self.wallet_id, depth=self.key_depth).\ group_by(DbKey.network_name).all() nw_list += [Network(nw[0]) for nw in keys_qr] @@ -2850,7 +2653,7 @@ def _balance_update(self, account_id=None, network=None, key_id=None, min_confir :return: Updated balance """ - qr = self.session.query(DbTransactionOutput, func.sum(DbTransactionOutput.value), DbTransaction.network_name, + qr = self._session.query(DbTransactionOutput, func.sum(DbTransactionOutput.value), DbTransaction.network_name, DbTransaction.account_id).\ join(DbTransaction). \ filter(DbTransactionOutput.spent.is_(False), @@ -2922,7 +2725,7 @@ def _balance_update(self, account_id=None, network=None, key_id=None, min_confir for kb in key_balance_list: if kb['id'] in self._key_objects: self._key_objects[kb['id']]._balance = kb['balance'] - self.session.bulk_update_mappings(DbKey, key_balance_list) + self._session.bulk_update_mappings(DbKey, key_balance_list) self._commit() _logger.info("Got balance for %d key(s)" % len(key_balance_list)) return self._balances @@ -2974,7 +2777,7 @@ def utxos_update(self, account_id=None, used=None, networks=None, key_id=None, d single_key = None if key_id: - single_key = self.session.query(DbKey).filter_by(id=key_id).scalar() + single_key = self._session.query(DbKey).filter_by(id=key_id).scalar() networks = [single_key.network_name] account_id = single_key.account_id rescan_all = False @@ -2989,14 +2792,14 @@ def utxos_update(self, account_id=None, used=None, networks=None, key_id=None, d for network in networks: # Remove current UTXO's if rescan_all: - cur_utxos = self.session.query(DbTransactionOutput). \ + cur_utxos = self._session.query(DbTransactionOutput). \ join(DbTransaction). \ filter(DbTransactionOutput.spent.is_(False), DbTransaction.account_id == account_id, DbTransaction.wallet_id == self.wallet_id, DbTransaction.network_name == network).all() for u in cur_utxos: - self.session.query(DbTransactionOutput).filter_by( + self._session.query(DbTransactionOutput).filter_by( transaction_id=u.transaction_id, output_n=u.output_n).update({DbTransactionOutput.spent: True}) self._commit() @@ -3012,7 +2815,7 @@ def utxos_update(self, account_id=None, used=None, networks=None, key_id=None, d addresslist = self.addresslist(account_id=account_id, used=used, network=network, key_id=key_id, change=change, depth=depth) random.shuffle(addresslist) - srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) + srv = Service(network=network, wallet_name=self.name, providers=self.providers, cache_uri=self.db_cache_uri) utxos = [] for address in addresslist: if rescan_all: @@ -3034,7 +2837,7 @@ def utxos_update(self, account_id=None, used=None, networks=None, key_id=None, d for utxo in utxos: key = single_key if not single_key: - key = self.session.query(DbKey).\ + key = self._session.query(DbKey).\ filter_by(wallet_id=self.wallet_id, address=utxo['address']).scalar() if not key: raise WalletError("Key with address %s not found in this wallet" % utxo['address']) @@ -3044,14 +2847,14 @@ def utxos_update(self, account_id=None, used=None, networks=None, key_id=None, d status = 'confirmed' # Update confirmations in db if utxo was already imported - transaction_in_db = self.session.query(DbTransaction).\ + transaction_in_db = self._session.query(DbTransaction).\ filter_by(wallet_id=self.wallet_id, txid=bytes.fromhex(utxo['txid']), network_name=network) - utxo_in_db = self.session.query(DbTransactionOutput).join(DbTransaction).\ + utxo_in_db = self._session.query(DbTransactionOutput).join(DbTransaction).\ filter(DbTransaction.wallet_id == self.wallet_id, DbTransaction.txid == bytes.fromhex(utxo['txid']), DbTransactionOutput.output_n == utxo['output_n']) - spent_in_db = self.session.query(DbTransactionInput).join(DbTransaction).\ + spent_in_db = self._session.query(DbTransactionInput).join(DbTransaction).\ filter(DbTransaction.wallet_id == self.wallet_id, DbTransactionInput.prev_txid == bytes.fromhex(utxo['txid']), DbTransactionInput.output_n == utxo['output_n']) @@ -3075,7 +2878,7 @@ def utxos_update(self, account_id=None, used=None, networks=None, key_id=None, d wallet_id=self.wallet_id, txid=bytes.fromhex(utxo['txid']), status=status, is_complete=False, block_height=block_height, account_id=account_id, confirmations=utxo['confirmations'], network_name=network) - self.session.add(new_tx) + self._session.add(new_tx) # TODO: Get unique id before inserting to increase performance for large utxo-sets self._commit() tid = new_tx.id @@ -3089,7 +2892,7 @@ def utxos_update(self, account_id=None, used=None, networks=None, key_id=None, d script=bytes.fromhex(utxo['script']), script_type=script_type, spent=bool(spent_in_db.count())) - self.session.add(new_utxo) + self._session.add(new_utxo) count_utxos += 1 self._commit() @@ -3126,7 +2929,7 @@ def utxos(self, account_id=None, network=None, min_confirms=0, key_id=None): first_key_id = key_id[0] network, account_id, acckey = self._get_account_defaults(network, account_id, first_key_id) - qr = self.session.query(DbTransactionOutput, DbKey.address, DbTransaction.confirmations, DbTransaction.txid, + qr = self._session.query(DbTransactionOutput, DbKey.address, DbTransaction.confirmations, DbTransaction.txid, DbKey.network_name).\ join(DbTransaction).join(DbKey). \ filter(DbTransactionOutput.spent.is_(False), @@ -3198,7 +3001,7 @@ def utxo_last(self, address): :return str: """ - to = self.session.query( + to = self._session.query( DbTransaction.txid, DbTransaction.confirmations). \ join(DbTransactionOutput).join(DbKey). \ filter(DbKey.address == address, DbTransaction.wallet_id == self.wallet_id, @@ -3215,9 +3018,11 @@ def transactions_update_confirmations(self): network = self.network.name srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) blockcount = srv.blockcount() - self.session.query(DbTransaction).\ + db_txs = self._session.query(DbTransaction). \ filter(DbTransaction.wallet_id == self.wallet_id, - DbTransaction.network_name == network, DbTransaction.block_height > 0).\ + DbTransaction.network_name == network, DbTransaction.block_height > 0).all() + for db_tx in db_txs: + self._session.query(DbTransaction).filter_by(id=db_tx.id). \ update({DbTransaction.status: 'confirmed', DbTransaction.confirmations: (blockcount - DbTransaction.block_height) + 1}) self._commit() @@ -3251,7 +3056,7 @@ def transactions_update_by_txids(self, txids): utxo_set.update(utxos) for utxo in list(utxo_set): - tos = self.session.query(DbTransactionOutput).join(DbTransaction). \ + tos = self._session.query(DbTransactionOutput).join(DbTransaction). \ filter(DbTransaction.txid == bytes.fromhex(utxo[0]), DbTransactionOutput.output_n == utxo[1], DbTransactionOutput.spent.is_(False)).all() for u in tos: @@ -3289,9 +3094,10 @@ def transactions_update(self, account_id=None, used=None, network=None, key_id=N depth = self.key_depth # Update number of confirmations and status for already known transactions - self.transactions_update_confirmations() + if not key_id: + self.transactions_update_confirmations() - srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) + srv = Service(network=network, wallet_name=self.name, providers=self.providers, cache_uri=self.db_cache_uri) # Get transactions for wallet's addresses txs = [] @@ -3304,7 +3110,7 @@ def transactions_update(self, account_id=None, used=None, network=None, key_id=N if txs and txs[-1].date and txs[-1].date < last_updated: last_updated = txs[-1].date if txs and txs[-1].confirmations: - dbkey = self.session.query(DbKey).filter(DbKey.address == address, DbKey.wallet_id == self.wallet_id) + dbkey = self._session.query(DbKey).filter(DbKey.address == address, DbKey.wallet_id == self.wallet_id) if not dbkey.update({DbKey.latest_txid: bytes.fromhex(txs[-1].txid)}): raise WalletError("Failed to update latest transaction id for key with address %s" % address) self._commit() @@ -3319,7 +3125,7 @@ def transactions_update(self, account_id=None, used=None, network=None, key_id=N utxos = [(ti.prev_txid.hex(), ti.output_n_int) for ti in wt.inputs] utxo_set.update(utxos) for utxo in list(utxo_set): - tos = self.session.query(DbTransactionOutput).join(DbTransaction).\ + tos = self._session.query(DbTransactionOutput).join(DbTransaction).\ filter(DbTransaction.txid == bytes.fromhex(utxo[0]), DbTransactionOutput.output_n == utxo[1], DbTransactionOutput.spent.is_(False), DbTransaction.wallet_id == self.wallet_id).all() for u in tos: @@ -3340,7 +3146,7 @@ def transaction_last(self, address): :return str: """ - txid = self.session.query(DbKey.latest_txid).\ + txid = self._session.query(DbKey.latest_txid).\ filter(DbKey.address == address, DbKey.wallet_id == self.wallet_id).scalar() return '' if not txid else txid.hex() @@ -3371,7 +3177,7 @@ def transactions(self, account_id=None, network=None, include_new=False, key_id= network, account_id, acckey = self._get_account_defaults(network, account_id, key_id) # Transaction inputs - qr = self.session.query(DbTransactionInput, DbTransactionInput.address, DbTransaction.confirmations, + qr = self._session.query(DbTransactionInput, DbTransactionInput.address, DbTransaction.confirmations, DbTransaction.txid, DbTransaction.network_name, DbTransaction.status). \ join(DbTransaction).join(DbKey). \ filter(DbTransaction.account_id == account_id, @@ -3384,7 +3190,7 @@ def transactions(self, account_id=None, network=None, include_new=False, key_id= qr = qr.filter(or_(DbTransaction.status == 'confirmed', DbTransaction.status == 'unconfirmed')) txs = qr.all() # Transaction outputs - qr = self.session.query(DbTransactionOutput, DbTransactionOutput.address, DbTransaction.confirmations, + qr = self._session.query(DbTransactionOutput, DbTransactionOutput.address, DbTransaction.confirmations, DbTransaction.txid, DbTransaction.network_name, DbTransaction.status). \ join(DbTransaction).join(DbKey). \ filter(DbTransaction.account_id == account_id, @@ -3444,7 +3250,7 @@ def transactions_full(self, network=None, include_new=False, limit=0, offset=0): :return list of WalletTransaction: """ network, _, _ = self._get_account_defaults(network) - qr = self.session.query(DbTransaction.txid, DbTransaction.network_name, DbTransaction.status). \ + qr = self._session.query(DbTransaction.txid, DbTransaction.network_name, DbTransaction.status). \ filter(DbTransaction.wallet_id == self.wallet_id, DbTransaction.network_name == network) if not include_new: @@ -3524,7 +3330,7 @@ def transaction_spent(self, txid, output_n): txid = to_bytes(txid) if isinstance(output_n, bytes): output_n = int.from_bytes(output_n, 'big') - qr = self.session.query(DbTransactionInput, DbTransaction.confirmations, + qr = self._session.query(DbTransactionInput, DbTransaction.confirmations, DbTransaction.txid, DbTransaction.status). \ join(DbTransaction). \ filter(DbTransaction.wallet_id == self.wallet_id, @@ -3533,7 +3339,7 @@ def transaction_spent(self, txid, output_n): return qr.transaction.txid.hex() def _objects_by_key_id(self, key_id): - key = self.session.query(DbKey).filter_by(id=key_id).scalar() + key = self._session.query(DbKey).filter_by(id=key_id).scalar() if not key: raise WalletError("Key '%s' not found in this wallet" % key_id) if key.key_type == 'multisig': @@ -3585,7 +3391,7 @@ def select_inputs(self, amount, variance=None, input_key_id=None, account_id=Non if variance is None: variance = dust_amount - utxo_query = self.session.query(DbTransactionOutput).join(DbTransaction).join(DbKey). \ + utxo_query = self._session.query(DbTransactionOutput).join(DbTransaction).join(DbKey). \ filter(DbTransaction.wallet_id == self.wallet_id, DbTransaction.account_id == account_id, DbTransaction.network_name == network, DbKey.public != b'', DbTransactionOutput.spent.is_(False), DbTransaction.confirmations >= min_confirms) @@ -3595,7 +3401,7 @@ def select_inputs(self, amount, variance=None, input_key_id=None, account_id=Non else: utxo_query = utxo_query.filter(DbKey.id.in_(input_key_id)) if skip_dust_amounts: - utxo_query = utxo_query.filter(DbTransactionOutput.value >= dust_amount) + utxo_query = utxo_query.filter(DbTransactionOutput.value > dust_amount) utxos = utxo_query.order_by(DbTransaction.confirmations.desc()).all() if not utxos: raise WalletError("Create transaction: No unspent transaction outputs found or no key available for UTXO's") @@ -3650,7 +3456,7 @@ def select_inputs(self, amount, variance=None, input_key_id=None, account_id=Non def transaction_create(self, output_arr, input_arr=None, input_key_id=None, account_id=None, network=None, fee=None, min_confirms=1, max_utxos=None, locktime=0, number_of_change_outputs=1, - random_output_order=True, replace_by_fee=False): + random_output_order=True): """ Create new transaction with specified outputs. @@ -3687,8 +3493,6 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco :type number_of_change_outputs: int :param random_output_order: Shuffle order of transaction outputs to increase privacy. Default is True :type random_output_order: bool - :param replace_by_fee: Signal replace by fee and allow to send a new transaction with higher fees. Sets sequence value to SEQUENCE_REPLACE_BY_FEE - :type replace_by_fee: bool :return WalletTransaction: object """ @@ -3709,8 +3513,7 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco # Create transaction and add outputs amount_total_output = 0 - transaction = WalletTransaction(hdwallet=self, account_id=account_id, network=network, locktime=locktime, - replace_by_fee=replace_by_fee) + transaction = WalletTransaction(hdwallet=self, account_id=account_id, network=network, locktime=locktime) transaction.outgoing_tx = True for o in output_arr: if isinstance(o, Output): @@ -3724,7 +3527,7 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco addr = addr.key() transaction.add_output(value, addr) - srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) + srv = Service(network=network, wallet_name=self.name, providers=self.providers, cache_uri=self.db_cache_uri) if not locktime and self.anti_fee_sniping: srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) @@ -3751,10 +3554,8 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco # Add inputs sequence = 0xffffffff - if replace_by_fee: - sequence = SEQUENCE_REPLACE_BY_FEE - elif 0 < transaction.locktime < 0xffffffff: - sequence = SEQUENCE_ENABLE_LOCKTIME + if 0 < transaction.locktime < 0xffffffff: + sequence = 0xfffffffe amount_total_input = 0 if input_arr is None: selected_utxos = self.select_inputs(amount_total_output + fee_estimate, transaction.network.dust_amount, @@ -3765,14 +3566,12 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco amount_total_input += utxo.value inp_keys, key = self._objects_by_key_id(utxo.key_id) multisig = False if isinstance(inp_keys, list) and len(inp_keys) < 2 else True - witness_type = utxo.key.witness_type if utxo.key.witness_type else self.witness_type - unlock_script_type = get_unlocking_script_type(utxo.script_type, witness_type, - multisig=multisig) + unlock_script_type = get_unlocking_script_type(utxo.script_type, self.witness_type, multisig=multisig) transaction.add_input(utxo.transaction.txid, utxo.output_n, keys=inp_keys, script_type=unlock_script_type, sigs_required=self.multisig_n_required, sort=self.sort_keys, compressed=key.compressed, value=utxo.value, address=utxo.key.address, sequence=sequence, - key_path=utxo.key.path, witness_type=witness_type) + key_path=utxo.key.path, witness_type=self.witness_type) # FIXME: Missing locktime_cltv=locktime_cltv, locktime_csv=locktime_csv (?) else: for inp in input_arr: @@ -3793,7 +3592,6 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco sequence = inp.sequence locktime_cltv = inp.locktime_cltv locktime_csv = inp.locktime_csv - witness_type = inp.witness_type # elif isinstance(inp, DbTransactionOutput): # prev_txid = inp.transaction.txid # output_n = inp.output_n @@ -3812,12 +3610,11 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco signatures = None if len(inp) <= 4 else inp[4] unlocking_script = b'' if len(inp) <= 5 else inp[5] address = '' if len(inp) <= 6 else inp[6] - witness_type = self.witness_type # Get key_ids, value from Db if not specified if not (key_id and value and unlocking_script_type): if not isinstance(output_n, TYPE_INT): output_n = int.from_bytes(output_n, 'big') - inp_utxo = self.session.query(DbTransactionOutput).join(DbTransaction). \ + inp_utxo = self._session.query(DbTransactionOutput).join(DbTransaction). \ filter(DbTransaction.wallet_id == self.wallet_id, DbTransaction.txid == to_bytes(prev_txid), DbTransactionOutput.output_n == output_n).first() @@ -3826,11 +3623,11 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco value = inp_utxo.value address = inp_utxo.key.address unlocking_script_type = get_unlocking_script_type(inp_utxo.script_type, multisig=self.multisig) - witness_type = inp_utxo.key.witness_type + # witness_type = inp_utxo.witness_type else: _logger.info("UTXO %s not found in this wallet. Please update UTXO's if this is not an " "offline wallet" % to_hexstring(prev_txid)) - key_id = self.session.query(DbKey.id).\ + key_id = self._session.query(DbKey.id).\ filter(DbKey.wallet_id == self.wallet_id, DbKey.address == address).scalar() if not key_id: raise WalletError("UTXO %s and key with address %s not found in this wallet" % ( @@ -3847,7 +3644,7 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco unlocking_script=unlocking_script, address=address, unlocking_script_unsigned=unlocking_script_unsigned, sequence=sequence, locktime_cltv=locktime_cltv, locktime_csv=locktime_csv, - witness_type=witness_type, key_path=key.path) + witness_type=self.witness_type, key_path=key.path) # Calculate fees transaction.fee = fee fee_per_output = None @@ -3904,9 +3701,9 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco "or lower fees") if self.scheme == 'single': - change_keys = [self.get_key(account_id, self.witness_type, network, change=1)] + change_keys = [self.get_key(account_id=account_id, network=network, change=1)] else: - change_keys = self.get_keys(account_id, self.witness_type, network, change=1, + change_keys = self.get_keys(account_id=account_id, network=network, change=1, number_of_keys=number_of_change_outputs) if number_of_change_outputs > 1: @@ -4049,8 +3846,7 @@ def transaction_import_raw(self, rawtx, network=None): return rt def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, network=None, fee=None, - min_confirms=1, priv_keys=None, max_utxos=None, locktime=0, offline=True, number_of_change_outputs=1, - replace_by_fee=False): + min_confirms=1, priv_keys=None, max_utxos=None, locktime=0, offline=True, number_of_change_outputs=1): """ Create a new transaction with specified outputs and push it to the network. Inputs can be specified but if not provided they will be selected from wallets utxo's @@ -4089,8 +3885,6 @@ def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, n :type offline: bool :param number_of_change_outputs: Number of change outputs to create when there is a change value. Default is 1. Use 0 for random number of outputs: between 1 and 5 depending on send and change amount :type number_of_change_outputs: int - :param replace_by_fee: Signal replace by fee and allow to send a new transaction with higher fees. Sets sequence value to SEQUENCE_REPLACE_BY_FEE - :type replace_by_fee: bool :return WalletTransaction: """ @@ -4100,8 +3894,7 @@ def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, n (len(input_arr), max_utxos)) transaction = self.transaction_create(output_arr, input_arr, input_key_id, account_id, network, fee, - min_confirms, max_utxos, locktime, number_of_change_outputs, True, - replace_by_fee) + min_confirms, max_utxos, locktime, number_of_change_outputs) transaction.sign(priv_keys) # Calculate exact fees and update change output if necessary if fee is None and transaction.fee_per_kb and transaction.change: @@ -4113,7 +3906,7 @@ def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, n "Recreate transaction with correct fee" % (transaction.fee, fee_exact)) transaction = self.transaction_create(output_arr, input_arr, input_key_id, account_id, network, fee_exact, min_confirms, max_utxos, locktime, - number_of_change_outputs, True, replace_by_fee) + number_of_change_outputs) transaction.sign(priv_keys) transaction.rawtx = transaction.raw() @@ -4125,7 +3918,7 @@ def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, n return transaction def send_to(self, to_address, amount, input_key_id=None, account_id=None, network=None, fee=None, min_confirms=1, - priv_keys=None, locktime=0, offline=True, number_of_change_outputs=1, replace_by_fee=False): + priv_keys=None, locktime=0, offline=True, number_of_change_outputs=1): """ Create transaction and send it with default Service objects :func:`services.sendrawtransaction` method. @@ -4160,8 +3953,6 @@ def send_to(self, to_address, amount, input_key_id=None, account_id=None, networ :type offline: bool :param number_of_change_outputs: Number of change outputs to create when there is a change value. Default is 1. Use 0 for random number of outputs: between 1 and 5 depending on send and change amount :type number_of_change_outputs: int - :param replace_by_fee: Signal replace by fee and allow to send a new transaction with higher fees. Sets sequence value to SEQUENCE_REPLACE_BY_FEE - :type replace_by_fee: bool :return WalletTransaction: """ @@ -4169,10 +3960,10 @@ def send_to(self, to_address, amount, input_key_id=None, account_id=None, networ outputs = [(to_address, amount)] return self.send(outputs, input_key_id=input_key_id, account_id=account_id, network=network, fee=fee, min_confirms=min_confirms, priv_keys=priv_keys, locktime=locktime, offline=offline, - number_of_change_outputs=number_of_change_outputs, replace_by_fee=replace_by_fee) + number_of_change_outputs=number_of_change_outputs) def sweep(self, to_address, account_id=None, input_key_id=None, network=None, max_utxos=999, min_confirms=1, - fee_per_kb=None, fee=None, locktime=0, offline=True, replace_by_fee=False): + fee_per_kb=None, fee=None, locktime=0, offline=True): """ Sweep all unspent transaction outputs (UTXO's) and send them to one or more output addresses. @@ -4211,8 +4002,6 @@ def sweep(self, to_address, account_id=None, input_key_id=None, network=None, ma :type locktime: int :param offline: Just return the transaction object and do not send it when offline = True. Default is True :type offline: bool - :param replace_by_fee: Signal replace by fee and allow to send a new transaction with higher fees. Sets sequence value to SEQUENCE_REPLACE_BY_FEE - :type replace_by_fee: bool :return WalletTransaction: """ @@ -4264,7 +4053,7 @@ def sweep(self, to_address, account_id=None, input_key_id=None, network=None, ma "outputs, use amount value = 0 to indicate a change/rest output") return self.send(to_list, input_arr, network=network, fee=fee, min_confirms=min_confirms, locktime=locktime, - offline=offline, replace_by_fee=replace_by_fee) + offline=offline) def wif(self, is_private=False, account_id=0): """ @@ -4292,7 +4081,7 @@ def wif(self, is_private=False, account_id=0): wiflist.append(cs.wif(is_private=is_private)) return wiflist - def public_master(self, account_id=None, name=None, as_private=False, witness_type=None, network=None): + def public_master(self, account_id=None, name=None, as_private=False, network=None): """ Return public master key(s) for this wallet. Use to import in other wallets to sign transactions or create keys. @@ -4319,10 +4108,9 @@ def public_master(self, account_id=None, name=None, as_private=False, witness_ty key = self.main_key return key if as_private else key.public() elif not self.cosigner: - witness_type = witness_type if witness_type else self.witness_type depth = -self.key_depth + self.depth_public_master key = self.key_for_path([], depth, name=name, account_id=account_id, network=network, - cosigner_id=self.cosigner_id, witness_type=witness_type) + cosigner_id=self.cosigner_id) return key if as_private else key.public() else: pm_list = [] diff --git a/docker/README.rst b/docker/README.rst index 06294040..70813d75 100644 --- a/docker/README.rst +++ b/docker/README.rst @@ -3,5 +3,11 @@ Dockerfiles You can find some basic Dockerfiles here for various system images. -These are used for testing and are not optimized for size and configuration. If you run the container is will -start all unittests. +These are used for testing and are not optimized for size and configuration. If you run the container it will +run all unittests. + +.. code-block:: bash + + $ cd + $ docker build -t bitcoinlib . + $ docker run -it bitcoinlib diff --git a/docker/mint/Dockerfile b/docker/mint/Dockerfile new file mode 100644 index 00000000..a3e208cf --- /dev/null +++ b/docker/mint/Dockerfile @@ -0,0 +1,22 @@ +FROM linuxmintd/mint22-amd64 +MAINTAINER Cryp Toon + +WORKDIR /code + +RUN apt-get update && apt-get upgrade -y +RUN apt-get install -y \ + software-properties-common git \ + build-essential python3-dev libgmp3-dev python3-pip python3.12-venv + +ENV TZ=Europe/Brussels +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone +RUN apt-get install -y postgresql postgresql-contrib mariadb-server libpq-dev pkg-config default-libmysqlclient-dev +RUN apt-get clean + +RUN git clone https://github.com/1200wd/bitcoinlib.git + +WORKDIR /code/bitcoinlib +RUN python3 -m venv /opt/venv +RUN /opt/venv/bin/python3 -m pip install .[dev] + +CMD /opt/venv/bin/python3 -m unittest diff --git a/docs/_static/manuals.databases.rst b/docs/_static/manuals.databases.rst index 06df201b..381c5012 100644 --- a/docs/_static/manuals.databases.rst +++ b/docs/_static/manuals.databases.rst @@ -5,12 +5,12 @@ Bitcoinlib uses the SQLite database by default, because it easy to use and requi But you can also use other databases. At this moment Bitcoinlib is tested with MySQL and PostgreSQL. -The database URI can be passed to the Wallet or Service object, or you can set the database URI for wallets and / or cache in configuration file at ~/.bitcoinlib/config.ini -Using MariaDB / MySQL database ------------------------------- +Using MySQL database +-------------------- -We assume you have a MySQL server at localhost. Unlike with the SQLite database MySQL databases are not created automatically, so create one from the mysql command prompt: +We assume you have a MySQL server at localhost. Unlike with the SQLite database MySQL databases are not created +automatically, so create one from the mysql command prompt: .. code-block:: mysql @@ -32,7 +32,6 @@ In your application you can create a database link. The database tables are crea w = wallet_create_or_open('wallet_mysql', db_uri=db_uri) w.info() -At the moment it is not possible to use MySQL database for `caching `_, because the BLOB transaction ID's are used as primary key. For caching you need to use a PostgreSQL or SQLite database. Using PostgreSQL database ------------------------- @@ -55,23 +54,16 @@ And assume you unwisely have chosen the password 'secret' you can use the databa .. code-block:: python - db_uri = 'postgresql+psycopg://bitcoinlib:secret@localhost:5432/' + db_uri = 'postgresql://bitcoinlib:secret@localhost:5432/' w = wallet_create_or_open('wallet_mysql', db_uri=db_uri) w.info() -Please note 'postgresql+psycopg' has to be used as scheme, because SQLalchemy uses the latest version 3 of psycopg, if not provided it will use psycopg2. -PostgreSQL can also be used for `caching `_ of service requests. The URI can be passed to the Service object or provided in the configuration file (~/.bitcoiinlib/config.ini) +If you are using wallets with private keys it is advised to encrypt your database and / or private keys. .. code-block:: python srv = Service(cache_uri='postgresql+psycopg://postgres:postgres@localhost:5432/) res = srv.gettransactions('12spqcvLTFhL38oNJDDLfW1GpFGxLdaLCL') - -Encrypt database or private keys --------------------------------- - -If you are using wallets with private keys it is advised to use an encrypted database and / or to encrypt the private key fields. - -Please read `Encrypt Database or Private Keys `_ for more information. +Please read `Using Encrypted Databases `_ for more information. \ No newline at end of file diff --git a/docs/_static/manuals.faq.rst b/docs/_static/manuals.faq.rst new file mode 100644 index 00000000..b93d4fe8 --- /dev/null +++ b/docs/_static/manuals.faq.rst @@ -0,0 +1,71 @@ +Frequently Asked Questions +========================== + +Can I use Bitcoinlib on my system? +---------------------------------- + +BitcoinLib is platform independent and should run on your system. +Bitcoinlib is mainly developed on Ubuntu linux and runs unittests on every commit on Ubuntu and Windows. +Dockerfiles are available for Alpine, Kali and Fedora. You can find all dockerfiles on https://github.com/1200wd/bitcoinlib/tree/master/docker + +I run into an error 'x' when installing Bitcoinlib +-------------------------------------------------- + +1. Check the `installation page `_ and see if you have installed all the requirements. +2. Install the required packages one-by-one using pip install, and see if you get any specific errors. +3. Check for help in `Github Discussions `_. +4. See if you find any known `issue `_. +5. If it doesn't work out, do not hesitate to ask you question in the github discussions or post an issue! + +Does Bitcoinlib support 'x'-coin +-------------------------------- + +Bitcoinlib main focus is on Bitcoin. But besides Bitcoin it supports Litecoin and Dogecoin. For testing +it supports Bitcoin testnet3, Bitcoin regtest, Litecoin testnet and Dogecoin testnet. + +Support for Dash, Bitcoin Cash and Bitcoin SV has been dropped. + +There are currently no plans to support other coins. Main problem with supporting new coins is the lack of +service provides with a working and stable API. + +My wallet transactions are not (correctly) updating! +---------------------------------------------------- + +Most likely cause is a problem with a specific service provider. + +Please set log level to 'debug' and check the logs in bitcoinlib.log to see if you can pin down the specific error. +You could then disable the provider and post the `issue `_. + +To avoid these kind of errors it is adviced to run your local `Bcoin node `_. +With a local Bcoin node you do not depend on external Service providers which increases reliability, security, speed +and privacy. + +Can I use Bitcoinlib with another database besides SQLite? +---------------------------------------------------------- + +Yes, the library can also work with PostgreSQL or MySQL / MariaDB databases. +For more information see: `Databases `_. + +I found a bug! +-------------- + +Please help out project and post your `issue `_ on Github. +Try to include all code and data so we can reproduce and solve the issue. + +I have another question +----------------------- + +Maybe your question already has an answer om `Github Discussions `_. +Or search for an answer is this `documentation `_. + +If that does not answer your question, please post your question on on the +`Github Discussions Q&A `_. + + + +.. + My transaction is not confirming + I have imported a private key but address from other wallet does not match Bitcoinlib's address + Is Bitcoinlib secure? + Donations? + diff --git a/docs/_static/manuals.install.rst b/docs/_static/manuals.install.rst index b82adb87..b0d1b605 100644 --- a/docs/_static/manuals.install.rst +++ b/docs/_static/manuals.install.rst @@ -169,15 +169,19 @@ location for a config file in the BCL_CONFIG_FILE: os.environ['BCL_CONFIG_FILE'] = '/var/www/blocksmurfer/bitcoinlib.ini' -Tweak BitcoinLib ----------------- +Service providers and local nodes +--------------------------------- You can `Add another service Provider `_ to this library by updating settings and write a new service provider class. -If you use this library in a production environment it is advised to run your own Bcoin, Bitcoin, Litecoin or Dash node, -both for privacy and reliability reasons. More setup information: -`Setup connection to bitcoin node `_ +To increase reliability, speed and privacy or if you use this library in a production environment it +is advised to run your own Bcoin or Bitcoin node. + +More setup information: + +* `Setup connection to Bcoin node `_ +* `Setup connection to Bitcoin node `_ Some service providers require an API key to function or allow additional requests. You can add this key to the provider settings file in .bitcoinlib/providers.json diff --git a/docs/_static/manuals.security.rst b/docs/_static/manuals.security.rst index 9cde36d2..4b5a02b1 100644 --- a/docs/_static/manuals.security.rst +++ b/docs/_static/manuals.security.rst @@ -1,10 +1,10 @@ -10 Security and Privacy Tips -============================ +Frequently Asked Questions +========================== Ten tips for more privacy and security when using Bitcoin and Bitcoinlib: 1. Run your own `Bitcoin `_ - or Bcoin node, so you are not depending on external Blockchain API service providers anymore. + or `Bcoin `_ node, so you are not depending on external Blockchain API service providers anymore. This not only increases your privacy, but also makes your application much faster and more reliable. And as extra bonus you support the Bitcoin network. 2. Use multi-signature wallets. So you are able to store your private keys in separate (offline) locations. diff --git a/docs/_static/manuals.setup-bcoin.rst b/docs/_static/manuals.setup-bcoin.rst new file mode 100644 index 00000000..498841cd --- /dev/null +++ b/docs/_static/manuals.setup-bcoin.rst @@ -0,0 +1,42 @@ +How to connect Bitcoinlib to a Bcoin node +========================================= + +Bcoin is a full bitcoin node implementation, which can be used to parse the blockchain, send transactions and run a +wallet. With a Bcoin node you can retrieve transaction and utxo information for specific addresses, this is not easily +possible with a `Bitcoind `_ node. So if you want to use Bitcoinlib with a +wallet and not be dependant on external providers the best option is to run a local Bcoin node. + + +Install Bcoin node +------------------ + +You can find some instructions on how to install a bcoin node on https://coineva.com/install-bcoin-node-ubuntu.html. + +There are also some Docker images available. We have created a Docker image with the most optimal settings for +bitcoinlib. You can install them with the following command. + +.. code-block:: bash + + docker pull blocksmurfer/bcoin + + +Use Bcoin node with Bitcoinlib +------------------------------ + +To use Bcoin with bitcoinlib add the credentials to the providers.json configuration file in the .bitcoinlib directory. + +.. code-block:: text + + "bcoin": { + "provider": "bcoin", + "network": "bitcoin", + "client_class": "BcoinClient", + "provider_coin_id": "", + "url": "https://user:pass@localhost:8332/", + "api_key": "", + "priority": 20, + "denominator": 100000000, + "network_overrides": null + }, + +You can increase the priority so the Service object always connects to the Bcoin node first. diff --git a/docs/_static/manuals.setup-bitcoind-connection.rst b/docs/_static/manuals.setup-bitcoind-connection.rst index 2c38b555..d3193f86 100644 --- a/docs/_static/manuals.setup-bitcoind-connection.rst +++ b/docs/_static/manuals.setup-bitcoind-connection.rst @@ -1,4 +1,4 @@ -How to connect bitcoinlib to a bitcoin node +How to connect bitcoinlib to a Bitcoin node =========================================== This manual explains how to connect to a bitcoind server on your localhost or an a remote server. @@ -7,6 +7,12 @@ Running your own bitcoin node allows you to create a large number of requests, f and more control, privacy and independence. However you need to install and maintain it and it used a lot of resources. +.. warning:: + With a standard Bitcoin node you can only retrieve block and transaction information. You can not + query the node for information about specific addresses. So it not suitable to run in combination with a Bitcoinlib + wallet. If you would like to use Bitcoinlib wallets and not be dependent on external providers you should use a + `Bcoin node `_ instead. + Bitcoin node settings --------------------- @@ -16,47 +22,39 @@ For more information on how to install a full node read https://bitcoin.org/en/f Please make sure you have server and txindex option set to 1. +Generate a RPC authorization configuration string online: https://jlopp.github.io/bitcoin-core-rpc-auth-generator/ +or with the Python tool you can find in the Bitcoin repository: https://github.com/bitcoin/bitcoin/blob/master/share/rpcauth/rpcauth.py + So your bitcoin.conf file for testnet should look something like this. For mainnet use port 8332, and remove the 'testnet=1' line. .. code-block:: text - [rpc] - rpcuser=bitcoinrpc - rpcpassword=some_long_secure_password server=1 port=18332 txindex=1 testnet=1 + rpcauth=bitcoinlib:01cf8eb434e3c9434e244daf3fc1cc71$9cdfb346b76935569683c12858e13147eb5322399580ba51d2d878148a880d1d + rpcbind=0.0.0.0 + rpcallowip=192.168.0.0/24 +To increase your privacy and security, and for instance if you run a Bitcoin node on your home network, you can +use TOR. Bitcoind has TOR support build in, and it is ease to setup. +See https://en.bitcoin.it/wiki/Setting_up_a_Tor_hidden_service -Connect using config files --------------------------- - -Bitcoinlib looks for bitcoind config files on localhost. So if you running a full bitcoin node from -your local PC as the same user everything should work out of the box. +If you have a TOR service running you can add these lines to your bitcoin.conf settings to only use TOR. -Config files are read from the following files in this order: -* [USER_HOME_DIR]/.bitcoinlib/bitcoin.conf -* [USER_HOME_DIR]/.bitcoin/bitcoin.conf - -If your config files are at another location, you can specify this when you create a BitcoindClient -instance. - -.. code-block:: python +.. code-block:: text - from bitcoinlib.services.bitcoind import BitcoindClient - - bdc = BitcoindClient.from_config('/usr/local/src/.bitcoinlib/bitcoin.conf') - txid = 'e0cee8955f516d5ed333d081a4e2f55b999debfff91a49e8123d20f7ed647ac5' - rt = bdc.getrawtransaction(txid) - print("Raw: %s" % rt) + proxy=127.0.0.1:9050 + bind=127.0.0.1 + onlynet=onion Connect using provider settings ------------------------------- -Connection settings can also be added to the service provider settings file in +Connection settings can be added to the service provider settings file in .bitcoinlib/config/providers.json Example: @@ -79,7 +77,7 @@ Example: Connect using base_url argument ------------------------------- -Another options is to pass the 'base_url' argument to the BitcoindClient object directly. +You can also directly pass connection string wit the 'base_url' argument in the BitcoindClient object. This provides more flexibility but also the responsibility to store user and password information in a secure way. @@ -94,11 +92,27 @@ This provides more flexibility but also the responsibility to store user and pas print("Raw: %s" % rt) +You can directly r + +.. code-block:: python + + from bitcoinlib.services.bitcoind import BitcoindClient + + # Retrieve some blockchain information and statistics + bdc.proxy.getblockchaininfo() + bdc.proxy.getchaintxstats() + bdc.proxy.getmempoolinfo() + + # Add a node to the node list + bdc.proxy.addnode('blocksmurfer.io', 'add') + + + Please note: Using a remote bitcoind server ------------------------------------------- Using RPC over a public network is unsafe, so since bitcoind version 0.18 remote RPC for all network interfaces -is disabled. The rpcallowip option cannot be used to listen on all network interfaces and rpcbind has to be used to +are disabled. The rpcallowip option cannot be used to listen on all network interfaces and rpcbind has to be used to define specific IP addresses to listen on. See https://bitcoin.org/en/release/v0.18.0#configuration-option-changes You could setup a openvpn or ssh tunnel to connect to a remote server to avoid this issues. diff --git a/docs/conf.py b/docs/conf.py index 776715e6..ab253b49 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,7 +45,7 @@ # The short X.Y version. version = '0.6' # The full version, including alpha/beta/rc tags. -release = '0.6.14' +release = '0.6.15' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/index.rst b/docs/index.rst index a8ba2359..7b644038 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -226,12 +226,14 @@ For more examples see https://github.com/1200wd/bitcoinlib/tree/master/examples Installation and Settings source/_static/manuals.command-line-wallet - Add Service Provider Bitcoind Node + Bcoin Node + Add Service Provider Databases Encrypted Database Security & Privacy source/_static/manuals.caching + FAQ .. toctree:: diff --git a/docs/requirements.txt b/docs/requirements.txt index fc6b1bc9..1880f50c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,6 +2,6 @@ requests>=2.25.0 SQLAlchemy>=2.0.0 fastecdsa>=2.2.1 sphinx>=6.0.0 -sphinx_rtd_theme>=1.0.0 +sphinx_rtd_theme>=2.0.0 numpy>=1.22.0 -pycryptodome>=3.14.1 +pycryptodome>=3.16.0 diff --git a/setup.cfg b/setup.cfg index 38b30dd6..95aa31e1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = bitcoinlib -version = 0.6.14 +version = 0.6.15 url = http://github.com/1200wd/bitcoinlib author = 1200wd author_email = info@1200wd.com diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 547960c4..3e9bdc02 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2,7 +2,7 @@ # # BitcoinLib - Python Cryptocurrency Library # Unit Tests for Wallet Class -# © 2016 - 2024 February - 1200 Web Development +# © 2016 - 2023 May - 1200 Web Development # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -23,8 +23,10 @@ try: import mysql.connector - import psycopg - from psycopg import sql + from parameterized import parameterized_class + import psycopg2 + from psycopg2 import sql + from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT except ImportError as e: print("Could not import all modules. Error: %s" % e) # from psycopg2cffi import compat # Use for PyPy support @@ -42,55 +44,98 @@ DATABASE_NAME = 'bitcoinlib_test' DATABASE_NAME_2 = 'bitcoinlib2_test' -print("DATABASE USED: %s" % os.getenv('UNITTEST_DATABASE')) - - -def database_init(dbname=DATABASE_NAME): - session.close_all_sessions() - if os.getenv('UNITTEST_DATABASE') == 'postgresql': - con = psycopg.connect(user='postgres', host='localhost', password='postgres', autocommit=True) - cur = con.cursor() - try: - cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format(sql.Identifier(dbname))) - cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(dbname))) - except Exception as e: - print("Error exception %s" % str(e)) - pass - cur.close() - con.close() - return 'postgresql+psycopg://postgres:postgres@localhost:5432/' + dbname - elif os.getenv('UNITTEST_DATABASE') == 'mysql': - con = mysql.connector.connect(user='root', host='localhost', password='root') - cur = con.cursor() - cur.execute("DROP DATABASE IF EXISTS {}".format(dbname)) - cur.execute("CREATE DATABASE {}".format(dbname)) - con.commit() - cur.close() - con.close() - return 'mysql://root:root@localhost:3306/' + dbname - else: - dburi = os.path.join(str(BCL_DATABASE_DIR), '%s.sqlite' % dbname) - if os.path.isfile(dburi): - try: - os.remove(dburi) - except PermissionError: - db_obj = Db(dburi) - db_obj.drop_db(True) - db_obj.session.close() - db_obj.engine.dispose() - return dburi +db_uris = ( + ('sqlite', 'sqlite:///' + DATABASEFILE_UNITTESTS, 'sqlite:///' + DATABASEFILE_UNITTESTS_2),) + +print("UNITTESTS_FULL_DATABASE_TEST: %s" % UNITTESTS_FULL_DATABASE_TEST) + + +params = (('SCHEMA', 'DATABASE_URI', 'DATABASE_URI_2'), ( + db_uris +)) + +class TestWalletMixin: + SCHEMA = None -class TestWalletCreate(unittest.TestCase): + @classmethod + def create_db_if_needed(cls, db): + if cls.SCHEMA == 'postgresql': + con = psycopg2.connect(user='postgres', host='localhost', password='postgres') + con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) + cur = con.cursor() + try: + cur.execute(sql.SQL("CREATE DATABASE {}").format( + sql.Identifier(db)) + ) + except Exception: + pass + finally: + cur.close() + con.close() + elif cls.SCHEMA == 'mysql': + con = mysql.connector.connect(user='root', host='localhost') + cur = con.cursor() + cur.execute('CREATE DATABASE IF NOT EXISTS {}'.format(db)) + con.commit() + cur.close() + con.close() + + @classmethod + def db_remove(cls): + close_all_sessions() + if cls.SCHEMA == 'sqlite': + for db in [DATABASEFILE_UNITTESTS, DATABASEFILE_UNITTESTS_2]: + if os.path.isfile(db): + try: + os.remove(db) + except PermissionError: + db_obj = Db(db) + db_obj.drop_db(True) + db_obj.session.close() + db_obj.engine.dispose() + elif cls.SCHEMA == 'postgresql': + for db in [DATABASE_NAME, DATABASE_NAME_2]: + cls.create_db_if_needed(db) + con = psycopg2.connect(user='postgres', host='localhost', password='postgres', database=db) + con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) + cur = con.cursor() + try: + # drop all tables + cur.execute(sql.SQL(""" + DO $$ DECLARE + r RECORD; + BEGIN + FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = current_schema()) LOOP + EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE'; + END LOOP; + END $$;""")) + finally: + cur.close() + con.close() + elif cls.SCHEMA == 'mysql': + for db in [DATABASE_NAME, DATABASE_NAME_2]: + cls.create_db_if_needed(db) + con = mysql.connector.connect(user='root', host='localhost', database=db, autocommit=True) + cur = con.cursor(buffered=True) + try: + cur.execute("DROP DATABASE {};".format(db)) + cur.execute("CREATE DATABASE {};".format(db)) + finally: + cur.close() + con.close() + + +@parameterized_class(*params) +class TestWalletCreate(TestWalletMixin, unittest.TestCase): wallet = None - database_uri = None @classmethod def setUpClass(cls): - cls.database_uri = database_init() + cls.db_remove() cls.wallet = Wallet.create( - name='test_wallet_create', witness_type='legacy', - db_uri=cls.database_uri) + name='test_wallet_create', + db_uri=cls.DATABASE_URI) def test_wallet_create(self): self.assertTrue(isinstance(self.wallet, Wallet)) @@ -102,8 +147,8 @@ def test_wallet_info(self): self.assertIn("=\xfa\xe4\x1b-\xf4\x00uOX\xb9\xcd\xf33\x02?H\x0c~{\x96']) + self.assertListEqual(kms2.keys_private, + [b'w\xc5\x7fEC\xd2\xd2\xfa\xc8\xce\x9c\xa2\x96\x06\xda\xbc\xc7\xb3\xfav\x0b\xaeNl\x06*\xe2k\xf4-[\x12']) + +@parameterized_class(*params) +class TestWalletElectrum(TestWalletMixin, unittest.TestCase): wallet = None @classmethod def setUpClass(cls): - cls.database_uri = database_init() + cls.db_remove() cls.pk = 'xprv9s21ZrQH143K2fuscnMTwUadsPqEbYdFQVJ1uWPawUYi7C485NHhCiotGy6Kz3Cz7ReVr65oXNwhREZ8ePrz8p7zy' \ 'Hra82D1EGS7cQQmreK' cls.wallet = Wallet.create( keys=cls.pk, name='test_wallet_electrum', - db_uri=cls.database_uri) + db_uri=cls.DATABASE_URI) cls.wallet.key_path = ["m", "change", "address_index"] workdir = os.path.dirname(__file__) with open('%s/%s' % (workdir, 'electrum_keys.json')) as f: @@ -721,7 +772,7 @@ def test_electrum_keys(self): def test_wallet_electrum_p2pkh(self): phrase = 'smart donor clever resource stool denial wink under oak sand limb wagon' wlt = Wallet.create('wallet_electrum_p2pkh', phrase, network='bitcoin', witness_type='segwit', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) self.assertEqual(wlt.get_key().address, 'bc1qjz5l6mej6ptqlggfvdlys8pfukwqp8xnu0mn5u') self.assertEqual(wlt.get_key_change().address, 'bc1qz4tr569wfs2fuekgcjtdlz0eufk7rfs8gnu5j9') @@ -729,49 +780,44 @@ def test_wallet_electrum_p2sh_p2wsh(self): phrase1 = 'magnet voice math okay castle recall arrange music high sustain require crowd' phrase2 = 'wink tornado honey delay nest sing series timber album region suit spawn' wlt = Wallet.create('wallet_electrum_p2sh_p2wsh', [phrase1, phrase2], network='bitcoin', cosigner_id=0, - witness_type='p2sh-segwit', db_uri=self.database_uri) + witness_type='p2sh-segwit', db_uri=self.DATABASE_URI) self.assertEqual(wlt.get_key().address, '3ArRVGXfqcjw68XzUZr4iCCemrPoFZxm7s') self.assertEqual(wlt.get_key_change().address, '3FZEUFf59C3psUUiKB8TFbjsFUGWD73QPY') - @classmethod - def tearDownClass(cls): - del cls.wallet - -class TestWalletMultiCurrency(unittest.TestCase): +@parameterized_class(*params) +class TestWalletMultiCurrency(TestWalletMixin, unittest.TestCase): wallet = None @classmethod def setUpClass(cls): - cls.database_uri = database_init() + cls.db_remove() cls.pk = 'xprv9s21ZrQH143K4478MENLXpSXSvJSRYsjD2G3sY7s5sxAGubyujDmt9Qzfd1me5s1HokWGGKW9Uft8eB9dqryybAcFZ5JAs' \ 'rg84jAVYwKJ8c' cls.wallet = Wallet.create( - keys=cls.pk, network='dogecoin', witness_type='legacy', + keys=cls.pk, network='dash', name='test_wallet_multicurrency', - db_uri=cls.database_uri) + db_uri=cls.DATABASE_URI) cls.wallet.new_account(network='litecoin') cls.wallet.new_account(network='bitcoin') cls.wallet.new_account(network='testnet') - cls.wallet.new_account(network='dogecoin') + cls.wallet.new_account(network='dash') cls.wallet.new_key() cls.wallet.new_key() cls.wallet.new_key(network='bitcoin') def test_wallet_multiple_networks_defined(self): - networks_expected = sorted(['litecoin', 'bitcoin', 'dogecoin', 'testnet']) + networks_expected = sorted(['litecoin', 'bitcoin', 'dash', 'testnet']) networks_wlt = sorted([x.name for x in self.wallet.networks()]) self.assertListEqual(networks_wlt, networks_expected, msg="Not all network are defined correctly for this wallet") def test_wallet_multiple_networks_default_addresses(self): - addresses_expected = ['D5RuWXkLEWavHvFBanskaP2LFKTYg6J6fU', - 'DHUXe7QJfCo1gewXHsLyBB98zd6quWyxEK', - 'DSaM5oJ7rRbrVcSYuTc5KE21paw9kWqLf7', - 'DToob5uhE3hCMaCZxYd4S5eunFgr5f8XhD', - 'DAPKhNHuidSyzhBypVdnc5fRY3pcvihLgs'] - self.assertListEqual(self.wallet.addresslist(network='dogecoin'), addresses_expected) + addresses_expected = ['XqTpf6NYrrckvsauJKfHFBzZaD9wRHjQtv', 'Xamqfy4y21pXMUP8x8TeTPWCNzsKrhSfau', + 'XugknDhBtJFvfErjaobizCa8KAEDVU7bCJ', 'Xj6tV9Jc3qJ2AszpNxvEq7KVQKUMcfmBqH', + 'XgkpZbqaRsRb2e2BC1QsWxTDEfW6JVpP6r'] + self.assertListEqual(self.wallet.addresslist(network='dash'), addresses_expected) def test_wallet_multiple_networks_import_key(self): pk_bitcoin = 'xprv9s21ZrQH143K3RBvuNbSwpAHxXuPNWMMPfpjuX6ciwo91HpYq6gDLjZuyrQCPpo4qBDXyvftN7MdX7SBVXeGgHs' \ @@ -787,17 +833,17 @@ def test_wallet_multiple_networks_import_key_network(self): self.assertIn(address_ltc, addresses_ltc_in_wallet) def test_wallet_multiple_networks_import_error(self): - pk_test = ('BC19UtECk2r9PVQYhZYzX3m4arsu6tCL5VMpKPbeGpZdpzd9FHweoSRreTFKo96FAEFsUWBrASfKussgoxTrNQfm' - 'jWFrVraLbiHf4gCkUvwHEocA') + pk_dashtest = 'YXsfembHRwpatrAVUGY8MBUuKwhUDf9EEWeZwGoEfE5appg5rLjSfZ1GwoaNB5DgUZ2aVuU1ezmg7zDefubuWkZ17et5o' \ + 'KoMgKnjvAZ6a4Yn2QZg' error_str = "Network bitcoinlib_test not available in this wallet, please create an account for this network " \ "first." - self.assertRaisesRegex(WalletError, error_str, self.wallet.import_key, pk_test) + self.assertRaisesRegex(WalletError, error_str, self.wallet.import_key, pk_dashtest) def test_wallet_multiple_networks_value(self): pk = 'vprv9DMUxX4ShgxMM1FFB24BgXE3fMYXKicceSdMUtfhyyUzKNkCvPeYrcoZpPezahBEzFc23yHTPj46eqx3jKuQpQFq5kbd2oxDysd' \ 'teSN16sH' - wallet_delete_if_exists('test_wallet_multiple_networks_value', force=True, db_uri=self.database_uri) - w = wallet_create_or_open('test_wallet_multiple_networks_value', keys=pk, db_uri=self.database_uri) + wallet_delete_if_exists('test_wallet_multiple_networks_value', force=True, db_uri=self.DATABASE_URI) + w = wallet_create_or_open('test_wallet_multiple_networks_value', keys=pk, db_uri=self.DATABASE_URI) w.new_account(network='bitcoin') w.new_account(network='bitcoinlib_test') w.utxos_update(networks='testnet') @@ -810,23 +856,20 @@ def test_wallet_multiple_networks_value(self): self.assertFalse(t.pushed) self.assertTrue(t.verified) - @classmethod - def tearDownClass(cls): - del cls.wallet - -class TestWalletMultiNetworksMultiAccount(unittest.TestCase): +@parameterized_class(*params) +class TestWalletMultiNetworksMultiAccount(TestWalletMixin, unittest.TestCase): @classmethod def setUpClass(cls): - cls.database_uri = database_init() + cls.db_remove() def test_wallet_multi_networks_send_transaction(self): pk = 'tobacco defy swarm leaf flat pyramid velvet pen minor twist maximum extend' wallet = Wallet.create( keys=pk, network='bitcoin', - name='test_wallet_multi_network_multi_account', witness_type='legacy', - db_uri=self.database_uri) + name='test_wallet_multi_network_multi_account', + db_uri=self.DATABASE_URI) wallet.new_key() acc = wallet.new_account('BCL test home', network='bitcoinlib_test') @@ -838,12 +881,13 @@ def test_wallet_multi_networks_send_transaction(self): wallet.new_key(account_id=acc.account_id, network='bitcoinlib_test') wallet.get_keys(network='testnet', number_of_keys=2) wallet.get_key(network='testnet', change=1) + # wallet.utxos_update(networks='testnet') self.assertEqual(wallet.balance(network='bitcoinlib_test', account_id=0), 0) self.assertEqual(wallet.balance(network='bitcoinlib_test', account_id=1), 600000000) self.assertEqual(wallet.balance(network='testnet'), 0) - tbtc_addresses = ['mhHhSx66jdXdUPu2A8pXsCBkX1UvHmSkUJ', 'mrdtENj75WUfrJcZuRdV821tVzKA4VtCBf', + ltct_addresses = ['mhHhSx66jdXdUPu2A8pXsCBkX1UvHmSkUJ', 'mrdtENj75WUfrJcZuRdV821tVzKA4VtCBf', 'mmWFgfG43tnP2SJ8u8UDN66Xm63okpUctk'] - self.assertListEqual(wallet.addresslist(network='testnet'), tbtc_addresses) + self.assertListEqual(wallet.addresslist(network='testnet'), ltct_addresses) t = wallet.send_to('21EsLrvFQdYWXoJjGX8LSEGWHFJDzSs2F35', 10000000, account_id=1, network='bitcoinlib_test', fee=1000, offline=False) @@ -855,13 +899,13 @@ def test_wallet_multi_networks_send_transaction(self): del wallet def test_wallet_multi_networks_account_bip44_code_error(self): - wlt = Wallet.create("wallet-bip44-code-error", network='testnet', db_uri=self.database_uri) + wlt = Wallet.create("wallet-bip44-code-error", network='testnet', db_uri=self.DATABASE_URI) error_str = "Can not create new account for network litecoin_testnet with same BIP44 cointype" self.assertRaisesRegex(WalletError, error_str, wlt.new_account, network='litecoin_testnet') def test_wallet_get_account_defaults(self): w = wallet_create_or_open("test_wallet_get_account_defaults", witness_type='segwit', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) w.get_key(network='litecoin', account_id=100) network, account_id, account_key = w._get_account_defaults(network='litecoin') self.assertEqual(network, 'litecoin') @@ -869,7 +913,7 @@ def test_wallet_get_account_defaults(self): self.assertIn('account', account_key.name) def test_wallet_update_attributes(self): - w = Wallet.create('test_wallet_set_attributes', witness_type='legacy', db_uri=self.database_uri) + w = Wallet.create('test_wallet_set_attributes', db_uri=self.DATABASE_URI) w.new_account(network='litecoin', account_id=1200) owner = 'Satoshi' w.owner = owner @@ -877,24 +921,25 @@ def test_wallet_update_attributes(self): w.default_account_id = 1200 del w - w2 = Wallet('test_wallet_set_attributes', db_uri=self.database_uri) + w2 = Wallet('test_wallet_set_attributes', db_uri=self.DATABASE_URI) self.assertEqual(w2.owner, owner) self.assertEqual(w2.network.name, 'litecoin') nk = w2.new_key() self.assertEqual(nk.path, "m/44'/2'/1200'/0/1") -class TestWalletBitcoinlibTestnet(unittest.TestCase): +@parameterized_class(*params) +class TestWalletBitcoinlibTestnet(TestWalletMixin, unittest.TestCase): @classmethod def setUpClass(cls): - cls.database_uri = database_init() + cls.db_remove() def test_wallet_bitcoinlib_testnet_sendto(self): w = Wallet.create( network='bitcoinlib_test', name='test_wallet_bitcoinlib_testnet_sendto', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) w.new_key() w.utxos_update() @@ -906,7 +951,7 @@ def test_wallet_bitcoinlib_testnet_send_utxos_updated(self): w = Wallet.create( network='bitcoinlib_test', name='test_wallet_bitcoinlib_testnet_send_utxos_updated', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) w.utxos_update() self.assertEqual(len(w.utxos()), 2) @@ -917,7 +962,7 @@ def test_wallet_bitcoinlib_testnet_sendto_no_funds_txfee(self): w = Wallet.create( network='bitcoinlib_test', name='test_wallet_bitcoinlib_testnet_sendto_no_funds_txfee', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) w.new_key() w.utxos_update() balance = w.balance() @@ -928,7 +973,7 @@ def test_wallet_bitcoinlib_testnet_sweep(self): w = Wallet.create( network='bitcoinlib_test', name='test_wallet_bitcoinlib_testnet_sweep', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) w.new_key() w.new_key() w.new_key() @@ -939,23 +984,21 @@ def test_wallet_bitcoinlib_testnet_sweep(self): w.sweep, '21DBmFUMQMP7A6KeENXgZQ4wJdSCeGc2zFo', offline=False) -class TestWalletMultisig(unittest.TestCase): - database_uri = None - database_uri_2 = None +@parameterized_class(*params) +class TestWalletMultisig(TestWalletMixin, unittest.TestCase): @classmethod def setUpClass(cls): - cls.database_uri = database_init() - cls.database_uri_2 = database_init(DATABASE_NAME_2) + cls.db_remove() def test_wallet_multisig_2_wallets_private_master_plus_account_public(self): - # self.db_remove() + self.db_remove() pk1 = 'tprv8ZgxMBicQKsPdPVdNSEeAhagkU6tUDhUQi8DcCTmJyNLUyU7svTFzXQdkYqNJDEtQ3S2wAspz3K56CMcmMsZ9eXZ2nkNq' \ 'gVxJhMHq3bGJ1X' pk1_acc_pub = 'tpubDCZUk9HLxh5gdB9eC8FUxPB1AbZtsSnbvyrAAzsC8x3tiYDgbzyxcngU99rG333jegHG5vJhs11AHcSVkbwrU' \ 'bYEsPK8vA7E6yFB9qbsTYi' - w1 = Wallet.create(name='test_wallet_create_1', keys=pk1, db_uri=self.database_uri) - w2 = Wallet.create(name='test_wallet_create_2', keys=pk1_acc_pub, db_uri=self.database_uri) + w1 = Wallet.create(name='test_wallet_create_1', keys=pk1, db_uri=self.DATABASE_URI) + w2 = Wallet.create(name='test_wallet_create_2', keys=pk1_acc_pub, db_uri=self.DATABASE_URI) wk1 = w1.new_key() wk2 = w2.new_key() self.assertTrue(wk1.is_private) @@ -963,7 +1006,7 @@ def test_wallet_multisig_2_wallets_private_master_plus_account_public(self): self.assertEqual(wk1.address, wk2.address) def test_wallet_multisig_create_2_cosigner_wallets(self): - # self.db_remove() + self.db_remove() pk_wif1 = 'tprv8ZgxMBicQKsPdvHCP6VxtFgowj2k7nBJnuRiVWE4DReDFojkLjyqdT8mtR6XJK9dRBcaa3RwvqiKFjsEQVhKfQmHZCCY' \ 'f4jRTWvJuVuK67n' pk_wif2 = 'tprv8ZgxMBicQKsPdkJVWDkqQQAMVYB2usfVs3VS2tBEsFAzjC84M3TaLMkHyJWjydnJH835KHvksS92ecuwwWFEdLAAccwZ' \ @@ -972,17 +1015,17 @@ def test_wallet_multisig_create_2_cosigner_wallets(self): pk2 = HDKey(pk_wif2, network='testnet') wl1 = Wallet.create('multisig_test_wallet1', [pk_wif1, pk2.subkey_for_path("m/45'").wif_public()], - sigs_required=2, network='testnet', db_uri=self.database_uri) + sigs_required=2, network='testnet', db_uri=self.DATABASE_URI) wl2 = Wallet.create('multisig_test_wallet2', [pk1.subkey_for_path("m/45'").wif_public(), pk_wif2], - sigs_required=2, network='testnet', db_uri=self.database_uri) + sigs_required=2, network='testnet', db_uri=self.DATABASE_URI) wl1_key = wl1.new_key() wl2_key = wl2.new_key(cosigner_id=wl1.cosigner_id) self.assertEqual(wl1_key.address, wl2_key.address) self.assertRaisesRegex(WalletError, "Accounts are not supported for this wallet", wl1.account, 10) def test_wallet_multisig_bitcoinlib_testnet_transaction_send(self): - # self.db_remove() + self.db_remove() key_list = [ 'Pdke4WfXvALPdbrKEfBU9z9BNuRNbv1gRr66BEiZHKcRXDSZQ3gV', @@ -992,7 +1035,7 @@ def test_wallet_multisig_bitcoinlib_testnet_transaction_send(self): # Create wallet and generate key wl = Wallet.create('multisig_test_simple', key_list, sigs_required=2, network='bitcoinlib_test', - cosigner_id=0, db_uri=self.database_uri) + cosigner_id=0, db_uri=self.DATABASE_URI) wl.new_key() # Sign, verify and send transaction @@ -1004,16 +1047,16 @@ def test_wallet_multisig_bitcoinlib_testnet_transaction_send(self): self.assertIsNone(t.error) def test_wallet_multisig_bitcoin_transaction_send_offline(self): - # self.db_remove() - pk2 = HDKey('e2cbed99ad03c500f2110f1a3c90e0562a3da4ba0cff0e74028b532c3d69d29d', witness_type='legacy') + self.db_remove() + pk2 = HDKey('e2cbed99ad03c500f2110f1a3c90e0562a3da4ba0cff0e74028b532c3d69d29d') key_list = [ - HDKey('e9e5095d3e26643cc4d996efc6cb9a8d8eb55119fdec9fa28a684ba297528067', witness_type='legacy'), + HDKey('e9e5095d3e26643cc4d996efc6cb9a8d8eb55119fdec9fa28a684ba297528067'), pk2.public_master(multisig=True).public(), HDKey('86b77aee5cfc3a55eb0b1099752479d82cb6ebaa8f1c4e9ef46ca0d1dc3847e6').public_master( - multisig=True, witness_type='legacy').public(), + multisig=True).public(), ] - wl = Wallet.create('multisig_test_bitcoin_send', key_list, sigs_required=2, witness_type='legacy', - db_uri=self.database_uri) + wl = Wallet.create('multisig_test_bitcoin_send', key_list, sigs_required=2, + db_uri=self.DATABASE_URI) wl.utxo_add(wl.get_key().address, 200000, '46fcfdbdc3573756916a0ced8bbc5418063abccd2c272f17bf266f77549b62d5', 0, 1) t = wl.transaction_create([('3CuJb6XrBNddS79vr27SwqgR4oephY6xiJ', 100000)], fee=10000) @@ -1024,7 +1067,7 @@ def test_wallet_multisig_bitcoin_transaction_send_offline(self): self.assertEqual(t.export()[0][2], 'out') def test_wallet_multisig_bitcoin_transaction_send_no_subkey_for_path(self): - # self.db_remove() + self.db_remove() pk2 = HDKey('e2cbed99ad03c500f2110f1a3c90e0562a3da4ba0cff0e74028b532c3d69d29d') key_list = [ HDKey('e9e5095d3e26643cc4d996efc6cb9a8d8eb55119fdec9fa28a684ba297528067'), @@ -1032,7 +1075,7 @@ def test_wallet_multisig_bitcoin_transaction_send_no_subkey_for_path(self): HDKey('86b77aee5cfc3a55eb0b1099752479d82cb6ebaa8f1c4e9ef46ca0d1dc3847e6').public_master(multisig=True), ] wl = Wallet.create('multisig_test_bitcoin_send2', key_list, sigs_required=2, - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) wl.utxo_add(wl.get_key().address, 200000, '46fcfdbdc3573756916a0ced8bbc5418063abccd2c272f17bf266f77549b62d5', 0, 1) t = wl.transaction_create([('3CuJb6XrBNddS79vr27SwqgR4oephY6xiJ', 100000)], fee=10000) @@ -1042,14 +1085,14 @@ def test_wallet_multisig_bitcoin_transaction_send_no_subkey_for_path(self): self.assertIsNone(t.error) def test_wallet_multisig_bitcoin_transaction_send_fee_priority(self): - # self.db_remove() + self.db_remove() pk2 = HDKey('e2cbed99ad03c500f2110f1a3c90e0562a3da4ba0cff0e74028b532c3d69d29d') key_list = [ HDKey('e9e5095d3e26643cc4d996efc6cb9a8d8eb55119fdec9fa28a684ba297528067'), pk2.public_master(multisig=True), HDKey('86b77aee5cfc3a55eb0b1099752479d82cb6ebaa8f1c4e9ef46ca0d1dc3847e6').public_master(multisig=True), ] - wl = Wallet.create('multisig_test_bitcoin_send3', key_list, sigs_required=2, db_uri=self.database_uri) + wl = Wallet.create('multisig_test_bitcoin_send3', key_list, sigs_required=2, db_uri=self.DATABASE_URI) wl.utxo_add(wl.get_key().address, 20000000, '46fcfdbdc3573756916a0ced8bbc5418063abccd2c272f17bf266f77549b62d5', 0, 1) t = wl.sweep('3CuJb6XrBNddS79vr27SwqgR4oephY6xiJ', fee='low') @@ -1063,19 +1106,17 @@ def test_wallet_multisig_bitcoin_transaction_send_fee_priority(self): self.assertIsNone(t2.error) def test_wallet_multisig_litecoin_transaction_send_offline(self): - # self.db_remove() + self.db_remove() network = 'litecoin_legacy' - pk2 = HDKey('e2cbed99ad03c500f2110f1a3c90e0562a3da4ba0cff0e74028b532c3d69d29d', witness_type='legacy', - network=network) + pk2 = HDKey('e2cbed99ad03c500f2110f1a3c90e0562a3da4ba0cff0e74028b532c3d69d29d', network=network) key_list = [ - HDKey('e9e5095d3e26643cc4d996efc6cb9a8d8eb55119fdec9fa28a684ba297528067', witness_type='legacy', - network=network), + HDKey('e9e5095d3e26643cc4d996efc6cb9a8d8eb55119fdec9fa28a684ba297528067', network=network), pk2.public_master(multisig=True), HDKey('86b77aee5cfc3a55eb0b1099752479d82cb6ebaa8f1c4e9ef46ca0d1dc3847e6', - witness_type='legacy', network=network).public_master(multisig=True), + network=network).public_master(multisig=True), ] wl = Wallet.create('multisig_test_litecoin_send', key_list, sigs_required=2, network=network, - witness_type='legacy', db_uri=self.database_uri) + db_uri=self.DATABASE_URI) wl.get_keys(number_of_keys=2) wl.utxo_add(wl.get_key().address, 200000, '46fcfdbdc3573756916a0ced8bbc5418063abccd2c272f17bf266f77549b62d5', 0, 1) @@ -1092,20 +1133,20 @@ def test_wallet_multisig_2of2(self): and verify created transaction. """ - # self.db_remove() + self.db_remove() keys = [ - HDKey('BC12Se7KL1uS2bA6QQaWWrcA5kApD8UAM78dx91LrFvsvdvua3irnpQNjHUTCPJR7tZ72eGhMsy3mLPp5C' - 'SJcmKPchBvaf72i1mNY6yhrmY4RFSr', network='bitcoinlib_test', witness_type='legacy'), - HDKey('BC12Se7KL1uS2bA6QPiq4cdXWKfmQwuPPTXkRUJNBSLFZt9tPgLfgrRSfkVLRLYCYpgzsTmKybPHSX165w' - '42VBjw4Neub1KyPBfNpjFfgx9SyynF', network='bitcoinlib_test', witness_type='legacy')] + HDKey('YXscyqNJ5YK411nwB4wzazXjJn9L9iLAR1zEMFcpLipDA25rZregBGgwXmprsvQLeQAsuTvemtbCWR1AHaPv2qmvkartoiFUU6' + 'qu1uafT2FETtXT', network='bitcoinlib_test'), + HDKey('YXscyqNJ5YK411nwB4EyGbNZo9eQSUWb64vAFKHt7E2LYnbmoNz8Gyjs6xc7iYAudcnkgf127NPnaanuUgyRngAiwYBcXKGsSJ' + 'wadGhxByT2MnLd', network='bitcoinlib_test')] msw1 = Wallet.create('msw1', [keys[0], keys[1].subkey_for_path("m/45'").wif_public()], network='bitcoinlib_test', sort_keys=False, sigs_required=2, - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) msw2 = Wallet.create('msw2', [keys[0].subkey_for_path("m/45'").wif_public(), keys[1]], network='bitcoinlib_test', sort_keys=False, sigs_required=2, - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) msw1.new_key() self.assertEqual(len(msw1.get_key().key()), 2) msw2.new_key(cosigner_id=0) @@ -1126,18 +1167,18 @@ def test_wallet_multisig_2of2_different_database(self): separate databases to check for database interference. """ - # self.db_remove() + self.db_remove() keys = [ - HDKey('BC12Se7KL1uS2bA6QQaWWrcA5kApD8UAM78dx91LrFvsvdvua3irnpQNjHUTCPJR7tZ72eGhMsy3mLPp5C' - 'SJcmKPchBvaf72i1mNY6yhrmY4RFSr', network='bitcoinlib_test', witness_type='legacy'), - HDKey('BC12Se7KL1uS2bA6QPiq4cdXWKfmQwuPPTXkRUJNBSLFZt9tPgLfgrRSfkVLRLYCYpgzsTmKybPHSX165w' - '42VBjw4Neub1KyPBfNpjFfgx9SyynF', network='bitcoinlib_test', witness_type='legacy')] - - msw1 = Wallet.create('msw-2-2-1', [keys[0], keys[1].public_master(multisig=True)], network='bitcoinlib_test', - sort_keys=False, sigs_required=2, db_uri=self.database_uri) - msw2 = Wallet.create('msw-2-2-2', [keys[0].public_master(multisig=True), keys[1]], network='bitcoinlib_test', - sort_keys=False, sigs_required=2, db_uri=self.database_uri_2) + HDKey('YXscyqNJ5YK411nwB4wzazXjJn9L9iLAR1zEMFcpLipDA25rZregBGgwXmprsvQLeQAsuTvemtbCWR1AHaPv2qmvkartoiFUU6' + 'qu1uafT2FETtXT', network='bitcoinlib_test'), + HDKey('YXscyqNJ5YK411nwB4EyGbNZo9eQSUWb64vAFKHt7E2LYnbmoNz8Gyjs6xc7iYAudcnkgf127NPnaanuUgyRngAiwYBcXKGsSJ' + 'wadGhxByT2MnLd', network='bitcoinlib_test')] + + msw1 = Wallet.create('msw1', [keys[0], keys[1].public_master(multisig=True)], network='bitcoinlib_test', + sort_keys=False, sigs_required=2, db_uri=self.DATABASE_URI) + msw2 = Wallet.create('msw2', [keys[0].public_master(multisig=True), keys[1]], network='bitcoinlib_test', + sort_keys=False, sigs_required=2, db_uri=self.DATABASE_URI_2) msw1.new_key() msw2.new_key(cosigner_id=msw1.cosigner_id) msw1.utxos_update() @@ -1164,8 +1205,7 @@ def _multisig_test(cls, sigs_required, number_of_sigs, sort_keys, network, witne wallet_dict = {} wallet_keys = {} for wallet_id in range(number_of_sigs): - wallet_name = 'multisig-%d-%d-%d-%s-%s-%s' % (wallet_id, sigs_required, number_of_sigs, witness_type, - network, int(sort_keys)) + wallet_name = 'multisig-%d' % wallet_id key_list = [] for key_id in key_dict: if key_id == wallet_id: @@ -1174,7 +1214,7 @@ def _multisig_test(cls, sigs_required, number_of_sigs, sort_keys, network, witne key_list.append(key_dict[key_id].public_master(multisig=True, as_private=True)) wallet_dict[wallet_id] = Wallet.create( wallet_name, key_list, sigs_required=sigs_required, network=network, sort_keys=sort_keys, cosigner_id=0, - witness_type=witness_type, db_uri=cls.database_uri) + witness_type=witness_type, db_uri=cls.DATABASE_URI) wallet_keys[wallet_id] = wallet_dict[wallet_id].new_key() wallet_dict[wallet_id].utxos_update() @@ -1197,46 +1237,46 @@ def _multisig_test(cls, sigs_required, number_of_sigs, sort_keys, network, witne t = wallet_dict[wallet_id].transaction_import(t) t.sign() n_signs += 1 - wlt.session.close() + wlt._session.close() return t def test_wallet_multisig_2of3(self): - # self.db_remove() + self.db_remove() t = self._multisig_test(2, 3, False, 'bitcoinlib_test') self.assertTrue(t.verify()) def test_wallet_multisig_2of3_segwit(self): - # self.db_remove() + self.db_remove() t = self._multisig_test(2, 3, False, 'bitcoinlib_test', 'segwit') self.assertTrue(t.verify()) def test_wallet_multisig_2of3_sorted(self): - # self.db_remove() + self.db_remove() t = self._multisig_test(2, 3, True, 'bitcoinlib_test') self.assertTrue(t.verify()) def test_wallet_multisig_3of5(self): - # self.db_remove() + self.db_remove() t = self._multisig_test(3, 5, False, 'bitcoinlib_test') self.assertTrue(t.verify()) def test_wallet_multisig_3of5_segwit(self): - # self.db_remove() + self.db_remove() t = self._multisig_test(3, 5, False, 'bitcoinlib_test', 'segwit') self.assertTrue(t.verify()) def test_wallet_multisig_2of2_with_single_key(self): - # self.db_remove() + self.db_remove() keys = [HDKey(network='bitcoinlib_test'), HDKey(network='bitcoinlib_test', key_type='single')] key_list = [keys[0], keys[1].public()] wl = Wallet.create('multisig_expk2', key_list, sigs_required=2, network='bitcoinlib_test', - db_uri=self.database_uri, sort_keys=False) + db_uri=self.DATABASE_URI, sort_keys=False) k1 = wl.new_key() k2 = wl.new_key() k3 = wl.new_key_change() wl.utxos_update() - self.assertEqual(wl.public_master()[1].wif, keys[1].wif(multisig=True)) + self.assertEqual(wl.public_master()[1].wif, keys[1].wif()) key_names = [k.name for k in wl.keys(is_active=False)] self.assertListEqual(key_names, [k1.name, k2.name, k3.name]) @@ -1246,16 +1286,16 @@ def test_wallet_multisig_2of2_with_single_key(self): self.assertIsNone(t.error) def test_wallet_multisig_sorted_keys(self): - # self.db_remove() + self.db_remove() key1 = HDKey() key2 = HDKey() key3 = HDKey() w1 = Wallet.create('w1', [key1, key2.public_master(multisig=True), key3.public_master(multisig=True)], - sigs_required=2, db_uri=self.database_uri) + sigs_required=2, db_uri=self.DATABASE_URI) w2 = Wallet.create('w2', [key1.public_master(multisig=True), key2, key3.public_master(multisig=True)], - sigs_required=2, db_uri=self.database_uri) + sigs_required=2, db_uri=self.DATABASE_URI) w3 = Wallet.create('w3', [key1.public_master(multisig=True), key2.public_master(multisig=True), key3], - sigs_required=2, db_uri=self.database_uri) + sigs_required=2, db_uri=self.DATABASE_URI) for _ in range(5): cosigner_id = random.randint(0, 2) address1 = w1.new_key(cosigner_id=cosigner_id).address @@ -1265,7 +1305,7 @@ def test_wallet_multisig_sorted_keys(self): 'Different addressed generated: %s %s %s' % (address1, address2, address3)) def test_wallet_multisig_sign_with_external_single_key(self): - # self.db_remove() + self.db_remove() network = 'bitcoinlib_test' words = 'square innocent drama' seed = Mnemonic().to_seed(words, 'password') @@ -1277,8 +1317,8 @@ def test_wallet_multisig_sign_with_external_single_key(self): HDKey(network=network), hdkey.public() ] - wallet = Wallet.create('test_wallet_multisig_sign_with_external_single_key', - key_list, sigs_required=2, network=network, db_uri=self.database_uri) + wallet = Wallet.create('Multisig-2-of-3-example', key_list, sigs_required=2, network=network, + db_uri=self.DATABASE_URI) wallet.new_key() wallet.utxos_update() wt = wallet.send_to('21A6yyUPRL9hZZo1Rw4qP5G6h9idVVLUncE', 10000000, offline=False) @@ -1291,19 +1331,19 @@ def test_wallet_multisig_reopen_wallet(self): def _open_all_wallets(): wl1 = wallet_create_or_open( 'multisigmulticur1_tst', sigs_required=2, network=network, - db_uri=self.database_uri, sort_keys=False, witness_type='segwit', + db_uri=self.DATABASE_URI, sort_keys=False, keys=[pk1, pk2.public_master(), pk3.public_master()]) wl2 = wallet_create_or_open( 'multisigmulticur2_tst', sigs_required=2, network=network, - db_uri=self.database_uri, sort_keys=False, witness_type='segwit', + db_uri=self.DATABASE_URI, sort_keys=False, keys=[pk1.public_master(), pk2, pk3.public_master()]) wl3 = wallet_create_or_open( 'multisigmulticur3_tst', sigs_required=2, network=network, - db_uri=self.database_uri, sort_keys=False, witness_type='segwit', + db_uri=self.DATABASE_URI, sort_keys=False, keys=[pk1.public_master(), pk2.public_master(), pk3]) return wl1, wl2, wl3 - # self.db_remove() + self.db_remove() network = 'litecoin' phrase1 = 'shop cloth bench traffic vintage security hour engage omit almost episode fragile' phrase2 = 'exclude twice mention orchard grit ignore display shine cheap exercise same apart' @@ -1313,25 +1353,23 @@ def _open_all_wallets(): pk3 = HDKey.from_passphrase(phrase3, multisig=True, network=network) wallets = _open_all_wallets() for wlt in wallets: - self.assertEqual(wlt.get_key(cosigner_id=1).address, - 'ltc1qmw3e97pgrwypr0378wjje984guu0jy3ye4n523lcymk3rctuef6q7t3sek') + self.assertEqual(wlt.get_key(cosigner_id=1).address, 'MQVt7KeRHGe35b9ziZo16T5y4fQPg6Up7q') del wallets wallets2 = _open_all_wallets() for wlt in wallets2: - self.assertEqual(wlt.get_key(cosigner_id=1).address, - 'ltc1qmw3e97pgrwypr0378wjje984guu0jy3ye4n523lcymk3rctuef6q7t3sek') + self.assertEqual(wlt.get_key(cosigner_id=1).address, 'MQVt7KeRHGe35b9ziZo16T5y4fQPg6Up7q') def test_wallet_multisig_network_mixups(self): - # self.db_remove() + self.db_remove() network = 'litecoin_testnet' phrase1 = 'shop cloth bench traffic vintage security hour engage omit almost episode fragile' phrase2 = 'exclude twice mention orchard grit ignore display shine cheap exercise same apart' phrase3 = 'citizen obscure tribe index little welcome deer wine exile possible pizza adjust' - pk2 = HDKey.from_passphrase(phrase2, multisig=True, network=network, witness_type='legacy') - pk3 = HDKey.from_passphrase(phrase3, multisig=True, network=network, witness_type='legacy') + pk2 = HDKey.from_passphrase(phrase2, multisig=True, network=network) + pk3 = HDKey.from_passphrase(phrase3, multisig=True, network=network) wlt = wallet_create_or_open( - 'multisig_network_mixups', sigs_required=2, network=network, db_uri=self.database_uri, - keys=[phrase1, pk2.public_master(), pk3.public_master()], witness_type='legacy', + 'multisig_network_mixups', sigs_required=2, network=network, db_uri=self.DATABASE_URI, + keys=[phrase1, pk2.public_master(), pk3.public_master()], sort_keys=False) self.assertEqual(wlt.get_key().address, 'QjecchURWzhzUzLkhJ8Xijnm29Z9PscSqD') self.assertEqual(wlt.get_key().network.name, network) @@ -1339,7 +1377,7 @@ def test_wallet_multisig_network_mixups(self): def test_wallet_multisig_info(self): w = Wallet.create('test_wallet_multisig_info', keys=[HDKey(network='bitcoinlib_test'), HDKey(network='bitcoinlib_test')], - network='bitcoinlib_test', cosigner_id=0, db_uri=self.database_uri) + network='bitcoinlib_test', cosigner_id=0, db_uri=self.DATABASE_URI) w.utxos_update() w.info(detail=6) @@ -1357,7 +1395,7 @@ def test_wallets_multisig_missing_private_and_cosigner(self): "This wallet does not contain any private keys, please specify cosigner_id for " "this wallet", wallet_create_or_open, 'test_wallets_multisig_missing_private_and_cosigner', - keys=[hdkey0, hdkey1, hdkey2], db_uri=self.database_uri) + keys=[hdkey0, hdkey1, hdkey2], db_uri=self.DATABASE_URI) def test_wallets_multisig_with_single_key_cosigner(self): k0 = 'xprv9s21ZrQH143K459uwGGCU3Wj3v1LFFJ42tgyTsNnr6p2BS6FZ9jQ7fmZMMnqsWSi2BBgpX3hFbR4ode8Jx58ibSNeaBLFQ68Xs3' \ @@ -1371,7 +1409,7 @@ def test_wallets_multisig_with_single_key_cosigner(self): hdkey2 = HDKey(k2, key_type='single') w = wallet_create_or_open('test_wallets_multisig_with_single_key_cosigner0', keys=[hdkey0, hdkey1, hdkey2], - cosigner_id=0, db_uri=self.database_uri) + cosigner_id=0, db_uri=self.DATABASE_URI) w.new_key(cosigner_id=2) w.new_key(cosigner_id=2) # Cosigner 0 use a single key wallet, so calling new_key() repeatedly has no effect @@ -1382,7 +1420,7 @@ def test_wallets_multisig_with_single_key_cosigner(self): self.assertEqual(w.keys()[1].address, '3K2eBv2hm3SjhVRaJJK8Dt7wMb8mRTWcMH') w2 = wallet_create_or_open('test_wallets_multisig_with_single_key_cosigner2', keys=[hdkey0, hdkey1, hdkey2], - cosigner_id=2, db_uri=self.database_uri) + cosigner_id=2, db_uri=self.DATABASE_URI) w2.new_key() w2.new_key() self.assertEqual(w2.keys()[0].address, '39b2tosg9To6cQTrqnZLhuhW5auqCqXKsH') @@ -1393,7 +1431,7 @@ def test_wallets_multisig_with_single_key_cosigner(self): # Close wallet and reopen to test for database issues for example del w w = wallet_create_or_open('test_wallets_multisig_with_single_key_cosigner0', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) w.new_key(cosigner_id=2) self.assertEqual(w.keys()[3].address, '3Q9rnDniMa55jZFyzBDKihtXwZSM34zfEj') @@ -1409,7 +1447,7 @@ def test_wallets_multisig_huge(self): key_list_wallet = [key_list[pk_n]] + key_list_cosigners w = Wallet.create(wallet_name, keys=key_list_wallet, sigs_required=sigs_req, - witness_type=witness_type, network=network, db_uri=self.database_uri) + witness_type=witness_type, network=network, db_uri=self.DATABASE_URI) w.get_keys(number_of_keys=2) w.utxos_update() to_address = HDKey(network=network, witness_type=witness_type).address() @@ -1434,7 +1472,7 @@ def test_wallets_multisig_huge_error(self): self.assertRaisesRegex(WalletError, 'Redeemscripts with more then 15 keys are non-standard and could ' 'result in locked up funds', Wallet.create, wallet_name, keys=key_list_wallet, sigs_required=sigs_req, - network=network, db_uri=self.database_uri) + network=network, db_uri=self.DATABASE_URI) def test_wallet_multisig_replace_sig_bug(self): network = 'bitcoinlib_test' @@ -1449,11 +1487,11 @@ def test_wallet_multisig_replace_sig_bug(self): '2631ab1a4745f657f7216c636fb8ac708a3f6b63a6cd5cf773bfc9a3ebe6e1ba', '97a66126f42fd3241cf256846e58cd7049d4d395f84b1811f73a3f5d33ff833e', ] - key_list = [HDKey(pk, witness_type='legacy', network=network) for pk in pk_hex_list] + key_list = [HDKey(pk, network=network) for pk in pk_hex_list] key_list_cosigners = [k.public_master(multisig=True) for k in key_list if k is not key_list[0]] key_list_wallet = [key_list[0]] + key_list_cosigners w = wallet_create_or_open(wallet_name, keys=key_list_wallet, sigs_required=sigs_req, witness_type=witness_type, - network=network, db_uri=self.database_uri) + network=network, db_uri=self.DATABASE_URI) w.get_keys() w.utxos_update() to_address = HDKey(network=network, witness_type=witness_type).address() @@ -1466,30 +1504,12 @@ def test_wallet_multisig_replace_sig_bug(self): key_pool.remove(co_id) self.assertTrue(t.verify()) - def test_wallet_multisig_multinetwork(self): - network1 = 'litecoin' - network2 = 'bitcoinlib_test' - p1 = 'only sing document speed outer gauge stand govern way column material odor' - p2 = 'oyster pelican debate mass scene title pipe lock recipe flavor razor accident' - k2 = HDKey.from_passphrase(p2, network=network1, multisig=True).public_master() - w = wallet_create_or_open('ltcswms', [p1, k2], network=network1, witness_type='segwit', - cosigner_id=0, db_uri=self.database_uri) - self.assertEqual(len(w.get_keys(number_of_keys=2)), 2) - w.utxo_add('ltc1qkewaz7lxn75y6wppvqlsfhrnq5p5mksmlp26n8xsef0556cdfzqq2uhdrt', 2100000000000001, - '21da13be453624cf46b3d883f39602ce74d04efa7a186037898b6d7bcfd405ee', 0, 15) - t = w.sweep('ltc1q9h8xvtrge5ttcwzy3xtz7l8kj4dewgh6hgqfjdhtq6lwr4k3527qd8tyzs') - self.assertFalse(t.verified) - t.sign(p2) - self.assertTrue(t.verified) - self.assertRaisesRegex(WalletError, "Cannot create new keys for network bitcoinlib_test, " - "no private masterkey found", w.new_key, network=network2) - - -class TestWalletKeyImport(unittest.TestCase): +@parameterized_class(*params) +class TestWalletKeyImport(TestWalletMixin, unittest.TestCase): @classmethod def setUpClass(cls): - cls.database_uri = database_init() + cls.db_remove() def test_wallet_key_import_and_sign_multisig(self): network = 'bitcoinlib_test' @@ -1504,7 +1524,7 @@ def test_wallet_key_import_and_sign_multisig(self): hdkey.public() ] with Wallet.create('Multisig-2-of-3-example', key_list, sigs_required=2, - db_uri=self.database_uri) as wlt: + db_uri=self.DATABASE_URI) as wlt: wlt.new_key() wlt.utxos_update() wt = wlt.send_to('21A6yyUPRL9hZZo1Rw4qP5G6h9idVVLUncE', 10000000, offline=False) @@ -1516,7 +1536,7 @@ def test_wallet_import_private_for_known_public(self): hdkey = HDKey( 'xprv9s21ZrQH143K2noEZoqGHnaDDLjrnFpis8jm7NWDhkWuNNCqMupGSy7PMYtGL9jvdTY7Nx3GZ6UZ9C52nebwbYXK73imaPUK24' 'dZJtGZhGd') - with Wallet.create('public-private', hdkey.public_master().public(), db_uri=self.database_uri) \ + with Wallet.create('public-private', hdkey.public_master().public(), db_uri=self.DATABASE_URI) \ as wlt: self.assertFalse(wlt.main_key.is_private) wlt.import_key(hdkey) @@ -1537,19 +1557,19 @@ def test_wallet_import_private_for_known_public_multisig(self): "iFEwaUv85GA" with wallet_create_or_open("mstest", [puk1, puk2, puk3], 2, network='litecoin', sort_keys=False, - cosigner_id=0, db_uri=self.database_uri) as wlt: + cosigner_id=0, db_uri=self.DATABASE_URI) as wlt: self.assertFalse(wlt.cosigner[2].main_key.is_private) wlt.import_key(prk3) self.assertTrue(wlt.cosigner[2].main_key.is_private) def test_wallet_import_private_for_known_public_p2sh_segwit(self): - pk1 = HDKey('BC15gwSkKnLsWk3GmCxBwzdbij4fXUa5j6UB8bSYJt9a81aSUCVjg7tVJaEDpxRZ4X2dt3VKjuy8po1fbo6opZ6tCqxVAg' - 'XgQKUBwWi6EGh2eLRC') - pk2 = HDKey('BC15gwSkKnLsWk3GmCWRgmp2edaya3UgmQ4TqjiBfx2cuvMC5ASQwJ5N5wwKcMp627AucznuYvTzKnhYRERcPFnEAn1a7w' - 'VKQy7FMLXzMq7N2nQq') + pk1 = HDKey('YXscyqNJ5YK411nwB3VjLYgjht3dqfKxyLdGSqNMGKhYdcK4Gh1CRSJyxS2So8KXSQrxtysS1jAmHtLnxRKa47xEiAx6hP' + 'vrj8tuEzyeR8TQNu5e') + pk2 = HDKey('YXscyqNJ5YK411nwB4Jo3JCQ1GZNetf4BrLJjZiqdWKVzoXwPtyJ5xyNdZjuEWtqCeSZGtmg7SuQerERwniHLYL3aVcnyS' + 'ciEAxk7gLgDkoZC5Lq') w = Wallet.create('segwit-p2sh-p2wsh-import', [pk1, pk2.public_master(witness_type='p2sh-segwit', multisig=True)], - witness_type='p2sh-segwit', network='bitcoinlib_test', db_uri=self.database_uri) + witness_type='p2sh-segwit', network='bitcoinlib_test', db_uri=self.DATABASE_URI) w.get_key() w.utxos_update() t = w.sweep('23CvEnQKsTVGgqCZzW6ewXPSJH9msFPsBt3', offline=False) @@ -1581,7 +1601,7 @@ def test_wallet_import_private_for_known_public_segwit_passphrases(self): pubk2 = k2.public_master_multisig(witness_type=witness_type) self.assertEqual(pubk1.wif(), wif1) self.assertEqual(pubk2.wif(), wif2) - w = Wallet.create('mswlt', [p1, pubk2], db_uri=self.database_uri, witness_type=witness_type) + w = Wallet.create('mswlt', [p1, pubk2], db_uri=self.DATABASE_URI, witness_type=witness_type) wk = w.new_key() self.assertEqual(wk.address, 'bc1qr7r7zpr5gqnz0zs39ve7c0g54gwe7h7322lt3kae6gh8tzc5epts0j9rhm') self.assertFalse(w.public_master(as_private=True)[1].is_private) @@ -1598,16 +1618,17 @@ def test_wallet_import_private_for_known_public_segwit_passphrases(self): self.assertListEqual(txids, txids_expected) -class TestWalletTransactions(unittest.TestCase, CustomAssertions): +@parameterized_class(*params) +class TestWalletTransactions(TestWalletMixin, unittest.TestCase, CustomAssertions): wallet = None @classmethod def setUpClass(cls): - cls.database_uri = database_init() + cls.db_remove() account_key = 'tpubDCmJWqxWch7LYDhSuE1jEJMbAkbkDm3DotWKZ69oZfNMzuw7U5DwEaTVZHGPzt5j9BJDoxqVkPHt2EpUF66FrZhpfq' \ 'ZY6DFj6x61Wwbrg8Q' cls.wallet = wallet_create_or_open('utxo-test', keys=account_key, network='testnet', - db_uri=cls.database_uri) + db_uri=cls.DATABASE_URI) cls.wallet.new_key() cls.wallet.utxos_update() @@ -1644,7 +1665,7 @@ def test_wallet_offline_create_transaction(self): 'qjcDhTcyVLhrXXZ' hdkey = HDKey(hdkey_wif) wlt = wallet_create_or_open('offline-create-transaction', keys=hdkey, network='testnet', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) self.assertEqual(wlt.wif(is_private=True), hdkey_wif) wlt.get_key() utxos = [{ @@ -1664,8 +1685,9 @@ def test_wallet_offline_create_transaction(self): def test_wallet_scan(self): account_key = 'tpubDCmJWqxWch7LYDhSuE1jEJMbAkbkDm3DotWKZ69oZfNMzuw7U5DwEaTVZHGPzt5j9BJDoxqVkPHt2EpUF66FrZhpfq' \ 'ZY6DFj6x61Wwbrg8Q' - wallet = wallet_create_or_open('scan-test', keys=account_key, network='testnet', db_uri=self.database_uri) + wallet = wallet_create_or_open('scan-test', keys=account_key, network='testnet', db_uri=self.DATABASE_URI) wallet.scan(scan_gap_limit=8) + wallet.info() self.assertEqual(len(wallet.keys()), 27) self.assertEqual(wallet.balance(), 60500000) self.assertEqual(len(wallet.transactions()), 4) @@ -1673,13 +1695,13 @@ def test_wallet_scan(self): # Check tx order in same block address = 'tb1qlh9x3jwhfqspp7u9w6l7zqxpmuvplzaczaele3' - w = wallet_create_or_open('fix-multiple-tx-1-block', keys=address, db_uri=self.database_uri) + w = wallet_create_or_open('fix-multiple-tx-1-block', keys=address, db_uri=self.DATABASE_URI) w.scan() self.assertEqual(w.transactions()[0].txid, 'bae05e65c13a1b1635abf581a6250a458cbd672c914e2563b5bb175274f9c5a7') def test_wallet_scan_utxos(self): pk = 'tpubDDi7dF92m7UrWNuAmzR9mzETcCjFT9v6XZq2oXjvhH4Bzr4L13np7d6bBB5tZk1Kg3y2vB79ohpgsLiubcRA8RfA6L69nmZvSG26XfmC5Ao' - w = wallet_create_or_open('kladkladklieder3', keys=pk, db_uri=self.database_uri) + w = wallet_create_or_open('kladkladklieder3', keys=pk, db_uri=self.DATABASE_URI) w.scan(scan_gap_limit=1) self.assertEqual(len(w.utxos()), 2) self.assertEqual(w.balance(), 2000) @@ -1692,7 +1714,7 @@ def test_wallet_scan_utxos(self): self.assertListEqual(tx_list, exp_tx_list) def test_wallet_two_utxos_one_key(self): - wlt = Wallet.create('double-utxo-test', network='bitcoinlib_test', db_uri=self.database_uri) + wlt = Wallet.create('double-utxo-test', network='bitcoinlib_test', db_uri=self.DATABASE_URI) key = wlt.new_key() wlt.utxos_update() utxos = wlt.utxos() @@ -1709,7 +1731,7 @@ def test_wallet_two_utxos_one_key(self): del wlt def test_wallet_balance_update(self): - wlt = Wallet.create('test-balance-update', network='bitcoinlib_test', db_uri=self.database_uri) + wlt = Wallet.create('test-balance-update', network='bitcoinlib_test', db_uri=self.DATABASE_URI) to_key = wlt.get_key() wlt.utxos_update() self.assertEqual(wlt.balance(), 200000000) @@ -1724,7 +1746,7 @@ def test_wallet_balance_update_multi_network(self): k = "tpubDCutwJADa3iSbFtB2LgnaaqJgZ8FPXRRzcrMq7Tms41QNnTV291rpkps9vRwyss9zgDc7hS5V1aM1by8nFip5VjpGpz1oP54peKt" \ "hJzfabX" wlt = Wallet.create("test_wallet_balance_update_multi_network", network='bitcoinlib_test', - witness_type='legacy', db_uri=self.database_uri) + db_uri=self.DATABASE_URI) wlt.new_key() wlt.new_account(network='testnet') wlt.import_key(k) @@ -1738,14 +1760,13 @@ def test_wallet_balance_update_total(self): k = "tpubDCutwJADa3iSbFtB2LgnaaqJgZ8FPXRRzcrMq7Tms41QNnTV291rpkps9vRwyss9zgDc7hS5V1aM1by8nFip5VjpGpz1oP54peKt" \ "hJzfabX" wlt = Wallet.create("test_wallet_balance_update_total", keys=k, network='testnet', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) wlt.get_key() self.assertEqual(wlt.balance_update_from_serviceprovider(), 900) def test_wallet_add_dust_to_fee(self): # Send bitcoinlib test transaction and check if dust resume amount is added to fee - wlt = Wallet.create('bcltestwlt', network='bitcoinlib_test', witness_type='legacy', - db_uri=self.database_uri) + wlt = Wallet.create('bcltestwlt', network='bitcoinlib_test', db_uri=self.DATABASE_URI) to_key = wlt.get_key() wlt.utxos_update() t = wlt.send_to(to_key.address, 99992000, offline=False) @@ -1754,7 +1775,7 @@ def test_wallet_add_dust_to_fee(self): def test_wallet_transactions_send_update_utxos(self): # Send bitcoinlib test transaction and check if all utxo's are updated after send - wlt = Wallet.create('bcltestwlt2', network='bitcoinlib_test', db_uri=self.database_uri) + wlt = Wallet.create('bcltestwlt2', network='bitcoinlib_test', db_uri=self.DATABASE_URI) to_keys = wlt.get_keys(number_of_keys=5) wlt.utxos_update() self.assertEqual(wlt.balance(), 1000000000) @@ -1765,7 +1786,7 @@ def test_wallet_transactions_send_update_utxos(self): del wlt def test_wallet_transaction_import(self): - wlt = Wallet.create('bcltestwlt3', network='bitcoinlib_test', db_uri=self.database_uri) + wlt = Wallet.create('bcltestwlt3', network='bitcoinlib_test', db_uri=self.DATABASE_URI) to_key = wlt.get_key() wlt.utxos_update() t = wlt.send_to(to_key.address, 40000000) @@ -1774,7 +1795,7 @@ def test_wallet_transaction_import(self): del wlt def test_wallet_transaction_import_raw(self): - wlt = Wallet.create('bcltestwlt4', network='bitcoinlib_test', db_uri=self.database_uri) + wlt = Wallet.create('bcltestwlt4', network='bitcoinlib_test', db_uri=self.DATABASE_URI) to_key = wlt.get_key() wlt.utxos_update() t = wlt.send_to(to_key.address, 50000000) @@ -1783,7 +1804,7 @@ def test_wallet_transaction_import_raw(self): del wlt def test_wallet_transaction_import_raw_locktime(self): - wlt = Wallet.create('bcltestwlt4b', network='bitcoinlib_test', db_uri=self.database_uri) + wlt = Wallet.create('bcltestwlt4b', network='bitcoinlib_test', db_uri=self.DATABASE_URI) to_key = wlt.get_key() wlt.utxos_update() t = wlt.send_to(to_key.address, 12312837) @@ -1793,11 +1814,11 @@ def test_wallet_transaction_import_raw_locktime(self): del wlt def test_wallet_transaction_import_raw_segwit_fee(self): - wallet_delete_if_exists('bcltestwlt-size', force=True, db_uri=self.database_uri) - pk = ('BC19UtECk2r9PVQYhZT9iJZvzK7jDgQXFQxRiguB28ESn53b8BjZjT4ZyQEStPD9yKAXBhTq6Wtb9zyPQiRU4chaTjEwvtpKW' - 'EdrMscH3ZqPTtdV') + wallet_delete_if_exists('bcltestwlt-size', force=True, db_uri=self.DATABASE_URI) + pk = 'YXscyqNJ5YK411nwB2peYdMeJPmkJmMJCfNdo9JuWkEKLZhVSoUjbRRinVqqtBN2GNC2A6L1Taz1e3LWApHkC84GgTp3vr7neD' \ + 'ZTxXnvGkUwVz4c' wlt = Wallet.create('bcltestwlt-size', keys=pk, network='bitcoinlib_test', witness_type='segwit', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) wlt.utxos_update() t = wlt.send_to(wlt.get_key().address, 8237234) t2 = wlt.transaction_import_raw(t.raw()) @@ -1809,10 +1830,10 @@ def test_wallet_transaction_import_raw_segwit_fee(self): del wlt def test_wallet_transaction_load_segwit_size(self): - pk = ('BC19UtECk2r9PVQYhZT9iJZvzK7jDgQXFQxRiguB28ESn53b8BjZjT4ZyQEStPD9yKAXBhTq6Wtb9zyPQiRU4chaTjEwvtpKW' - 'EdrMscH3ZqPTtdV') + pk = 'YXscyqNJ5YK411nwB2peYdMeJPmkJmMJCfNdo9JuWkEKLZhVSoUjbRRinVqqtBN2GNC2A6L1Taz1e3LWApHkC84GgTp3vr7neD' \ + 'ZTxXnvGkUwVz4c' wlt = Wallet.create('bcltestwlt2-size', keys=pk, network='bitcoinlib_test', witness_type='segwit', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) wlt.utxos_update() t = wlt.send_to(wlt.get_key().address, 50000000, offline=False) t.verify() @@ -1825,7 +1846,7 @@ def test_wallet_transaction_load_segwit_size(self): self.assertEqual(t.witness_data(), t2.witness_data()) def test_wallet_transaction_import_dict(self): - wlt = Wallet.create('bcltestwlt5', network='bitcoinlib_test', db_uri=self.database_uri) + wlt = Wallet.create('bcltestwlt5', network='bitcoinlib_test', db_uri=self.DATABASE_URI) to_key = wlt.get_key() wlt.utxos_update() t = wlt.send_to(to_key.address, 60000000) @@ -1834,8 +1855,7 @@ def test_wallet_transaction_import_dict(self): del wlt def test_wallet_transaction_fee_limits(self): - wlt = Wallet.create('bcltestwlt6', network='bitcoinlib_test', witness_type='legacy', - db_uri=self.database_uri) + wlt = Wallet.create('bcltestwlt6', network='bitcoinlib_test', db_uri=self.DATABASE_URI) to_key = wlt.get_key() wlt.utxos_update() self.assertRaisesRegex(WalletError, 'Fee per kB of 660 is lower then minimal network fee of 1000', @@ -1844,7 +1864,7 @@ def test_wallet_transaction_fee_limits(self): wlt.send_to, to_key.address, 50000000, fee=300000) def test_wallet_transaction_fee_zero_problem(self): - wlt = Wallet.create(name='bcltestwlt7', network='bitcoinlib_test', db_uri=self.database_uri) + wlt = Wallet.create(name='bcltestwlt7', network='bitcoinlib_test', db_uri=self.DATABASE_URI) nk = wlt.get_key() wlt.utxos_update() t = wlt.send_to(nk.address, 100000000, offline=False) @@ -1855,9 +1875,9 @@ def test_wallet_transactions_estimate_size(self): prev_txid = '46fcfdbdc3573756916a0ced8bbc5418063abccd2c272f17bf266f77549b62d5' for witness_type in ['legacy', 'p2sh-segwit', 'segwit']: - wallet_delete_if_exists('wallet_estimate_size', force=True, db_uri=self.database_uri) + wallet_delete_if_exists('wallet_estimate_size', force=True, db_uri=self.DATABASE_URI) wl3 = Wallet.create('wallet_estimate_size', witness_type=witness_type, - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) wl3.utxo_add(wl3.get_key().address, 110000, prev_txid, 0, 1) to_address = wl3.get_key_change().address t = wl3.transaction_create([(to_address, 90000)], fee=10000, random_output_order=False) @@ -1874,9 +1894,9 @@ def test_wallet_transactions_estimate_size(self): p2 = HDKey(witness_type=witness_type, multisig=True) p3 = HDKey(witness_type=witness_type, multisig=True) - wallet_delete_if_exists('wallet_estimate_size_multisig', force=True, db_uri=self.database_uri) + wallet_delete_if_exists('wallet_estimate_size_multisig', force=True, db_uri=self.DATABASE_URI) wl3 = Wallet.create('wallet_estimate_size_multisig', [p1, p2.public_master(), p3.public_master()], - sigs_required=2, db_uri=self.database_uri) + sigs_required=2, db_uri=self.DATABASE_URI) wl3.utxo_add(wl3.get_key().address, 110000, prev_txid, 0, 1) to_address = wl3.get_key_change().address t = wl3.transaction_create([(to_address, 90000)], fee=10000, random_output_order=False) @@ -1892,7 +1912,7 @@ def test_wallet_transaction_method(self): pk1 = HDKey(network='bitcoinlib_test') pk2 = HDKey(network='bitcoinlib_test') w = Wallet.create('wallet_transaction_tests', keys=[pk1, pk2], cosigner_id=0, - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) w.get_key() w.utxos_update() self.assertEqual(len(w.transactions()), 2) @@ -1901,7 +1921,7 @@ def test_wallet_transaction_method(self): def test_wallet_transaction_from_txid(self): w = Wallet.create('testwltbcl', keys='dda84e87df25f32d73a7f7d008ed2b89fc00d9d07fde588d1b8af0af297023de', - witness_type='legacy', network='bitcoinlib_test', db_uri=self.database_uri) + network='bitcoinlib_test', db_uri=self.DATABASE_URI) w.utxos_update() wts = w.transactions() txid = wts[0].txid @@ -1917,9 +1937,9 @@ def test_wallet_transaction_from_txid(self): self.assertIsNone(WalletTransaction.from_txid(w, '112233')) def test_wallet_transaction_sign_with_hex(self): - k = HDKey(network='bitcoinlib_test', witness_type='legacy') + k = HDKey(network='bitcoinlib_test') pmk = k.public_master() - w = Wallet.create('wallet_tx_tests', keys=pmk, network='bitcoinlib_test', db_uri=self.database_uri) + w = Wallet.create('wallet_tx_tests', keys=pmk, network='bitcoinlib_test', db_uri=self.DATABASE_URI) w.utxos_update() wt = w.transaction_create([(w.get_key(), 190000000)]) sk = k.subkey_for_path("m/44'/9999999'/0'/0/0") @@ -1927,14 +1947,14 @@ def test_wallet_transaction_sign_with_hex(self): self.assertTrue(wt.verified) def test_wallet_transaction_sign_with_wif(self): - wif = ('BC19UtECk2r9PVQYhZuLSVjB6M7QPkAQSJN59RJKZQuuuPxaxNBEwmnfpWYvrQTrJZCANKoXBm7HKY78dVHjTkqoqA67aUf' - 'NSLZjuwNGDBMQD7uM') - wif2 = ('BC19UtECk2r9PVQYhYJrXwB3We4E9Xc6uJngAEoqBrntN1gpGZwAWKRdcupdf2iKFLfY3pYRxHAi99EZ7dyYcKLZ2a7999' - 'Lu2NRSZzToFXib5kcE') + wif = 'YXscyqNJ5YK411nwB4eU6PmyGTJkBUHjgXEf53z4TTjHCDXPPXKJD2PyfXonwtT7VwSdqcZJS2oeDbvg531tEsx3yq4425Mfrb9aS' \ + 'PyNQ5bUGFwu' + wif2 = 'YXscyqNJ5YK411nwB4UK8ScMahPWewyKrTBjgM5BZKRkPg8B2HmKT3r8yc2GFg9GqgFXaWmxkTRhNkRGVxbzUREMH8L5HxoKGCY8' \ + 'WDdf1GcW2k8q' w = wallet_create_or_open('test_wallet_transaction_sign_with_wif', keys=[wif, HDKey(wif2).public_master_multisig(witness_type='segwit')], witness_type='segwit', network='bitcoinlib_test', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) w.get_key() w.utxos_update() t = w.send_to('blt1q285vnphcs4r0t5dw06tmxl7aryj3jnx88duehv4p7eldsshrmygsmlq84z', 2000, fee=1000, offline=False) @@ -1944,7 +1964,7 @@ def test_wallet_transaction_sign_with_wif(self): def test_wallet_transaction_restore_saved_tx(self): w = wallet_create_or_open('test_wallet_transaction_restore', network='bitcoinlib_test', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) wk = w.get_key() if not USE_FASTECDSA: self.skipTest("Need fastecdsa module with deterministic txid's to run this test") @@ -1960,9 +1980,9 @@ def test_wallet_transaction_restore_saved_tx(self): to = w.get_key_change() t = w.sweep(to.address) tx_id = t.store() - wallet_empty('test_wallet_transaction_restore', db_uri=self.database_uri) + wallet_empty('test_wallet_transaction_restore', db_uri=self.DATABASE_URI) w = wallet_create_or_open('test_wallet_transaction_restore', network='bitcoinlib_test', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) w.get_key() w.utxos_update(utxos=utxos) to = w.get_key_change() @@ -1971,7 +1991,7 @@ def test_wallet_transaction_restore_saved_tx(self): def test_wallet_transaction_send_keyid(self): w = Wallet.create('wallet_send_key_id', witness_type='segwit', network='bitcoinlib_test', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) keys = w.get_keys(number_of_keys=2) w.utxos_update() t = w.send_to('blt1qtk5swtntg8gvtsyr3kkx3mjcs5ncav84exjvde', 150000000, input_key_id=keys[1].key_id, @@ -1984,7 +2004,7 @@ def test_wallet_transaction_send_keyid(self): def test_wallet_transactions_limit(self): address = '15yN7NPEpu82sHhB6TzCW5z5aXoamiKeGy' - w = wallet_create_or_open('ftrtxtstwlt', address, db_uri=self.database_uri) + w = wallet_create_or_open('ftrtxtstwlt', address, db_uri=self.DATABASE_URI) w.transactions_update(limit=2) self.assertGreaterEqual(w.balance(), 1000) self.assertGreaterEqual(len(w.transactions()), 2) @@ -1993,7 +2013,7 @@ def test_wallet_transactions_limit(self): def test_wallet_transactions_update_by_txids(self): address = '15yN7NPEpu82sHhB6TzCW5z5aXoamiKeGy' - w = wallet_create_or_open('ftrtxtstwlt2', address, db_uri=self.database_uri) + w = wallet_create_or_open('ftrtxtstwlt2', address, db_uri=self.DATABASE_URI) w.transactions_update_by_txids(['b246dad8980093fa55814b4739396fd6a7f5f28994f721f55d1a862d6c98e7ab']) txs = w.transactions() self.assertEqual(len(txs), 1) @@ -2003,7 +2023,7 @@ def test_wallet_transactions_sign_with_mnenomic(self): phr = 'battle alter frequent adult tuna follow always jar obtain until ice arrange' prv_key = HDKey.from_passphrase(phr, network='bitcoinlib_test') w = wallet_create_or_open('wlt-sign-with-mnemonic', keys=prv_key.public(), network='bitcoinlib_test', - scheme='single', db_uri=self.database_uri) + scheme='single', db_uri=self.DATABASE_URI) w.new_key() w.utxos_update() t = w.send_to('214bP4ZpdejMppADEnxSkzziPgEG6XGcxiJ', 100000) @@ -2013,16 +2033,16 @@ def test_wallet_transactions_sign_with_mnenomic(self): self.assertTrue(t.verified) def test_wallet_select_inputs(self): - wlt = Wallet.create('bclwltsltinputs', network='bitcoinlib_test', db_uri=self.database_uri) + wlt = Wallet.create('bclwltsltinputs', network='bitcoinlib_test', db_uri=self.DATABASE_URI) wlt.get_key() wlt.utxos_update() self.assertEqual(wlt.select_inputs(150000000, max_utxos=1), []) self.assertEqual(len(wlt.select_inputs(150000000)), 2) def test_wallet_transaction_create_exceptions(self): - wif = ('BC17qWy2RMw8AmwsqwTjmX2SwwSHBNA2c6KyGHs5Kghg3q6dPa4ajP1jwFBPCkoSeXWsPAiVD2iAcroVc6cJQmHrYatvi' - 'NCk5jDM83DkbPGFxbCK') - wlt = Wallet.create('test_wallet_transaction_create_exceptions', keys=wif, db_uri=self.database_uri) + wif = 'YXscyqNJ5YK411nwB3kahnuWqF2KUJfJNRGG1n3zLPi3MSPCRcD2cmcVz1UKBjTMuMxAfXrbSAe5qJfu5nnSuRZKEtqJt' \ + 'FwNjcNknbseoKp1vR2h' + wlt = Wallet.create('test_wallet_transaction_create_exceptions', keys=wif, db_uri=self.DATABASE_URI) wlt.utxos_update() self.assertRaisesRegex(WalletError, "Output array must be a list of tuples with address and amount. " "Use 'send_to' method to send to one address", @@ -2031,13 +2051,13 @@ def test_wallet_transaction_create_exceptions(self): self.assertRaisesRegex(WalletError, "Input array contains 2 UTXO's but max_utxos=1 parameter specified", wlt.transaction_create, [('217rBycQpv9rjhiLcg94vdZ7muMVLJ9yysJ', 150000000)], inps, max_utxos=1) - wallet_empty('test_wallet_transaction_create_exceptions', db_uri=self.database_uri) + wallet_empty('test_wallet_transaction_create_exceptions', db_uri=self.DATABASE_URI) self.assertRaisesRegex(WalletError, "UTXO", wlt.transaction_create, [('217rBycQpv9rjhiLcg94vdZ7muMVLJ9yysJ', 150000000)], inps) def test_wallet_transactions_sweep(self): - w = wallet_create_or_open('test_wallet_sweep_check_fee', db_uri=self.database_uri) + w = wallet_create_or_open('test_wallet_sweep_check_fee', db_uri=self.DATABASE_URI) w.utxo_add(w.new_key().address, 5000, 'f31446151f06522eb321d5992f4f1c95123c8b9d082b92c391df83c6d0a35516', 0, 1) t = w.sweep('14pThTJoEnQxbJJVYLhzSKcs6EmZgShscX', fee=2000) @@ -2048,7 +2068,7 @@ def test_wallet_transactions_sweep(self): w.sweep, to_address='14pThTJoEnQxbJJVYLhzSKcs6EmZgShscX', fee=6000) def test_wallet_sweep_multiple_inputs_or_outputs(self): - w = wallet_create_or_open('test_wallet_sweep_multiple_outputs', db_uri=self.database_uri) + w = wallet_create_or_open('test_wallet_sweep_multiple_outputs', db_uri=self.DATABASE_URI) utxos = [ { @@ -2099,7 +2119,7 @@ def test_wallet_sweep_multiple_inputs_or_outputs(self): self.assertTrue(t.verified) def test_wallet_send_multiple_change_outputs(self): - w = wallet_create_or_open('test_wallet_send_multiple_change_outputs', db_uri=self.database_uri) + w = wallet_create_or_open('test_wallet_send_multiple_change_outputs', db_uri=self.DATABASE_URI) utxos = [ { @@ -2146,7 +2166,7 @@ def test_wallet_send_multiple_change_outputs(self): def test_wallet_transaction_save_and_load(self): w = wallet_create_or_open('wallet_transaction_save_and_load', network='bitcoinlib_test', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) w.utxos_update() t = w.send_to(w.get_key(), 50000001) self.assertTrue(t.verify()) @@ -2158,7 +2178,7 @@ def test_wallet_transaction_save_and_load(self): def test_wallet_transaction_save_and_load_filename(self): w = wallet_create_or_open('wallet_transaction_save_and_load_filename', network='bitcoinlib_test', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) w.utxos_update() t = w.send_to(w.get_key(), 44000001) self.assertTrue(t.verify()) @@ -2170,7 +2190,7 @@ def test_wallet_transaction_save_and_load_filename(self): def test_wallet_avoid_forced_address_reuse(self): w = wallet_create_or_open('wallet_avoid_forced_address_reuse_test', network='bitcoinlib_test', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) k1 = w.get_key() w.utxos_update() k2 = w.new_key() @@ -2184,7 +2204,7 @@ def test_wallet_avoid_forced_address_reuse(self): self.assertEqual(len(t.inputs), 1) def test_wallet_avoid_forced_address_reuse2(self): - w = wallet_create_or_open('wallet_avoid_forced_address_reuse_test2', db_uri=self.database_uri) + w = wallet_create_or_open('wallet_avoid_forced_address_reuse_test2', db_uri=self.DATABASE_URI) utxos = [ { 'address': w.new_key().address, @@ -2208,19 +2228,19 @@ def test_wallet_avoid_forced_address_reuse2(self): fee=150) def test_wallet_transactions_delete(self): - w = wallet_create_or_open('wallet_transactions_delete', network='bitcoinlib_test', db_uri=self.database_uri) + w = wallet_create_or_open('wallet_transactions_delete', network='bitcoinlib_test', db_uri=self.DATABASE_URI) w.utxos_update() w.transactions()[0].delete() self.assertEqual(len(w.transactions()), 1) def test_wallet_create_import_key(self): w = wallet_create_or_open("test_wallet_create_import_key_private", network='bitcoinlib_test', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) w.utxos_update() wk = w.public_master() w2 = wallet_create_or_open('test_wallet_create_import_key_public', network='bitcoinlib_test', keys=wk, - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) w2.utxos_update() wt = w2.send_to('21HKMUVtSUETuWyDESrmCj6Vwvtuns8XG5k', 1000, fee=1000) @@ -2230,7 +2250,7 @@ def test_wallet_create_import_key(self): self.assertEqual(wt, wt2) def test_wallet_transactions_pagination(self): - w = wallet_create_or_open("bclt_wallet_pagination_test", network='bitcoinlib_test', db_uri=self.database_uri) + w = wallet_create_or_open("bclt_wallet_pagination_test", network='bitcoinlib_test', db_uri=self.DATABASE_URI) w.get_keys(number_of_keys=10) w.utxos_update() txs_all = w.transactions_full() @@ -2240,9 +2260,61 @@ def test_wallet_transactions_pagination(self): self.assertEqual(txs_all[10], txs_page[0]) self.assertEqual(txs_all[11], txs_page[1]) + +@parameterized_class(*params) +class TestWalletDash(TestWalletMixin, unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.db_remove() + + def test_wallet_create_with_passphrase_dash(self): + passphrase = "always reward element perfect chunk father margin slab pond suffer episode deposit" + wlt = Wallet.create("wallet-passphrase-litecoin", keys=passphrase, network='dash', + db_uri=self.DATABASE_URI) + keys = wlt.get_keys(number_of_keys=5) + self.assertEqual(keys[4].address, "XhxXcRvTm4yZZzbH4MYz2udkdHWEMMf9GM") + + def test_wallet_import_dash(self): + accountkey = 'xprv9yQgG6Z38AXWuhkxScDCkLzThWWZgDKHKinMHUAPTH1uihrBWQw99sWBsN2HMpzeTze1YEYb8acT1x7sHKhXX8AbT' \ + 'GNf8tdbycySUi2fRaa' + wallet = Wallet.create( + db_uri=self.DATABASE_URI, + name='test_wallet_import_dash', + keys=accountkey, + network='dash') + newkey = wallet.get_key() + self.assertEqual(wallet.main_key.wif, accountkey) + self.assertEqual(newkey.address, u'XtVa6s1rqo9BNXir1tb6KEXsj5NGogp1QB') + self.assertEqual(newkey.path, "M/0/0") + + def test_wallet_multisig_dash(self): + network = 'dash' + pk1 = HDKey(network=network) + pk2 = HDKey(network=network) + wl1 = Wallet.create('multisig_test_wallet1', [pk1, pk2.public_master(multisig=True)], sigs_required=2, + db_uri=self.DATABASE_URI) + wl2 = Wallet.create('multisig_test_wallet2', [pk1.public_master(multisig=True), pk2], sigs_required=2, + db_uri=self.DATABASE_URI) + wl1_key = wl1.new_key() + wl2_key = wl2.new_key(cosigner_id=wl1.cosigner_id) + self.assertEqual(wl1_key.address, wl2_key.address) + + def test_wallet_import_private_for_known_public_multisig_dash(self): + network = 'dash' + pk1 = HDKey(network=network) + pk2 = HDKey(network=network) + pk3 = HDKey(network=network) + with wallet_create_or_open("mstest_dash", [pk1.public_master(multisig=True), pk2.public_master(multisig=True), + pk3.public_master(multisig=True)], 2, network=network, + sort_keys=False, cosigner_id=0, db_uri=self.DATABASE_URI) as wlt: + self.assertFalse(wlt.cosigner[1].main_key.is_private) + wlt.import_key(pk2) + self.assertTrue(wlt.cosigner[1].main_key.is_private) + def test_wallet_merge_transactions(self): w = wallet_create_or_open('wallet_merge_transactions_tests', network='bitcoinlib_test', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) w.utxos_update() u = w.utxos() @@ -2254,51 +2326,18 @@ def test_wallet_merge_transactions(self): self.assertEqual(t1.output_total + t2.output_total, t.output_total) self.assertEqual(t1.fee + t2.fee, t.fee) - def test_wallet_transaction_replace_by_fee(self): - w = wallet_create_or_open('wallet_transaction_rbf', network='bitcoinlib_test', - db_uri=self.database_uri) - w.utxos_update() - address = w.get_key() - t = w.send_to(address, 10000, fee=500, replace_by_fee=True) - self.assertTrue(t.verify()) - t2 = w.send_to(address, 10000, fee=1000, replace_by_fee=True) - self.assertTrue(t2.verify()) - self.assertTrue(t2.replace_by_fee) - self.assertEqual(t2.inputs[0].sequence, SEQUENCE_REPLACE_BY_FEE) - - def test_wallet_anti_fee_sniping(self): - w = wallet_create_or_open('antifeesnipingtestwallet', network='testnet', db_uri=self.database_uri) - w.utxo_add(w.get_key().address, 1234567, os.urandom(32).hex(), 1) - t = w.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456) - block_height = Service(network='testnet').blockcount() - # Bitaps and Bitgo return incorrect blockcount for testnet, so set delta to 5000 - self.assertAlmostEqual(t.locktime, block_height+1, delta=10000) - - w2 = wallet_create_or_open('antifeesnipingtestwallet2', network='testnet', anti_fee_sniping=True) - w2.utxo_add(w2.get_key().address, 1234567, os.urandom(32).hex(), 1) - t = w2.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456, locktime=1901070183) - self.assertEqual(t.locktime, 1901070183) - - w3 = wallet_create_or_open('antifeesnipingtestwallet3', network='testnet', anti_fee_sniping=False) - w3.utxo_add(w3.get_key().address, 1234567, os.urandom(32).hex(), 1) - t = w3.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456) - self.assertEqual(t.locktime, 0) - - @classmethod - def tearDownClass(cls): - del cls.wallet - -class TestWalletSegwit(unittest.TestCase): +@parameterized_class(*params) +class TestWalletSegwit(TestWalletMixin, unittest.TestCase): @classmethod def setUpClass(cls): - cls.database_uri = database_init() + cls.db_remove() def test_wallet_segwit_create_p2pkh(self): phrase = "review arch uniform illness hello animal device reform bicycle obscure cruise boat" wlt = wallet_create_or_open('thetestwallet-bech32', keys=phrase, network='bitcoin', witness_type='segwit', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) self.assertEqual(wlt.get_key().address, 'bc1ql22f3gqqphsejtqkqjqg9an03sjz4l6gztcm34') def test_wallet_segwit_create_pswsh(self): @@ -2307,7 +2346,7 @@ def test_wallet_segwit_create_pswsh(self): pk1 = HDKey.from_passphrase(phrase1, witness_type='segwit', multisig=True) pk2 = HDKey.from_passphrase(phrase2, witness_type='segwit', multisig=True) w = Wallet.create('multisig-segwit', [pk1, pk2.public_master()], sigs_required=1, witness_type='segwit', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) self.assertEqual(w.get_key().address, 'bc1qfjhmzzt9l6dmm0xx3tc6qrtff8dve7j7qrcyp88tllszm97r84aqxel5jk') def test_wallet_segwit_create_p2sh_p2wsh(self): @@ -2316,7 +2355,7 @@ def test_wallet_segwit_create_p2sh_p2wsh(self): pk1 = HDKey.from_passphrase(phrase1) pk2 = HDKey.from_passphrase(phrase2) w = Wallet.create('segwit-p2sh-p2wsh', [pk1, pk2.public_master(witness_type='p2sh-segwit', multisig=True)], - sigs_required=2, witness_type='p2sh-segwit', db_uri=self.database_uri) + sigs_required=2, witness_type='p2sh-segwit', db_uri=self.DATABASE_URI) nk = w.get_key() self.assertEqual(nk.address, '3JFyRjKWYFz5BMFHLZvT7EZQJ85gLFvtkT') self.assertEqual(nk.key_type, 'multisig') @@ -2325,7 +2364,7 @@ def test_wallet_segwit_create_p2sh_p2wsh(self): def test_wallet_segwit_create_p2sh_p2wpkh(self): phrase = 'fun brick apology sport museum vague once gospel walnut jump spawn hedgehog' w = wallet_create_or_open('segwit-p2sh-p2wpkh', phrase, purpose=49, witness_type='p2sh-segwit', - network='bitcoin', db_uri=self.database_uri) + network='bitcoin', db_uri=self.DATABASE_URI) k1 = w.get_key() address = '3Disr2CmERuYuuMkkfGrjRUHqDENQvtNep' @@ -2335,7 +2374,7 @@ def test_wallet_segwit_create_p2sh_p2wpkh(self): def test_wallet_segwit_p2wpkh_send(self): w = Wallet.create('segwit_p2wpkh_send', witness_type='segwit', network='bitcoinlib_test', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) w.get_key() w.utxos_update() t = w.send_to('blt1q7ywlg3lsyntsmp74jh65pnkntk3csagdwpz78k', 10000, offline=False) @@ -2348,7 +2387,7 @@ def test_wallet_segwit_p2wpkh_send(self): def test_wallet_segwit_p2wsh_send(self): w = Wallet.create('segwit_p2wsh_send', witness_type='segwit', network='bitcoinlib_test', keys=[HDKey(network='bitcoinlib_test'), HDKey(network='bitcoinlib_test')], sigs_required=2, - cosigner_id=0, db_uri=self.database_uri) + cosigner_id=0, db_uri=self.DATABASE_URI) k = w.get_key() w.utxos_update() w.utxos_update(key_id=k.key_id) # Test db updates after second request and only update single key @@ -2361,7 +2400,7 @@ def test_wallet_segwit_p2wsh_send(self): def test_wallet_segwit_p2sh_p2wpkh_send(self): w = Wallet.create('segwit_p2sh_p2wpkh_send', witness_type='p2sh-segwit', network='bitcoinlib_test', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) w.get_key() w.utxos_update() t = w.send_to('blt1q7ywlg3lsyntsmp74jh65pnkntk3csagdwpz78k', 10000, offline=False) @@ -2375,7 +2414,7 @@ def test_wallet_segwit_p2sh_p2wsh_send(self): w = Wallet.create('segwit_p2sh_p2wsh_send', witness_type='p2sh-segwit', network='bitcoinlib_test', keys=[HDKey(network='bitcoinlib_test'), HDKey(network='bitcoinlib_test'), HDKey(network='bitcoinlib_test')], sigs_required=2, cosigner_id=0, - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) w.get_key() w.utxos_update() t = w.send_to('blt1q7ywlg3lsyntsmp74jh65pnkntk3csagdwpz78k', 10000, offline=False) @@ -2389,7 +2428,7 @@ def test_wallet_segwit_uncompressed_error(self): k = HDKey(compressed=False, network='bitcoinlib_test') self.assertRaisesRegex(BKeyError, 'Uncompressed keys are non-standard', wallet_create_or_open, 'segwit_uncompressed_error', k, witness_type='segwit', network='bitcoinlib_test', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) def test_wallet_segwit_bitcoin_send(self): # Create several SegWit wallet and create transaction to send to each other. Uses utxo_add() method to create @@ -2406,7 +2445,7 @@ def test_wallet_segwit_bitcoin_send(self): ] wl1 = Wallet.create('segwit_bitcoin_p2wsh_send', key_list, sigs_required=2, witness_type='segwit', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) wl1.utxo_add(wl1.get_key().address, 10000000, prev_txid, 0, 1) to_address = wl1.get_key_change().address t = wl1.transaction_create([(to_address, 100000)], fee=10000, random_output_order=False) @@ -2417,7 +2456,7 @@ def test_wallet_segwit_bitcoin_send(self): self.assertFalse(t.error) # === Segwit P2WPKH to P2WSH === - wl2 = Wallet.create('segwit_bitcoin_p2wpkh_send', witness_type='segwit', db_uri=self.database_uri) + wl2 = Wallet.create('segwit_bitcoin_p2wpkh_send', witness_type='segwit', db_uri=self.DATABASE_URI) wl2.utxo_add(wl2.get_key().address, 200000, prev_txid, 0, 1) to_address = wl1.get_key_change().address t = wl2.transaction_create([(to_address, 100000)], fee=10000, random_output_order=False) @@ -2428,7 +2467,7 @@ def test_wallet_segwit_bitcoin_send(self): # === Segwit P2SH-P2WPKH to P2WPK === wl3 = Wallet.create('segwit_bitcoin_p2sh_p2wpkh_send', witness_type='p2sh-segwit', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) wl3.utxo_add(wl3.get_key().address, 110000, prev_txid, 0, 1) t = wl3.transaction_create([(to_address, 100000)], fee=10000, random_output_order=False) t.sign() @@ -2439,7 +2478,7 @@ def test_wallet_segwit_bitcoin_send(self): def test_wallet_segwit_litecoin_multi_accounts(self): phrase = 'rug rebuild group coin artwork degree basic humor flight away praise able' w = Wallet.create('segwit_wallet_litecoin_p2wpkh', keys=phrase, network='litecoin', - db_uri=self.database_uri, witness_type='segwit') + db_uri=self.DATABASE_URI, witness_type='segwit') self.assertEqual(w.get_key().address, "ltc1qsrzxzg39jyt8knsw5hlqmpwmuc8ejxvp9hfch8") self.assertEqual(w.get_key_change().address, "ltc1q9n6zknsw2hhq7dkyvczars8vl8zta5yusjjem5") acc2 = w.new_account() @@ -2451,14 +2490,14 @@ def test_wallet_segwit_litecoin_multi_accounts(self): phrase = 'rug rebuild group coin artwork degree basic humor flight away praise able' w = Wallet.create('segwit_wallet_litecoin_p2sh_p2wpkh', keys=phrase, network='litecoin', - db_uri=self.database_uri, witness_type='p2sh-segwit') + db_uri=self.DATABASE_URI, witness_type='p2sh-segwit') self.assertEqual(w.get_key().address, "MW1V5XPPW1YYQ5BGL5mSWEZNZSyD4XQPgh") self.assertEqual(w.get_key_change().address, "MWQoYMDTNvwZPNNypLMzkQ7JNSCtvS554j") def test_wallet_segwit_litecoin_sweep(self): phrase = 'wagon tunnel garage blast eager jaguar shop bring lake dumb chalk emerge' w = wallet_create_or_open('ltcsw', phrase, network='litecoin', witness_type='segwit', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) w.utxo_add('ltc1qu8dum66gd6dfr2cchgenf87qqxgenyme2kyhn8', 28471723, '21da13be453624cf46b3d883f39602ce74d04efa7a186037898b6d7bcfd405ee', 10, 99) t = w.sweep('MLqham8sXULvktmNMuDQdrBbHRdytVZ1QK') @@ -2467,8 +2506,8 @@ def test_wallet_segwit_litecoin_sweep(self): def test_wallet_segwit_litecoin_multisig(self): p1 = 'only sing document speed outer gauge stand govern way column material odor' p2 = 'oyster pelican debate mass scene title pipe lock recipe flavor razor accident' - w = wallet_create_or_open('ltc_segwit_ms', [p1, p2], network='litecoin', witness_type='segwit', - cosigner_id=0, db_uri=self.database_uri) + w = wallet_create_or_open('ltcswms', [p1, p2], network='litecoin', witness_type='segwit', + cosigner_id=0, db_uri=self.DATABASE_URI) w.get_keys(number_of_keys=2) w.utxo_add('ltc1qkewaz7lxn75y6wppvqlsfhrnq5p5mksmlp26n8xsef0556cdfzqq2uhdrt', 2100000000000001, '21da13be453624cf46b3d883f39602ce74d04efa7a186037898b6d7bcfd405ee', 0, 15) @@ -2481,7 +2520,7 @@ def test_wallet_segwit_multisig_multiple_inputs(self): cosigner = HDKey(network='bitcoinlib_test') w = wallet_create_or_open('test_wallet_segwit_multisig_multiple_inputs', [main_key, cosigner.public_master(witness_type='segwit', multisig=True)], - witness_type='segwit', network='bitcoinlib_test', db_uri=self.database_uri) + witness_type='segwit', network='bitcoinlib_test', db_uri=self.DATABASE_URI) w.get_keys(number_of_keys=2) w.utxos_update() @@ -2498,7 +2537,7 @@ def test_wallet_segwit_multiple_account_paths(self): "ZprvAhadJRUYsNgeBbjftwKvAhDEV1hrYBGY19wATHqnEt5jfWXxXChYP8Qfnw3w2zJZskNercma5S1fWYH7e7XwbTVPgbabvs1CfU" "zY2KQD2cB") w = Wallet.create("account-test", keys=[pk1, pk2.public_master(multisig=True)], witness_type='segwit', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) w.new_account() w.new_account() w.new_account(account_id=100) @@ -2515,9 +2554,9 @@ def test_wallet_segwit_multiple_account_paths(self): def test_wallet_segwit_multiple_networks_accounts(self): pk1 = 'surround vacant shoot aunt acoustic liar barely you expand rug industry grain' pk2 = 'defy push try brush ceiling sugar film present goat settle praise toilet' - wallet = Wallet.create(keys=[pk1, pk2], network='bitcoin', name='test_wallet_segwit_multicurrency', + wallet = Wallet.create(keys=[pk1, pk2], network='bitcoin', name='test_wallet_multicurrency', witness_type='segwit', cosigner_id=0, encoding='base58', - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) wallet.new_account(network='litecoin') wallet.new_account(network='litecoin') wallet.new_account(network='bitcoin') @@ -2531,21 +2570,19 @@ def test_wallet_segwit_multiple_networks_accounts(self): self.assertListEqual(sorted([nw.name for nw in wallet.networks()]), networks_expected) self.assertListEqual([k.path for k in wallet.keys_accounts(network='litecoin')], ["m/48'/2'/0'/2'", "m/48'/2'/1'/2'"]) - self.assertEqual(wallet.keys(network='litecoin')[0].address, - "ltc1qcu83hcdwy46dv85vmwnnfgzeu8d9arfy7kfyct52dxqcyvq2q6ds5kq2ah") - self.assertEqual(wallet.keys(network='bitcoin')[0].address, - "bc1qd0f952amxkmqc9e60u4g8w4r5a3cx22lt5vqeeyljllacxq8ezusclkwa0") + self.assertEqual(wallet.keys(network='litecoin')[0].address, "MQNA8FYrN2fvD7SSYny3Ccvpapvsu9cVJH") + self.assertEqual(wallet.keys(network='bitcoin')[0].address, "3L6XFzC6RPeXSFpZS8v4S86v4gsNmKFnFT") -class TestWalletKeyStructures(unittest.TestCase): +@parameterized_class(*params) +class TestWalletKeyStructures(TestWalletMixin, unittest.TestCase): @classmethod def setUpClass(cls): - cls.database_uri = database_init() + cls.db_remove() def test_wallet_path_expand(self): - wlt = wallet_create_or_open('wallet_path_expand', network='bitcoin', witness_type='legacy', - db_uri=self.database_uri) + wlt = wallet_create_or_open('wallet_path_expand', network='bitcoin', db_uri=self.DATABASE_URI) self.assertListEqual(wlt.path_expand([8]), ['m', "44'", "0'", "0'", '0', '8']) self.assertListEqual(wlt.path_expand(['8']), ['m', "44'", "0'", "0'", '0', '8']) self.assertListEqual(wlt.path_expand(["99'", 1, 2]), ['m', "44'", "0'", "99'", '1', '2']) @@ -2562,16 +2599,16 @@ def test_wallet_path_expand(self): wlt.path_expand, ['m', "bestaatnie'", "coin_type'", "1", 2, 3]) def test_wallet_exotic_key_paths(self): - w = Wallet.create("simple_custom_keypath", key_path="m/change/address_index", witness_type='legacy', - db_uri=self.database_uri) + w = Wallet.create("simple_custom_keypath", key_path="m/change/address_index", + db_uri=self.DATABASE_URI) self.assertEqual(w.new_key().path, "m/0/1") self.assertEqual(w.new_key_change().path, "m/1/0") self.assertEqual(w.wif()[:4], 'xpub') w = Wallet.create( "strange_key_path", keys=[HDKey(), HDKey()], purpose=100, cosigner_id=0, - key_path="m/purpose'/cosigner_index/change/address_index", witness_type='legacy', - db_uri=self.database_uri) + key_path="m/purpose'/cosigner_index/change/address_index", + db_uri=self.DATABASE_URI) self.assertEqual(w.new_key().path, "m/100'/0/0/0") self.assertEqual(w.new_key_change().path, "m/100'/0/1/0") @@ -2582,7 +2619,7 @@ def test_wallet_exotic_key_paths(self): w = Wallet.create( "long_key_path", keys=[wif1, wif2], witness_type='segwit', cosigner_id=1, key_path="m/purpose'/coin_type'/account'/script_type'/cosigner_index/change/address_index", - db_uri=self.database_uri) + db_uri=self.DATABASE_URI) self.assertEqual(w.new_key().path, "M/1/0/0") self.assertEqual(w.new_key_change().path, "M/1/1/0") self.assertEqual(w.public_master()[0].wif, wif1) @@ -2594,23 +2631,24 @@ def test_wallet_normalize_path(self): "m/44h/0p/100H//1201") def test_wallet_accounts(self): - w = Wallet.create('test_litecoin_accounts', network='litecoin', account_id=1111, db_uri=self.database_uri) + w = Wallet.create('test_litecoin_accounts', network='litecoin', account_id=1111, db_uri=self.DATABASE_URI) w.new_account(account_id=2222) w.new_account(account_id=5555, network='testnet') self.assertListEqual(w.accounts(), [1111, 2222]) self.assertListEqual(w.accounts(network='testnet'), [5555]) -class TestWalletReadonlyAddress(unittest.TestCase): +@parameterized_class(*params) +class TestWalletReadonlyAddress(TestWalletMixin, unittest.TestCase): @classmethod def setUpClass(cls): - cls.database_uri = database_init() + cls.db_remove() def test_wallet_readonly_create_and_import(self): k = '13A1W4jLPP75pzvn2qJ5KyyqG3qPSpb9jM' - w = wallet_create_or_open('addrwlt', k, db_uri=self.database_uri) - addr = Address.parse('12yuSkjKmHzXCFn39PK1XP3XyeoVw9LJdN') + w = wallet_create_or_open('addrwlt', k, db_uri=self.DATABASE_URI) + addr = Address.import_address('12yuSkjKmHzXCFn39PK1XP3XyeoVw9LJdN') w.import_key(addr) self.assertEqual(len(w.accounts()), 1) w.utxos_update() @@ -2625,7 +2663,7 @@ def test_wallet_address_import_public_key(self): wif = 'xpub661MyMwAqRbcFCwFkcko75u2VEinbG1u5U4nq8AFJq4AbLPEvwcmhZGgGcnDcEBpcfAFEP8vVhbJJvX1ieGWdoaa5AnHfyB' \ 'DAY95TfYH6H6' address = '1EJiPa66sT4PCDCFnc7oRnpWebAogPqppr' - w = Wallet.create('test_wallet_address_import_public_key', address, db_uri=self.database_uri) + w = Wallet.create('test_wallet_address_import_public_key', address, db_uri=self.DATABASE_URI) self.assertEqual(w.addresslist(), [address]) self.assertIsNone(w.main_key.key_public) w.import_key(wif) @@ -2636,7 +2674,7 @@ def test_wallet_address_import_public_key_segwit(self): address = 'bc1q84xq6lrzr09t3h2pw5ys5zee7rn3mxh5v65732' wif = 'zprvAWgYBBk7JR8Gj9CNFRBUq3DvNHDbZhH4L6AybFSTCH7DDW6boPyiQfTigAPhJma5wC4TP1o53Gz1XLh94xD3dVQUpsFDaCb2' \ '9XmDQKBwKhz' - w = Wallet.create('test_wallet_address_import_public_key_segwit', address, db_uri=self.database_uri) + w = Wallet.create('test_wallet_address_import_public_key_segwit', address, db_uri=self.DATABASE_URI) self.assertEqual(w.addresslist(), [address]) self.assertIsNone(w.main_key.wif) w.import_key(wif) @@ -2646,97 +2684,8 @@ def test_wallet_address_import_private_key(self): wif = 'xprv9s21ZrQH143K2irnebDnjwxHwCtJBoJ3iF9C2jkdkVXBiY46PQJX9kxCRMVcM1YXLERWUiUBoxQEUDqFAKbrTaL9FB4HfRY' \ 'jsGgCGpRPWTy' address = '1EJiPa66sT4PCDCFnc7oRnpWebAogPqppr' - w = Wallet.create('test_wallet_address_import_private_key', address, db_uri=self.database_uri) + w = Wallet.create('test_wallet_address_import_private_key', address, db_uri=self.DATABASE_URI) self.assertListEqual(w.addresslist(), [address]) self.assertFalse(w.main_key.is_private) w.import_key(wif) self.assertTrue(w.main_key.is_private) - - -class TestWalletMixedWitnessTypes(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.database_uri = database_init() - - def test_wallet_mixed_witness_types_address(self): - pk = 'a9b723d90282710036e06ec5b3d0c303817e486fa3e8bc117aec839deaedb961' - wseg = Wallet.create(name='wallet_witness_type_segwit', keys=pk, db_uri=self.database_uri) - wleg = Wallet.create(name='wallet_witness_type_legacy', keys=pk, witness_type='legacy', - db_uri=self.database_uri) - wp2sh = Wallet.create(name='wallet_witness_type_p2sh', keys=pk, witness_type='p2sh-segwit', - db_uri=self.database_uri) - self.assertEqual(wseg.get_key(witness_type='legacy').address, wleg.get_key().address) - self.assertEqual(wseg.get_key(witness_type='p2sh-segwit').address, wp2sh.get_key().address) - self.assertEqual(wleg.get_key(witness_type='segwit').address, wseg.get_key().address) - wmix_legkey = wseg.new_account(witness_type='legacy').address - self.assertEqual(wmix_legkey, '18nM5LxmzaEcf4rv9pK7FLiAtfmH1VgVWD') - self.assertEqual(wmix_legkey, wleg.new_account().address) - self.assertEqual(wseg.new_key(witness_type='p2sh-segwit').address, wp2sh.new_key().address) - self.assertEqual(wseg.new_key_change(witness_type='p2sh-segwit').address, wp2sh.new_key_change().address) - self.assertEqual(wleg.get_key_change(witness_type='segwit').address, - wp2sh.get_key_change(witness_type='segwit').address) - self.assertEqual(wleg.new_key_change(witness_type='p2sh-segwit', account_id=111).address, - wp2sh.new_key_change(account_id=111).address) - - def test_wallet_mixed_witness_types_masterkeys(self): - pk = '5f5b1f7d8c023c4bf5deff1eefe7ee27c126879da7e65487cf9ff64bdc3a1518' - wseg = Wallet.create(name='wallet_witness_types_masterkey_segwit', keys=pk, db_uri=self.database_uri) - wleg = Wallet.create(name='wallet_witness_types_masterkey_legacy', keys=pk, witness_type='legacy', - db_uri=self.database_uri) - wp2sh = Wallet.create(name='wallet_witness_types_masterkey_p2sh', keys=pk, witness_type='p2sh-segwit', - db_uri=self.database_uri) - self.assertEqual(wseg.public_master().wif, wleg.public_master(witness_type='segwit').wif) - self.assertEqual(wseg.public_master(witness_type='p2sh-segwit').wif, - wleg.public_master(witness_type='p2sh-segwit').wif) - self.assertEqual(wp2sh.public_master().wif, wleg.public_master(witness_type='p2sh-segwit').wif) - self.assertEqual(wleg.public_master().wif, wp2sh.public_master(witness_type='legacy').wif) - - def test_wallet_mixed_witness_types_send(self): - w = Wallet.create(name='wallet_mixed_bcltest', network='bitcoinlib_test', db_uri=self.database_uri) - seg_key = w.get_key() - leg_key = w.new_key(witness_type='legacy') - p2sh_key = w.new_key(witness_type='p2sh-segwit') - self.assertEqual(seg_key.witness_type, 'segwit') - self.assertEqual(leg_key.witness_type, 'legacy') - self.assertEqual(p2sh_key.witness_type, 'p2sh-segwit') - w.utxos_update() - t = w.sweep('blt1qgk3zp30pnpggylp84enh0zpfpkdu63kv4xak4p', fee=30000) - self.assertEqual(len(t.inputs), len(w.addresslist()) * 2) - self.assertEqual(t.outputs[0].value, int(w.balance() - 30000)) - self.assertTrue(t.verified) - t.send() - self.assertIsNone(t.error) - - def test_wallet_mixed_witness_no_private_key(self): - pub_master = ('zpub6qwhKTArtsgtCpVweSyJdVqmXTkmH3HXE2sc7RdhF5drnmcW2HXuFBqRPzVxhQkdaER3bSZeJbAbYxNGeShwUu' - 'T49JfJqZLHNAsEUHD76AR') - address = 'bc1qgf8fzfj65lcr5vae0sh77akurh4zc9s9m4uspm' - w = Wallet.create('wallet_mix_no_private', keys=pub_master, db_uri=self.database_uri) - self.assertEqual(address, w.get_key().address) - self.assertRaisesRegex(WalletError, "This wallet has no private key, cannot use multiple witness types", - w.get_key, witness_type='legacy') - - def test_wallet_mixed_witness_type_create(self): - w = Wallet.create('test_wallet_mixed_witness_type_create', network='testnet', db_uri=self.database_uri) - w.get_key(witness_type='legacy') - w.new_account('test-account', 101, witness_type='p2sh-segwit') - w.get_key(account_id=101) - kltc = w.get_key(network='litecoin') - self.assertEqual(kltc.network, 'litecoin') - self.assertListEqual(sorted(w.witness_types()), ['legacy', 'p2sh-segwit', 'segwit']) - self.assertListEqual(sorted(w.witness_types(account_id=101)), ['p2sh-segwit', 'segwit']) - self.assertListEqual(w.witness_types(network='litecoin'), ['segwit']) - - def test_wallet_mixed_witness_types_passphrase(self): - p1 = 'advance upset milk quit sword tide pumpkin unit weekend denial tobacco alien' - p2 = 'danger aspect north choose run bean short race prepare receive armed burst' - w = Wallet.create('multiwitnessmultisigtest', keys=[p1, p2], cosigner_id=0, db_uri=self.database_uri) - w.new_key() - w.new_key(network='litecoin', witness_type='p2sh-segwit') - w.new_key(witness_type='legacy') - w.new_key(witness_type='p2sh-segwit') - expected_addresslist = \ - ['39h96ozh8F8W2sVrc2EhEbFwwdRoLHJAfB', '3LdJC6MSmFqKrn2WrxRfhd8DYkYYr8FNDr', - 'MTSW4eC7xJiyp4YjwGZqpGmubsdm28Cdvc', 'bc1qgw8rg0057q9fmupx7ru6vtkxzy03gexc9ljycagj8z3hpzdfg7usvu56dp'] - self.assertListEqual(sorted(w.addresslist()), expected_addresslist) \ No newline at end of file