diff --git a/.github/workflows/unittests-mysql.yaml b/.github/workflows/unittests-mysql.yaml new file mode 100644 index 00000000..c4b1e807 --- /dev/null +++ b/.github/workflows/unittests-mysql.yaml @@ -0,0 +1,30 @@ +name: Bitcoinlib Tests Ubuntu - MySQL +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + architecture: 'x64' + - name: Set up MySQL + env: + DB_DATABASE: bitcoinlib_test + DB_USER: root + DB_PASSWORD: root + run: | + sudo /etc/init.d/mysql start + mysql -e 'CREATE DATABASE ${{ env.DB_DATABASE }};' -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }} + - name: Install dependencies + run: | + python -m pip install .[dev] + - name: Test with coverage + env: + BCL_CONFIG_FILE: ${{ github.workspace }}/tests/config.ini.unittest + UNITTEST_DATABASE: mysql + run: coverage run --source=bitcoinlib -m unittest -v diff --git a/.github/workflows/unittests-noscrypt.yaml b/.github/workflows/unittests-noscrypt.yaml index 9dae687c..2e3f6320 100644 --- a/.github/workflows/unittests-noscrypt.yaml +++ b/.github/workflows/unittests-noscrypt.yaml @@ -1,4 +1,4 @@ -name: Bitcoinlib Unittests Coveralls Ubuntu - No scrypt +name: Bitcoinlib Tests Ubuntu - No scrypt on: [push] jobs: @@ -7,9 +7,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: '3.10' architecture: 'x64' @@ -19,7 +19,6 @@ jobs: pip uninstall -y scrypt - name: Test with coverage env: - BCL_CONFIG_FILE: config_encryption.ini.unittest - UNITTESTS_FULL_DATABASE_TEST: False + BCL_CONFIG_FILE: ${{ github.workspace }}/tests/config_encryption.ini.unittest DB_FIELD_ENCRYPTION_KEY: 11223344556677889900aabbccddeeff11223344556677889900aabbccddeeff run: coverage run --source=bitcoinlib -m unittest -v diff --git a/.github/workflows/unittests-postgresql.yaml b/.github/workflows/unittests-postgresql.yaml new file mode 100644 index 00000000..2ed01b27 --- /dev/null +++ b/.github/workflows/unittests-postgresql.yaml @@ -0,0 +1,40 @@ +name: Bitcoinlib Tests Ubuntu - PostgreSQL +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres + + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + POSTGRES_DB: bitcoinlib_test + + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + architecture: 'x64' + - name: Install dependencies + run: | + python -m pip install .[dev] + - name: Test with coverage + env: + BCL_CONFIG_FILE: ${{ github.workspace }}/tests/config.ini.unittest + UNITTEST_DATABASE: postgresql + DB_FIELD_ENCRYPTION_PASSWORD: verybadpassword + run: coverage run --source=bitcoinlib -m unittest -v diff --git a/.github/workflows/unittests.yaml b/.github/workflows/unittests.yaml index 28ae94bc..a02df66a 100644 --- a/.github/workflows/unittests.yaml +++ b/.github/workflows/unittests.yaml @@ -1,4 +1,4 @@ -name: Bitcoinlib Unittests Coveralls Ubuntu +name: Bitcoinlib Tests Ubuntu on: [push] jobs: @@ -7,12 +7,12 @@ jobs: strategy: matrix: - python: ["3.8", "3.10", "3.11"] + python: ["3.8", "3.11"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} architecture: 'x64' @@ -21,8 +21,7 @@ jobs: python -m pip install .[dev] - name: Test with coverage env: - BCL_CONFIG_FILE: config.ini.unittest - UNITTESTS_FULL_DATABASE_TEST: False + BCL_CONFIG_FILE: ${{ github.workspace }}/tests/config.ini.unittest run: coverage run --source=bitcoinlib -m unittest -v - name: Coveralls diff --git a/.github/workflows/unittests_windows.yaml b/.github/workflows/unittests_windows.yaml index 560fc4e6..f3c5a2fa 100644 --- a/.github/workflows/unittests_windows.yaml +++ b/.github/workflows/unittests_windows.yaml @@ -1,4 +1,4 @@ -name: Bitcoinlib Windows Unittests +name: Bitcoinlib Tests Windows on: [push] jobs: @@ -7,9 +7,9 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: '3.11' architecture: 'x64' @@ -18,7 +18,6 @@ jobs: python -m pip install .[dev] - name: Test with coverage env: - BCL_CONFIG_FILE: config.ini.unittest - UNITTESTS_FULL_DATABASE_TEST: False + BCL_CONFIG_FILE: ${{ github.workspace }}/tests/config.ini.unittest PYTHONUTF8: 1 run: coverage run --source=bitcoinlib -m unittest -v diff --git a/.readthedocs.yml b/.readthedocs.yml index 358b81bb..f909144d 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -9,11 +9,11 @@ version: 2 formats: all build: - image: latest + os: ubuntu-22.04 + tools: + python: "3.11" # Optionally set the version of Python and requirements required to build your docs python: - version: 3.10 install: - requirements: docs/requirements.txt -# - requirements: requirements.txt diff --git a/CHANGELOG b/CHANGELOG index e591bcfa..1a066191 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,18 @@ +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 +* Add Docker files for testing and installation testing +* Fix Bitcoind constructors +* Fix issue with hexadecimal passwords for Mnemonics +* Fix regtest address prefix + RELEASE 0.6.13 - Update config & fix some bugs ============================================== * Rewrite configuration files for easier installation diff --git a/README.rst b/README.rst index ed6c00b6..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,8 +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. Documentation ------------- @@ -64,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 @@ -78,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 @@ -86,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 @@ -114,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/blocks.py b/bitcoinlib/blocks.py index bd12d2be..4384d646 100644 --- a/bitcoinlib/blocks.py +++ b/bitcoinlib/blocks.py @@ -251,11 +251,13 @@ def parse_bytesio(cls, raw, block_hash=None, height=None, parse_transactions=Fal raw.seek(tx_start_pos) transactions = [] + index = 0 while parse_transactions and raw.tell() < txs_data_size: if limit != 0 and len(transactions) >= limit: break - t = Transaction.parse_bytesio(raw, strict=False) + t = Transaction.parse_bytesio(raw, strict=False, index=index) transactions.append(t) + index += 1 # TODO: verify transactions, need input value from previous txs # if verify and not t.verify(): # raise ValueError("Could not verify transaction %s in block %s" % (t.txid, block_hash)) @@ -270,81 +272,6 @@ def parse_bytesio(cls, raw, block_hash=None, height=None, parse_transactions=Fal block.tx_count = tx_count return block - @classmethod - @deprecated - def from_raw(cls, raw, block_hash=None, height=None, parse_transactions=False, limit=0, network=DEFAULT_NETWORK): # pragma: no cover - """ - Create Block object from raw serialized block in bytes. - - Get genesis block: - - >>> from bitcoinlib.services.services import Service - >>> srv = Service() - >>> b = srv.getblock(0) - >>> b.block_hash.hex() - '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f' - - :param raw: Raw serialize block - :type raw: bytes - :param block_hash: Specify block hash if known to verify raw block. Value error will be raised if calculated block hash is different than specified. - :type block_hash: bytes - :param height: Specify height if known. Will be derived from coinbase transaction if not provided. - :type height: int - :param parse_transactions: Indicate if transactions in raw block need to be parsed and converted to Transaction objects. Default is False - :type parse_transactions: bool - :param limit: Maximum number of transactions to parse. Default is 0: parse all transactions. Only used if parse_transaction is set to True - :type limit: int - :param network: Name of network - :type network: str - - :return Block: - """ - block_hash_calc = double_sha256(raw[:80])[::-1] - if not block_hash: - block_hash = block_hash_calc - elif block_hash != block_hash_calc: - raise ValueError("Provided block hash does not correspond to calculated block hash %s" % - block_hash_calc.hex()) - - version = raw[0:4][::-1] - prev_block = raw[4:36][::-1] - merkle_root = raw[36:68][::-1] - time = raw[68:72][::-1] - bits = raw[72:76][::-1] - nonce = raw[76:80][::-1] - tx_count, size = varbyteint_to_int(raw[80:89]) - txs_data = BytesIO(raw[80+size:]) - - # Parse coinbase transaction so we can extract extra information - # transactions = [Transaction.parse(txs_data, network=network)] - # txs_data = BytesIO(txs_data[transactions[0].size:]) - # block_txs_data = txs_data.read() - txs_data_size = txs_data.seek(0, 2) - txs_data.seek(0) - transactions = [] - - while parse_transactions and txs_data and txs_data.tell() < txs_data_size: - if limit != 0 and len(transactions) >= limit: - break - t = Transaction.parse_bytesio(txs_data, strict=False) - transactions.append(t) - # t = transaction_deserialize(txs_data, network=network, check_size=False) - # transactions.append(t) - # txs_data = txs_data[t.size:] - # TODO: verify transactions, need input value from previous txs - # if verify and not t.verify(): - # raise ValueError("Could not verify transaction %s in block %s" % (t.txid, block_hash)) - - if parse_transactions and limit == 0 and tx_count != len(transactions): - raise ValueError("Number of found transactions %d is not equal to expected number %d" % - (len(transactions), tx_count)) - - block = cls(block_hash, version, prev_block, merkle_root, time, bits, nonce, transactions, height, - network=network) - block.txs_data = txs_data - block.tx_count = tx_count - return block - def parse_transactions(self, limit=0): """ Parse raw transactions from Block, if transaction data is available in txs_data attribute. Creates @@ -373,11 +300,13 @@ def parse_transactions_dict(self): """ transactions_dict = [] txs_data_orig = deepcopy(self.txs_data) + index = 0 while self.txs_data and len(self.transactions) < self.tx_count: - tx = self.parse_transaction_dict() + tx = self.parse_transaction_dict(index) if not tx: break transactions_dict.append(tx) + index += 1 self.txs_data = txs_data_orig return transactions_dict @@ -394,7 +323,7 @@ def parse_transaction(self): return t return False - def parse_transaction_dict(self): + def parse_transaction_dict(self, index=None): """ Parse a single transaction from Block, if transaction data is available in txs_data attribute. Add Transaction object in Block and return the transaction @@ -480,6 +409,7 @@ def parse_transaction_dict(self): tx['txid'] = double_sha256(tx['version'][::-1] + raw_n_inputs + inputs_raw + raw_n_outputs + outputs_raw + tx_locktime)[::-1] tx['size'] = len(tx['rawtx']) + tx['index'] = index # TODO: tx['vsize'] = len(tx['rawtx']) return tx return False diff --git a/bitcoinlib/config/VERSION b/bitcoinlib/config/VERSION index 4655c9e9..7e3c84c3 100644 --- a/bitcoinlib/config/VERSION +++ b/bitcoinlib/config/VERSION @@ -1 +1 @@ -0.6.13 \ No newline at end of file +0.6.15 \ No newline at end of file diff --git a/bitcoinlib/config/config.py b/bitcoinlib/config/config.py index 9af041d3..6de88491 100644 --- a/bitcoinlib/config/config.py +++ b/bitcoinlib/config/config.py @@ -23,6 +23,7 @@ import platform import configparser import enum +from .opcodes import * from pathlib import Path from datetime import datetime, timezone @@ -46,6 +47,7 @@ ALLOW_DATABASE_THREADS = None DATABASE_ENCRYPTION_ENABLED = False DB_FIELD_ENCRYPTION_KEY = None +DB_FIELD_ENCRYPTION_PASSWORD = None # Services TIMEOUT_REQUESTS = 5 @@ -54,33 +56,46 @@ SERVICE_MAX_ERRORS = 4 # Fail service request when more then max errors occur for providers # Transactions -SCRIPT_TYPES_LOCKING = { - # Locking scripts / scriptPubKey (Output) - 'p2pkh': ['OP_DUP', 'OP_HASH160', 'hash-20', 'OP_EQUALVERIFY', 'OP_CHECKSIG'], - 'p2sh': ['OP_HASH160', 'hash-20', 'OP_EQUAL'], - 'p2wpkh': ['OP_0', 'hash-20'], - 'p2wsh': ['OP_0', 'hash-32'], - 'p2tr': ['op_n', 'hash-32'], - 'multisig': ['op_m', 'multisig', 'op_n', 'OP_CHECKMULTISIG'], - 'p2pk': ['public_key', 'OP_CHECKSIG'], - 'nulldata': ['OP_RETURN', 'return_data'], -} - -SCRIPT_TYPES_UNLOCKING = { - # Unlocking scripts / scriptSig (Input) - 'sig_pubkey': ['signature', 'SIGHASH_ALL', 'public_key'], - 'p2sh_multisig': ['OP_0', 'multisig', 'redeemscript'], - 'p2sh_p2wpkh': ['OP_0', 'OP_HASH160', 'redeemscript', 'OP_EQUAL'], - 'p2sh_p2wsh': ['OP_0', 'push_size', 'redeemscript'], - 'locktime_cltv': ['locktime_cltv', 'OP_CHECKLOCKTIMEVERIFY', 'OP_DROP'], - 'locktime_csv': ['locktime_csv', 'OP_CHECKSEQUENCEVERIFY', 'OP_DROP'], - 'signature': ['signature'] +SCRIPT_TYPES = { + # : (, , ) + 'p2pkh': ('locking', [op.op_dup, op.op_hash160, 'data', op.op_equalverify, op.op_checksig], [20]), + 'p2pkh_drop': ('locking', ['data', op.op_drop, op.op_dup, op.op_hash160, 'data', op.op_equalverify, op.op_checksig], + [32, 20]), + 'p2sh': ('locking', [op.op_hash160, 'data', op.op_equal], [20]), + 'p2wpkh': ('locking', [op.op_0, 'data'], [20]), + 'p2wsh': ('locking', [op.op_0, 'data'], [32]), + 'p2tr': ('locking', ['op_n', 'data'], [32]), + 'multisig': ('locking', ['op_n', 'key', 'op_n', op.op_checkmultisig], []), + 'p2pk': ('locking', ['key', op.op_checksig], []), + 'locktime_cltv_script': ('locking', ['locktime_cltv', op.op_checklocktimeverify, op.op_drop, op.op_dup, + op.op_hash160, 'data', op.op_equalverify, op.op_checksig], [20]), + 'nulldata': ('locking', [op.op_return, 'data'], [0]), + 'nulldata_1': ('locking', [op.op_return, op.op_0], []), + 'nulldata_2': ('locking', [op.op_return], []), + 'sig_pubkey': ('unlocking', ['signature', 'key'], []), + # 'p2sh_multisig': ('unlocking', [op.op_0, 'signature', 'op_n', 'key', 'op_n', op.op_checkmultisig], []), + 'p2sh_multisig': ('unlocking', [op.op_0, 'signature', 'redeemscript'], []), + 'multisig_redeemscript': ('unlocking', ['op_n', 'key', 'op_n', op.op_checkmultisig], []), + 'p2tr_unlock': ('unlocking', ['data'], [64]), + 'p2sh_multisig_2?': ('unlocking', [op.op_0, 'signature', op.op_verify, 'redeemscript'], []), + 'p2sh_multisig_3?': ('unlocking', [op.op_0, 'signature', op.op_1add, 'redeemscript'], []), + # 'p2sh_p2wpkh': ('unlocking', [op.op_0, op.op_hash160, 'redeemscript', op.op_equal], []), + # 'p2sh_p2wsh': ('unlocking', [op.op_0, 'redeemscript'], []), + 'p2sh_p2wpkh': ('unlocking', [op.op_0, 'data'], [20]), + 'p2sh_p2wsh': ('unlocking', [op.op_0, 'data'], [32]), + 'signature': ('unlocking', ['signature'], []), + 'signature_multisig': ('unlocking', [op.op_0, 'signature'], []), + 'locktime_cltv': ('unlocking', ['locktime_cltv', op.op_checklocktimeverify, op.op_drop], []), + 'locktime_csv': ('unlocking', ['locktime_csv', op.op_checksequenceverify, op.op_drop], []), + # + # List of nonstandard scripts, use for blockchain parsing. Must begin with 'nonstandard' + 'nonstandard_0001': ('unlocking', [op.op_0], []), } SIGHASH_ALL = 1 SIGHASH_NONE = 2 SIGHASH_SINGLE = 3 -SIGHASH_ANYONECANPAY = 80 +SIGHASH_ANYONECANPAY = 0x80 SEQUENCE_LOCKTIME_DISABLE_FLAG = (1 << 31) # To enable sequence time locks SEQUENCE_LOCKTIME_TYPE_FLAG = (1 << 22) # If set use timestamp based lock otherwise use block height @@ -92,9 +107,22 @@ SIGNATURE_VERSION_STANDARD = 0 SIGNATURE_VERSION_SEGWIT = 1 +BUMPFEE_DEFAULT_MULTIPLIER = 5 + # Mnemonics DEFAULT_LANGUAGE = 'english' +# BIP38 +BIP38_MAGIC_LOT_AND_SEQUENCE = b'\x2c\xe9\xb3\xe1\xff\x39\xe2\x51' +BIP38_MAGIC_NO_LOT_AND_SEQUENCE = b'\x2c\xe9\xb3\xe1\xff\x39\xe2\x53' +BIP38_MAGIC_LOT_AND_SEQUENCE_UNCOMPRESSED_FLAG = b'\x04' +BIP38_MAGIC_LOT_AND_SEQUENCE_COMPRESSED_FLAG = b'\x24' +BIP38_MAGIC_NO_LOT_AND_SEQUENCE_UNCOMPRESSED_FLAG = b'\x00' +BIP38_MAGIC_NO_LOT_AND_SEQUENCE_COMPRESSED_FLAG = b'\x20' +BIP38_NO_EC_MULTIPLIED_PRIVATE_KEY_PREFIX = b'\x01\x42' +BIP38_EC_MULTIPLIED_PRIVATE_KEY_PREFIX = b'\x01\x43' +BIP38_CONFIRMATION_CODE_PREFIX = b'\x64\x3b\xf6\xa8\x9a' + # Networks DEFAULT_NETWORK = 'bitcoin' NETWORK_DENOMINATORS = { # source: https://en.bitcoin.it/wiki/Units, https://en.wikipedia.org/wiki/Metric_prefix @@ -131,9 +159,14 @@ # Keys / Addresses SUPPORTED_ADDRESS_ENCODINGS = ['base58', 'bech32'] -ENCODING_BECH32_PREFIXES = ['bc', 'tb', 'ltc', 'tltc', 'tdash', 'tdash', 'blt'] -DEFAULT_WITNESS_TYPE = 'legacy' +ENCODING_BECH32_PREFIXES = ['bc', 'tb', 'ltc', 'tltc', 'blt'] +DEFAULT_WITNESS_TYPE = 'segwit' BECH32M_CONST = 0x2bc830a3 +KEY_PATH_LEGACY = ["m", "purpose'", "coin_type'", "account'", "change", "address_index"] +KEY_PATH_P2SH = ["m", "purpose'", "cosigner_index", "change", "address_index"] +KEY_PATH_P2WSH = ["m", "purpose'", "coin_type'", "account'", "script_type'", "change", "address_index"] +KEY_PATH_P2WPKH = ["m", "purpose'", "coin_type'", "account'", "change", "address_index"] +KEY_PATH_BITCOINCORE = ['m', "account'", "change'", "address_index'"] # Wallets WALLET_KEY_STRUCTURES = [ @@ -153,7 +186,7 @@ 'multisig': False, 'encoding': 'base58', 'description': 'Legacy wallet using pay-to-public-key-hash scripts', - 'key_path': ["m", "purpose'", "coin_type'", "account'", "change", "address_index"] + 'key_path': KEY_PATH_LEGACY }, { 'purpose': 45, @@ -162,7 +195,7 @@ 'multisig': True, 'encoding': 'base58', 'description': 'Legacy multisig wallet using pay-to-script-hash scripts', - 'key_path': ["m", "purpose'", "cosigner_index", "change", "address_index"] + 'key_path': KEY_PATH_P2SH }, { 'purpose': 48, @@ -171,7 +204,7 @@ 'multisig': True, 'encoding': 'base58', 'description': 'Segwit multisig wallet using pay-to-wallet-script-hash scripts nested in p2sh scripts', - 'key_path': ["m", "purpose'", "coin_type'", "account'", "script_type'", "change", "address_index"] + 'key_path': KEY_PATH_P2WSH }, { 'purpose': 48, @@ -180,7 +213,7 @@ 'multisig': True, 'encoding': 'bech32', 'description': 'Segwit multisig wallet using native segwit pay-to-wallet-script-hash scripts', - 'key_path': ["m", "purpose'", "coin_type'", "account'", "script_type'", "change", "address_index"] + 'key_path': KEY_PATH_P2WSH }, { 'purpose': 49, @@ -189,7 +222,7 @@ 'multisig': False, 'encoding': 'base58', 'description': 'Segwit wallet using pay-to-wallet-public-key-hash scripts nested in p2sh scripts', - 'key_path': ["m", "purpose'", "coin_type'", "account'", "change", "address_index"] + 'key_path': KEY_PATH_P2WPKH }, { 'purpose': 84, @@ -198,7 +231,7 @@ 'multisig': False, 'encoding': 'bech32', 'description': 'Segwit multisig wallet using native segwit pay-to-wallet-public-key-hash scripts', - 'key_path': ["m", "purpose'", "coin_type'", "account'", "change", "address_index"] + 'key_path': KEY_PATH_P2WPKH }, # { # 'purpose': 86, @@ -211,9 +244,6 @@ # }, ] -# UNITTESTS -UNITTESTS_FULL_DATABASE_TEST = False - # CACHING SERVICE_CACHING_ENABLED = True @@ -235,7 +265,7 @@ def config_get(section, var, fallback, is_boolean=False): global ALLOW_DATABASE_THREADS, DEFAULT_DATABASE_CACHE global BCL_LOG_FILE, LOGLEVEL, ENABLE_BITCOINLIB_LOGGING global TIMEOUT_REQUESTS, DEFAULT_LANGUAGE, DEFAULT_NETWORK, DEFAULT_WITNESS_TYPE - global UNITTESTS_FULL_DATABASE_TEST, SERVICE_CACHING_ENABLED, DATABASE_ENCRYPTION_ENABLED, DB_FIELD_ENCRYPTION_KEY + global SERVICE_CACHING_ENABLED, DATABASE_ENCRYPTION_ENABLED, DB_FIELD_ENCRYPTION_KEY, DB_FIELD_ENCRYPTION_PASSWORD global SERVICE_MAX_ERRORS, BLOCK_COUNT_CACHE_TIME, MAX_TRANSACTIONS # Read settings from Configuration file provided in OS environment~/.bitcoinlib/ directory @@ -258,16 +288,19 @@ def config_get(section, var, fallback, is_boolean=False): BCL_DATABASE_DIR.mkdir(parents=True, exist_ok=True) default_databasefile = DEFAULT_DATABASE = \ config_get('locations', 'default_databasefile', fallback='bitcoinlib.sqlite') - if not default_databasefile.startswith('postgresql') or default_databasefile.startswith('mysql'): + if not (default_databasefile.startswith('postgresql') or default_databasefile.startswith('mysql') or + default_databasefile.startswith('mariadb')): DEFAULT_DATABASE = str(Path(BCL_DATABASE_DIR, default_databasefile)) default_databasefile_cache = DEFAULT_DATABASE_CACHE = \ config_get('locations', 'default_databasefile_cache', fallback='bitcoinlib_cache.sqlite') - if not default_databasefile_cache.startswith('postgresql') or default_databasefile_cache.startswith('mysql'): + if not (default_databasefile_cache.startswith('postgresql') or default_databasefile_cache.startswith('mysql') or + default_databasefile_cache.startswith('mariadb')): DEFAULT_DATABASE_CACHE = str(Path(BCL_DATABASE_DIR, default_databasefile_cache)) ALLOW_DATABASE_THREADS = config_get("common", "allow_database_threads", fallback=True, is_boolean=True) SERVICE_CACHING_ENABLED = config_get('common', 'service_caching_enabled', fallback=True, is_boolean=True) DATABASE_ENCRYPTION_ENABLED = config_get('common', 'database_encryption_enabled', fallback=False, is_boolean=True) DB_FIELD_ENCRYPTION_KEY = os.environ.get('DB_FIELD_ENCRYPTION_KEY') + DB_FIELD_ENCRYPTION_PASSWORD = os.environ.get('DB_FIELD_ENCRYPTION_PASSWORD') # Log settings ENABLE_BITCOINLIB_LOGGING = config_get("logs", "enable_bitcoinlib_logging", fallback=True, is_boolean=True) @@ -286,10 +319,6 @@ def config_get(section, var, fallback, is_boolean=False): DEFAULT_NETWORK = config_get('common', 'default_network', fallback=DEFAULT_NETWORK) DEFAULT_WITNESS_TYPE = config_get('common', 'default_witness_type', fallback=DEFAULT_WITNESS_TYPE) - full_db_test = os.environ.get('UNITTESTS_FULL_DATABASE_TEST') - if full_db_test in [1, True, 'True', 'true', 'TRUE']: - UNITTESTS_FULL_DATABASE_TEST = True - if not data: return False return True diff --git a/bitcoinlib/config/secp256k1.py b/bitcoinlib/config/secp256k1.py index b97022f1..39e27168 100644 --- a/bitcoinlib/config/secp256k1.py +++ b/bitcoinlib/config/secp256k1.py @@ -23,12 +23,12 @@ # Parameters secp256k1 # from http://www.secg.org/sec2-v2.pdf, par 2.4.1 -secp256k1_p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F -secp256k1_n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 +secp256k1_p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F # field size +secp256k1_n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 # order secp256k1_b = 7 secp256k1_a = 0 -secp256k1_Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 -secp256k1_Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 +secp256k1_Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 # generator point x +secp256k1_Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 # generator point y # secp256k1_curve = ecdsa.ellipticcurve.CurveFp(secp256k1_p, secp256k1_a, secp256k1_b) # secp256k1_generator = ecdsa.ellipticcurve.Point(secp256k1_curve, secp256k1_Gx, secp256k1_Gy, secp256k1_n) diff --git a/bitcoinlib/data/config.ini b/bitcoinlib/data/config.ini index b2bfe197..39610d9d 100644 --- a/bitcoinlib/data/config.ini +++ b/bitcoinlib/data/config.ini @@ -16,13 +16,14 @@ # Default directory for database files. Relative paths will be based in user or bitcoinlib installation directory. Only used for sqlite files. ;database_dir=database -# Default database file for wallets, keys and transactions. Relative paths will be based in 'database_dir' +# Default database file for wallets, keys and transactions. Relative paths for sqlite will be based in 'database_dir'. +# You can use SQLite, PostgreSQL and MariaDB (MySQL) databases ;default_databasefile=bitcoinlib.sqlite -;default_databasefile_cache=bitcoinlib_cache.sqlite +;default_databasefile=postgresql+psycopg://postgres:bitcoinlib@localhost:5432/bitcoinlib -# You can also use PostgreSQL or MySQL databases, for instance: -;default_databasefile=postgresql://postgres:bitcoinlib@localhost:5432/bitcoinlib -;default_databasefile_cache==postgresql://postgres:bitcoinlib@localhost:5432/bitcoinlib_cache +# For caching SQLite or PostgreSQL databases can be used. +;default_databasefile_cache=bitcoinlib_cache.sqlite +;default_databasefile_cache==postgresql+psycopg://postgres:bitcoinlib@localhost:5432/bitcoinlib_cache [common] # Allow database threads in SQLite databases diff --git a/bitcoinlib/data/networks.json b/bitcoinlib/data/networks.json index 3530125c..0948aea9 100644 --- a/bitcoinlib/data/networks.json +++ b/bitcoinlib/data/networks.json @@ -11,18 +11,18 @@ "prefix_bech32": "blt", "prefix_wif": "99", "prefixes_wif": [ - ["9488B21E", "YXsf", "public", false, "legacy", "p2pkh"], - ["9488B21E", "YXsf", "public", true, "legacy", "p2sh"], - ["9488ADE4", "YXsc", "private", false, "legacy", "p2pkh"], - ["9488ADE4", "YXsc", "private", true, "legacy", "p2sh"], - ["9488B21E", "YXsf", "public", false, "p2sh-segwit", "p2sh_p2wpkh"], - ["9488B21E", "YXsf", "public", true, "p2sh-segwit", "p2sh_p2wsh"], - ["9488ADE4", "YXsc", "private", false, "p2sh-segwit", "p2sh_p2wpkh"], - ["9488ADE4", "YXsc", "private", true, "p2sh-segwit", "p2sh_p2wsh"], - ["9488B21E", "YXsf", "public", false, "segwit", "p2wpkh"], - ["9488B21E", "YXsf", "public", true, "segwit", "p2wsh"], - ["9488ADE4", "YXsc", "private", false, "segwit", "p2wpkh"], - ["9488ADE4", "YXsc", "private", true, "segwit", "p2wsh"] + ["2FFFACCC", "BC11", "public", false, "legacy", "p2pkh"], + ["2FFFACCC", "BC11", "public", true, "legacy", "p2sh"], + ["2FFFADDD", "BC12", "private", false, "legacy", "p2pkh"], + ["2FFFADDD", "BC12", "private", true, "legacy", "p2sh"], + ["2FFFAEEE", "BC13", "public", false, "p2sh-segwit", "p2sh_p2wpkh"], + ["2FFFB100", "BC14", "public", true, "p2sh-segwit", "p2sh_p2wsh"], + ["2FFFB300", "BC15", "private", false, "p2sh-segwit", "p2sh_p2wpkh"], + ["2FFFB500", "BC16", "private", true, "p2sh-segwit", "p2sh_p2wsh"], + ["2FFFB666", "BC17", "public", false, "segwit", "p2wpkh"], + ["2FFFB800", "BC18", "public", true, "segwit", "p2wsh"], + ["2FFFB900", "BC19", "private", false, "segwit", "p2wpkh"], + ["2FFFBA00", "BC1A", "private", true, "segwit", "p2wsh"] ], "bip44_cointype": 9999999, "denominator": 0.00000001, @@ -230,56 +230,6 @@ "fee_max": 1000000, "priority": 6 }, - "dash": - { - "description": "Dash Network", - "currency_name": "dash", - "currency_name_plural": "dash coins", - "currency_symbol": "DASH", - "currency_code": "DASH", - "prefix_address": "4C", - "prefix_address_p2sh": "10", - "prefix_bech32": "dash", - "prefix_wif": "CC", - "prefixes_wif": [ - ["0488B21E", "xpub", "public", false, "legacy", "p2pkh"], - ["0488B21E", "xpub", "public", true, "legacy", "p2sh"], - ["0488ADE4", "xprv", "private", false, "legacy", "p2pkh"], - ["0488ADE4", "xprv", "private", true, "legacy", "p2sh"] - ], - "bip44_cointype": 5, - "denominator": 0.00000001, - "dust_amount": 1000, - "fee_default": 2000, - "fee_min": 1000, - "fee_max": 50000, - "priority": 10 - }, - "dash_testnet": - { - "description": "Dash Testnet Network", - "currency_name": "test-dash coins", - "currency_name_plural": "test-dash", - "currency_symbol": "TDASH", - "currency_code": "tDASH", - "prefix_address": "8C", - "prefix_address_p2sh": "13", - "prefix_bech32": "tdash", - "prefix_wif": "EF", - "prefixes_wif": [ - ["043587CF", "tpub", "public", false, "legacy", "p2pkh"], - ["043587CF", "tpub", "public", true, "legacy", "p2sh"], - ["04358394", "tprv", "private", false, "legacy", "p2pkh"], - ["04358394", "tprv", "private", true, "legacy", "p2sh"] - ], - "bip44_cointype": 1, - "denominator": 0.00000001, - "dust_amount": 1000, - "fee_default": 10000, - "fee_min": 1000, - "fee_max": 50000, - "priority": 6 - }, "dogecoin": { "description": "Dogecoin", diff --git a/bitcoinlib/data/providers.examples.json b/bitcoinlib/data/providers.examples.json index 6a700910..283733ea 100644 --- a/bitcoinlib/data/providers.examples.json +++ b/bitcoinlib/data/providers.examples.json @@ -153,39 +153,6 @@ "denominator": 100000000, "network_overrides": {"prefix_address_p2sh": "32"} }, - "dashd": { - "provider": "dashd", - "network": "dash", - "client_class": "DashdClient", - "url": "", - "provider_coin_id": "", - "api_key": "", - "priority": 20, - "denominator": 100000000, - "network_overrides": null - }, - "dashd.testnet": { - "provider": "dashd", - "network": "dash_testnet", - "client_class": "DashdClient", - "url": "", - "provider_coin_id": "", - "api_key": "", - "priority": 20, - "denominator": 100000000, - "network_overrides": null - }, - "cryptoid.dash": { - "provider": "cryptoid", - "network": "dash", - "client_class": "CryptoID", - "provider_coin_id": "dash", - "url": "https://chainz.cryptoid.info/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, "litecoreio.litecoin": { "provider": "litecoreio", "network": "litecoin", @@ -329,17 +296,6 @@ "denominator": 100000000, "network_overrides": null }, - "insightdash": { - "provider": "insightdash", - "network": "dash", - "client_class": "InsightDashClient", - "provider_coin_id": "", - "url": "https://insight.dash.org/api/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, "smartbit": { "provider": "smartbit", "network": "bitcoin", @@ -461,28 +417,6 @@ "denominator": 100000000, "network_overrides": null }, - "chainso.dash": { - "provider": "chainso", - "network": "dash", - "client_class": "ChainSo", - "provider_coin_id": "DASH", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "chainso.dash.testnet": { - "provider": "chainso", - "network": "dash_testnet", - "client_class": "ChainSo", - "provider_coin_id": "DASHTEST", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, "chainso.dogecoin": { "provider": "chainso", "network": "dogecoin", diff --git a/bitcoinlib/data/providers.json b/bitcoinlib/data/providers.json index bc2a275c..d7fd2dc2 100644 --- a/bitcoinlib/data/providers.json +++ b/bitcoinlib/data/providers.json @@ -32,17 +32,6 @@ "denominator": 1, "network_overrides": null }, - "bitgo.testnet": { - "provider": "bitgo", - "network": "testnet", - "client_class": "BitGoClient", - "provider_coin_id": "", - "url": "https://test.bitgo.com/api/v1/", - "api_key": "", - "priority": 10, - "denominator": 1, - "network_overrides": null - }, "blockcypher.litecoin": { "provider": "blockcypher", "network": "litecoin", @@ -87,17 +76,6 @@ "denominator": 100000000, "network_overrides": {"prefix_address_p2sh": "32"} }, - "cryptoid.dash": { - "provider": "cryptoid", - "network": "dash", - "client_class": "CryptoID", - "provider_coin_id": "dash", - "url": "https://chainz.cryptoid.info/", - "api_key": "api-key-needed", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, "blockchair": { "provider": "blockchair", "network": "bitcoin", @@ -131,17 +109,6 @@ "denominator": 100000000, "network_overrides": null }, - "blockchair.dash": { - "provider": "blockchair", - "network": "dash", - "client_class": "BlockChairClient", - "provider_coin_id": "", - "url": "https://api.blockchair.com/dash/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, "blockchair.dogecoin": { "provider": "blockchair", "network": "dogecoin", @@ -164,17 +131,6 @@ "denominator": 100000000, "network_overrides": null }, - "bitaps.testnet": { - "provider": "bitaps", - "network": "testnet", - "client_class": "BitapsClient", - "provider_coin_id": "", - "url": "https://api.bitaps.com/btc/testnet/v1/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, "bitaps.litecoin": { "provider": "bitaps", "network": "litecoin", @@ -240,24 +196,13 @@ "priority": 10, "denominator": 100000000, "network_overrides": null - }, - "litecoinblockexplorer.dash": { - "provider": "litecoinblockexplorer", - "network": "dash", - "client_class": "LitecoinBlockexplorerClient", - "provider_coin_id": "", - "url": "https://dashblockexplorer.com/api/v1/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null }, "litecoinblockexplorer.testnet2": { "provider": "litecoinblockexplorer", "network": "testnet", "client_class": "LitecoinBlockexplorerClient", "provider_coin_id": "", - "url": "https://tbtc1.blockbook.bitaccess.net/api/v1/", + "url": "https://tbtc1.trezor.io/api/v1/", "api_key": "", "priority": 10, "denominator": 100000000, @@ -306,24 +251,13 @@ "priority": 10, "denominator": 100000000, "network_overrides": null - }, - "blockbook.dash": { - "provider": "blockbook", - "network": "dash", - "client_class": "BlockbookClient", - "provider_coin_id": "", - "url": "https://dashblockexplorer.com/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null }, "blockbook.testnet2": { "provider": "blockbook", "network": "testnet", "client_class": "BlockbookClient", "provider_coin_id": "", - "url": "https://tbtc1.blockbook.bitaccess.net/api/v2/", + "url": "https://tbtc2.trezor.io/api/v2/", "api_key": "", "priority": 10, "denominator": 100000000, @@ -340,17 +274,6 @@ "denominator": 100000000, "network_overrides": null }, - "insightdash": { - "provider": "insightdash", - "network": "dash", - "client_class": "InsightDashClient", - "provider_coin_id": "", - "url": "https://insight.dash.org/api/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, "blockstream": { "provider": "blockstream", "network": "bitcoin", @@ -384,6 +307,17 @@ "denominator": 1, "network_overrides": null }, + "blockcypher.testnet": { + "provider": "blockcypher", + "network": "testnet", + "client_class": "BlockCypher", + "provider_coin_id": "", + "url": "https://api.blockcypher.com/v1/btc/test3/", + "api_key": "", + "priority": 10, + "denominator": 1, + "network_overrides": null + }, "mempool": { "provider": "mempool", "network": "bitcoin", @@ -405,27 +339,5 @@ "priority": 10, "denominator": 1, "network_overrides": null - }, - "blocksmurfer": { - "provider": "blocksmurfer", - "network": "bitcoin", - "client_class": "BlocksmurferClient", - "provider_coin_id": "", - "url": "https://harari.blocksmurfer.io/api/v1/btc/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "blocksmurfer.testnet": { - "provider": "blocksmurfer", - "network": "testnet", - "client_class": "BlocksmurferClient", - "provider_coin_id": "", - "url": "https://harari.blocksmurfer.io/api/v1/tbtc/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null } } diff --git a/bitcoinlib/data/providers.old.json b/bitcoinlib/data/providers.old.json deleted file mode 100644 index 5e5ad934..00000000 --- a/bitcoinlib/data/providers.old.json +++ /dev/null @@ -1,476 +0,0 @@ -{ - "bitcoinlib_test": { - "provider": "bitcoinlibtest", - "network": "bitcoinlib_test", - "client_class": "BitcoinLibTestClient", - "provider_coin_id": "", - "url": "local", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "blockchaininfo": { - "provider": "blockchaininfo", - "network": "bitcoin", - "client_class": "BlockchainInfoClient", - "provider_coin_id": "", - "url": "https://blockchain.info/", - "api_key": "", - "priority": 10, - "denominator": 1, - "network_overrides": null - }, - "blockcypher": { - "provider": "blockcypher", - "network": "bitcoin", - "client_class": "BlockCypher", - "provider_coin_id": "", - "url": "https://api.blockcypher.com/v1/btc/main/", - "api_key": "", - "priority": 10, - "denominator": 1, - "network_overrides": null - }, - "bitgo": { - "provider": "bitgo", - "network": "bitcoin", - "client_class": "BitGoClient", - "provider_coin_id": "", - "url": "https://www.bitgo.com/api/v1/", - "api_key": "", - "priority": 10, - "denominator": 1, - "network_overrides": null - }, - "bitgo.testnet": { - "provider": "bitgo", - "network": "testnet", - "client_class": "BitGoClient", - "provider_coin_id": "", - "url": "https://test.bitgo.com/api/v1/", - "api_key": "", - "priority": 10, - "denominator": 1, - "network_overrides": null - }, - "coinfees": { - "provider": "coinfees", - "network": "bitcoin", - "client_class": "CoinfeesClient", - "provider_coin_id": "", - "url": "https://bitcoinfees.earn.com/api/v1/", - "api_key": "", - "priority": 10, - "denominator": 1, - "network_overrides": null - }, - "blockcypher.testnet": { - "provider": "blockcypher", - "network": "testnet", - "client_class": "BlockCypher", - "provider_coin_id": "", - "url": "https://api.blockcypher.com/v1/btc/test3/", - "api_key": "", - "priority": 10, - "denominator": 1, - "network_overrides": null - }, - "blockcypher.litecoin": { - "provider": "blockcypher", - "network": "litecoin", - "client_class": "BlockCypher", - "provider_coin_id": "", - "url": "https://api.blockcypher.com/v1/ltc/main/", - "api_key": "", - "priority": 10, - "denominator": 1, - "network_overrides": {"prefix_address_p2sh": "05"} - }, - "blockcypher.litecoin.legacy": { - "provider": "blockcypher", - "network": "litecoin_legacy", - "client_class": "BlockCypher", - "provider_coin_id": "", - "url": "https://api.blockcypher.com/v1/ltc/main/", - "api_key": "", - "priority": 10, - "denominator": 1, - "network_overrides": null - }, - "cryptoid.litecoin": { - "provider": "cryptoid", - "network": "litecoin", - "client_class": "CryptoID", - "provider_coin_id": "ltc", - "url": "https://chainz.cryptoid.info/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "cryptoid.litecoin.legacy": { - "provider": "cryptoid", - "network": "litecoin_legacy", - "client_class": "CryptoID", - "provider_coin_id": "ltc", - "url": "https://chainz.cryptoid.info/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": {"prefix_address_p2sh": "32"} - }, - "cryptoid.dash": { - "provider": "cryptoid", - "network": "dash", - "client_class": "CryptoID", - "provider_coin_id": "dash", - "url": "https://chainz.cryptoid.info/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "litecoreio.litecoin": { - "provider": "litecoreio", - "network": "litecoin", - "client_class": "LitecoreIOClient", - "provider_coin_id": "", - "url": "https://insight.litecore.io/api/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "litecoreio.litecoin.legacy": { - "provider": "litecoreio", - "network": "litecoin_legacy", - "client_class": "LitecoreIOClient", - "provider_coin_id": "", - "url": "https://insight.litecore.io/api/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": {"prefix_address_p2sh": "32"} - }, - "litecoreio.litecoin.testnet": { - "provider": "litecoreio", - "network": "litecoin_testnet", - "client_class": "LitecoreIOClient", - "provider_coin_id": "", - "url": "https://testnet.litecore.io/api/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "blockchair": { - "provider": "blockchair", - "network": "bitcoin", - "client_class": "BlockChairClient", - "provider_coin_id": "", - "url": "https://api.blockchair.com/bitcoin/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "blockchair.testnet": { - "provider": "blockchair", - "network": "testnet", - "client_class": "BlockChairClient", - "provider_coin_id": "", - "url": "https://api.blockchair.com/bitcoin/testnet/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "blockchair.litecoin": { - "provider": "blockchair", - "network": "litecoin", - "client_class": "BlockChairClient", - "provider_coin_id": "", - "url": "https://api.blockchair.com/litecoin/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "blockchair.dash": { - "provider": "blockchair", - "network": "dash", - "client_class": "BlockChairClient", - "provider_coin_id": "", - "url": "https://api.blockchair.com/dash/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "blockchair.dogecoin": { - "provider": "blockchair", - "network": "dogecoin", - "client_class": "BlockChairClient", - "provider_coin_id": "", - "url": "https://api.blockchair.com/dogecoin/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "bitaps": { - "provider": "bitaps", - "network": "bitcoin", - "client_class": "BitapsClient", - "provider_coin_id": "", - "url": "https://api.bitaps.com/btc/v1/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "bitaps.testnet": { - "provider": "bitaps", - "network": "testnet", - "client_class": "BitapsClient", - "provider_coin_id": "", - "url": "https://api.bitaps.com/btc/testnet/v1/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "bitaps.litecoin": { - "provider": "bitaps", - "network": "litecoin", - "client_class": "BitapsClient", - "provider_coin_id": "", - "url": "https://api.bitaps.com/ltc/v1/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "bitaps.litecoin.legacy": { - "provider": "bitaps", - "network": "litecoin_legacy", - "client_class": "BitapsClient", - "provider_coin_id": "", - "url": "https://api.bitaps.com/ltc/v1/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "bitaps.litecoin.testnet": { - "provider": "bitaps", - "network": "litecoin_testnet", - "client_class": "BitapsClient", - "provider_coin_id": "", - "url": "https://api.bitaps.com/ltc/testnet/v1/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "litecoinblockexplorer.litecoin": { - "provider": "litecoinblockexplorer", - "network": "litecoin", - "client_class": "LitecoinBlockexplorerClient", - "provider_coin_id": "", - "url": "https://litecoinblockexplorer.net/api/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "litecoinblockexplorer.litecoin.legacy": { - "provider": "litecoinblockexplorer", - "network": "litecoin_legacy", - "client_class": "LitecoinBlockexplorerClient", - "provider_coin_id": "", - "url": "https://litecoinblockexplorer.net/api/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "insightdash": { - "provider": "insightdash", - "network": "dash", - "client_class": "InsightDashClient", - "provider_coin_id": "", - "url": "https://insight.dash.org/api/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "blockstream": { - "provider": "blockstream", - "network": "bitcoin", - "client_class": "BlockstreamClient", - "provider_coin_id": "", - "url": "https://blockstream.info/api/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "blockstream.testnet": { - "provider": "blockstream", - "network": "testnet", - "client_class": "BlockstreamClient", - "provider_coin_id": "", - "url": "https://blockstream.info/testnet/api/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "blocksmurfer": { - "provider": "blocksmurfer", - "network": "bitcoin", - "client_class": "BlocksmurferClient", - "provider_coin_id": "", - "url": "http://blocksmurfer.io/api/v1/btc/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "chainso.litecoin.testnet": { - "provider": "chainso", - "network": "litecoin_testnet", - "client_class": "ChainSo", - "provider_coin_id": "LTCTEST", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "chainso": { - "provider": "chainso", - "network": "bitcoin", - "client_class": "ChainSo", - "provider_coin_id": "BTC", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 8, - "denominator": 100000000, - "network_overrides": null - }, - "chainso.testnet": { - "provider": "chainso", - "network": "testnet", - "client_class": "ChainSo", - "provider_coin_id": "BTCTEST", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "chainso.litecoin": { - "provider": "chainso", - "network": "litecoin", - "client_class": "ChainSo", - "provider_coin_id": "LTC", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "chainso.litecoin.legacy": - { - "provider": "chainso", - "network": "litecoin_legacy", - "client_class": "ChainSo", - "provider_coin_id": "LTC", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "chainso.dash": { - "provider": "chainso", - "network": "dash", - "client_class": "ChainSo", - "provider_coin_id": "DASH", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "chainso.dash.testnet": { - "provider": "chainso", - "network": "dash_testnet", - "client_class": "ChainSo", - "provider_coin_id": "DASHTEST", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "chainso.dogecoin": { - "provider": "chainso", - "network": "dogecoin", - "client_class": "ChainSo", - "provider_coin_id": "DOGE", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "chainso.dogecoin.testnet": { - "provider": "chainso", - "network": "dogecoin_testnet", - "client_class": "ChainSo", - "provider_coin_id": "DOGETEST", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "dogecoin": { - "provider": "dogecoind", - "network": "dogecoin", - "client_class": "DogecoindClient", - "provider_coin_id": "", - "url": "", - "api_key": "", - "priority": 20, - "denominator": 100000000, - "network_overrides": null - }, - "dogecoind.testnet": { - "provider": "dogecoind", - "network": "dogecoin_testnet", - "client_class": "DogecoindClient", - "provider_coin_id": "", - "url": "", - "api_key": "", - "priority": 20, - "denominator": 100000000, - "network_overrides": null - }, - "blockcypher.dogecoin": { - "provider": "blockcypher", - "network": "dogecoin", - "client_class": "BlockCypher", - "provider_coin_id": "", - "url": "https://api.blockcypher.com/v1/doge/main/", - "api_key": "", - "priority": 10, - "denominator": 1, - "network_overrides": null - } -} diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index 8c0cfeb9..c52eaeb8 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -18,15 +18,15 @@ # along with this program. If not, see . # -from sqlalchemy import create_engine +from sqlalchemy import create_engine, text from sqlalchemy import (Column, Integer, BigInteger, UniqueConstraint, CheckConstraint, String, Boolean, Sequence, ForeignKey, DateTime, LargeBinary, TypeDecorator) from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.compiler import compiles -from sqlalchemy.orm import sessionmaker, relationship, close_all_sessions +from sqlalchemy.orm import sessionmaker, relationship, session from urllib.parse import urlparse from bitcoinlib.main import * -from bitcoinlib.encoding import aes_encrypt, aes_decrypt +from bitcoinlib.encoding import aes_encrypt, aes_decrypt, double_sha256 _logger = logging.getLogger(__name__) Base = declarative_base() @@ -44,7 +44,7 @@ class Db: Bitcoinlib Database object used by Service() and HDWallet() class. Initialize database and open session when creating database object. - Create new database if is doesn't exist yet + Create new database if it doesn't exist yet """ def __init__(self, db_uri=None, password=None): @@ -67,9 +67,11 @@ def __init__(self, db_uri=None, password=None): if self.o.scheme == 'mysql': db_uri += "&" if "?" in db_uri else "?" db_uri += 'binary_prefix=true' + if self.o.scheme == 'postgresql': + db_uri = self.o._replace(scheme="postgresql+psycopg").geturl() self.engine = create_engine(db_uri, isolation_level='READ UNCOMMITTED') - Session = sessionmaker(bind=self.engine) + Session = sessionmaker(bind=self.engine, expire_on_commit=False) Base.metadata.create_all(self.engine) self._import_config_data(Session) self.session = Session() @@ -96,8 +98,8 @@ def __init__(self, db_uri=None, password=None): def drop_db(self, yes_i_am_sure=False): if yes_i_am_sure: self.session.commit() - self.session.close_all() - close_all_sessions() + self.session.close() + session.close_all_sessions() Base.metadata.drop_all(self.engine) @staticmethod @@ -128,7 +130,26 @@ def add_column(engine, table_name, column): """ column_name = column.compile(dialect=engine.dialect) column_type = column.type.compile(engine.dialect) - engine.execute("ALTER TABLE %s ADD COLUMN %s %s" % (table_name, column_name, column_type)) + statement = text("ALTER TABLE %s ADD COLUMN %s %s" % (table_name, column_name, column_type)) + with engine.connect() as conn: + result = conn.execute(statement) + return result + + +def _get_encryption_key(default_impl): + impl = default_impl + key = None + if DATABASE_ENCRYPTION_ENABLED: + if not (DB_FIELD_ENCRYPTION_KEY or DB_FIELD_ENCRYPTION_PASSWORD): + _logger.warning("Database encryption is enabled but value DB_FIELD_ENCRYPTION_KEY not found in " + "environment. Please supply 32 bytes key as hexadecimal string.") + if DB_FIELD_ENCRYPTION_KEY: + impl = LargeBinary + key = bytes().fromhex(DB_FIELD_ENCRYPTION_KEY) + elif DB_FIELD_ENCRYPTION_PASSWORD: + impl = LargeBinary + key = double_sha256(bytes(DB_FIELD_ENCRYPTION_PASSWORD, 'utf8')) + return key, impl class EncryptedBinary(TypeDecorator): @@ -136,23 +157,16 @@ class EncryptedBinary(TypeDecorator): FieldType for encrypted Binary storage using EAS encryption """ - impl = LargeBinary cache_ok = True - key = None - if DATABASE_ENCRYPTION_ENABLED: - if not DB_FIELD_ENCRYPTION_KEY: - _logger.warning("Database encryption is enabled but value DB_FIELD_ENCRYPTION_KEY not found in " - "environment. Please supply 32 bytes key as hexadecimal string.") - else: - key = bytes().fromhex(DB_FIELD_ENCRYPTION_KEY) + key, impl = _get_encryption_key(LargeBinary) def process_bind_param(self, value, dialect): - if value is None or self.key is None or not DATABASE_ENCRYPTION_ENABLED: + if value is None or self.key is None or not (DB_FIELD_ENCRYPTION_KEY or DB_FIELD_ENCRYPTION_PASSWORD): return value return aes_encrypt(value, self.key) def process_result_value(self, value, dialect): - if value is None or self.key is None or not DATABASE_ENCRYPTION_ENABLED: + if value is None or self.key is None or not (DB_FIELD_ENCRYPTION_KEY or DB_FIELD_ENCRYPTION_PASSWORD): return value return aes_decrypt(value, self.key) @@ -162,26 +176,20 @@ class EncryptedString(TypeDecorator): FieldType for encrypted String storage using EAS encryption """ - impl = String cache_ok = True - key = None - if DATABASE_ENCRYPTION_ENABLED: - if not DB_FIELD_ENCRYPTION_KEY: - _logger.warning("Database encryption is enabled but value DB_FIELD_ENCRYPTION_KEY not found in " - "environment. Please supply 32 bytes key as hexadecimal string.") - else: - impl = LargeBinary - key = bytes().fromhex(DB_FIELD_ENCRYPTION_KEY) + key, impl = _get_encryption_key(String) def process_bind_param(self, value, dialect): - if value is None or self.key is None or not DATABASE_ENCRYPTION_ENABLED: + if value is None or self.key is None or not (DB_FIELD_ENCRYPTION_KEY or DB_FIELD_ENCRYPTION_PASSWORD): return value if not isinstance(value, bytes): value = bytes(value, 'utf8') return aes_encrypt(value, self.key) def process_result_value(self, value, dialect): - if value is None or self.key is None or not DATABASE_ENCRYPTION_ENABLED: + if value is None or self.key is None or not (DB_FIELD_ENCRYPTION_KEY or DB_FIELD_ENCRYPTION_PASSWORD): + if isinstance(value, bytes): + raise ValueError("Data is encrypted please provide key in environment") return value return aes_decrypt(value, self.key).decode('utf8') @@ -212,8 +220,9 @@ class DbWallet(Base): purpose = Column(Integer, doc="Wallet purpose ID. BIP-44 purpose field, indicating which key-scheme is used default is 44") scheme = Column(String(25), doc="Key structure type, can be BIP-32 or single") - witness_type = Column(String(20), default='legacy', - doc="Wallet witness type. Can be 'legacy', 'segwit' or 'p2sh-segwit'. Default is legacy.") + witness_type = Column(String(20), default='segwit', + doc="Wallet witness type. Can be 'legacy', 'segwit', 'p2sh-segwit' or 'mixed. Default is " + "segwit.") encoding = Column(String(15), default='base58', doc="Default encoding to use for address generation, i.e. base58 or bech32. Default is base58.") main_key_id = Column(Integer, @@ -239,12 +248,14 @@ class DbWallet(Base): "* If accounts are used, the account level must be 3. I.e.: m/purpose/coin_type/account/ " "* All keys must be hardened, except for change, address_index or cosigner_id " " Max length of path is 8 levels") + anti_fee_sniping = Column(Boolean, default=True, doc="Set default locktime in transactions to avoid fee-sniping") default_account_id = Column(Integer, doc="ID of default account for this wallet if multiple accounts are used") __table_args__ = ( CheckConstraint(scheme.in_(['single', 'bip32']), name='constraint_allowed_schemes'), CheckConstraint(encoding.in_(['base58', 'bech32']), name='constraint_default_address_encodings_allowed'), - CheckConstraint(witness_type.in_(['legacy', 'segwit', 'p2sh-segwit']), name='wallet_constraint_allowed_types'), + CheckConstraint(witness_type.in_(['legacy', 'segwit', 'p2sh-segwit', 'p2tr']), + name='wallet_constraint_allowed_types'), ) def __repr__(self): @@ -281,12 +292,12 @@ class DbKey(Base): "depth=1 are the masterkeys children.") change = Column(Integer, doc="Change or normal address: Normal=0, Change=1") address_index = Column(BigInteger, doc="Index of address in HD key structure address level") - public = Column(LargeBinary(128), index=True, doc="Bytes representation of public key") + public = Column(LargeBinary(65), index=True, doc="Bytes representation of public key") private = Column(EncryptedBinary(48), doc="Bytes representation of private key") - wif = Column(EncryptedString(255), index=True, doc="Public or private WIF (Wallet Import Format) representation") + wif = Column(EncryptedString(128), index=True, doc="Public or private WIF (Wallet Import Format) representation") compressed = Column(Boolean, default=True, doc="Is key compressed or not. Default is True") key_type = Column(String(10), default='bip32', doc="Type of key: single, bip32 or multisig. Default is bip32") - address = Column(String(255), index=True, + address = Column(String(100), index=True, doc="Address representation of key. An cryptocurrency address is a hash of the public key") cosigner_id = Column(Integer, doc="ID of cosigner, used if key is part of HD Wallet") encoding = Column(String(15), default='base58', doc='Encoding used to represent address: base58 or bech32') @@ -303,8 +314,11 @@ class DbKey(Base): used = Column(Boolean, default=False, doc="Has key already been used on the blockchain in as input or output? " "Default is False") network_name = Column(String(20), ForeignKey('networks.name'), - doc="Name of key network, i.e. bitcoin, litecoin, dash") - latest_txid = Column(LargeBinary(32), doc="TxId of latest transaction downloaded from the blockchain") + doc="Name of key network, i.e. bitcoin, litecoin") + latest_txid = Column(LargeBinary(33), doc="TxId of latest transaction downloaded from the blockchain") + witness_type = Column(String(20), default='segwit', + doc="Key witness type, only specify when using mixed wallets. Can be 'legacy', 'segwit' or " + "'p2sh-segwit'. Default is segwit.") network = relationship("DbNetwork", doc="DbNetwork object for this key") multisig_parents = relationship("DbKeyMultisigChildren", backref='child_key', primaryjoin=id == DbKeyMultisigChildren.child_id, @@ -336,7 +350,7 @@ class DbNetwork(Base): """ __tablename__ = 'networks' - name = Column(String(20), unique=True, primary_key=True, doc="Network name, i.e.: bitcoin, litecoin, dash") + name = Column(String(20), unique=True, primary_key=True, doc="Network name, i.e.: bitcoin, litecoin") description = Column(String(50)) def __repr__(self): @@ -361,13 +375,13 @@ class DbTransaction(Base): __tablename__ = 'transactions' id = Column(Integer, Sequence('transaction_id_seq'), primary_key=True, doc="Unique transaction index for internal usage") - txid = Column(LargeBinary(32), index=True, doc="Bytes representation of transaction ID") + txid = Column(LargeBinary(33), index=True, doc="Bytes representation of transaction ID") wallet_id = Column(Integer, ForeignKey('wallets.id'), index=True, doc="ID of wallet which contains this transaction") account_id = Column(Integer, index=True, doc="ID of account") wallet = relationship("DbWallet", back_populates="transactions", doc="Link to Wallet object which contains this transaction") - witness_type = Column(String(20), default='legacy', doc="Is this a legacy or segwit transaction?") + witness_type = Column(String(20), default='segwit', doc="Is this a legacy or segwit transaction?") version = Column(BigInteger, default=1, doc="Tranaction version. Default is 1 but some wallets use another version number") locktime = Column(BigInteger, default=0, @@ -403,6 +417,7 @@ class DbTransaction(Base): raw = Column(LargeBinary, doc="Raw transaction hexadecimal string. Transaction is included in raw format on the blockchain") verified = Column(Boolean, default=False, doc="Is transaction verified. Default is False") + index = Column(Integer, doc="Index of transaction in block") __table_args__ = ( UniqueConstraint('wallet_id', 'txid', name='constraint_wallet_transaction_hash_unique'), @@ -428,14 +443,14 @@ class DbTransactionInput(Base): transaction = relationship("DbTransaction", back_populates='inputs', doc="Related DbTransaction object") index_n = Column(Integer, primary_key=True, doc="Index number of transaction input") key_id = Column(Integer, ForeignKey('keys.id'), index=True, doc="ID of key used in this input") - key = relationship("DbKey", back_populates="transaction_inputs", doc="Related DbKey object") + key = relationship("DbKey", doc="Related DbKey object") address = Column(String(255), doc="Address string of input, used if no key is associated. " "An cryptocurrency address is a hash of the public key or a redeemscript") witnesses = Column(LargeBinary, doc="Witnesses (signatures) used in Segwit transaction inputs") - witness_type = Column(String(20), default='legacy', - doc="Type of transaction, can be legacy, segwit or p2sh-segwit. Default is legacy") - prev_txid = Column(LargeBinary(32), + witness_type = Column(String(20), default='segwit', + doc="Type of transaction, can be legacy, segwit or p2sh-segwit. Default is segwit") + prev_txid = Column(LargeBinary(33), doc="Transaction hash of previous transaction. Previous unspent outputs (UTXO) is spent " "in this input") output_n = Column(BigInteger, doc="Output_n of previous transaction output that is spent in this input") @@ -467,9 +482,9 @@ class DbTransactionOutput(Base): doc="Transaction ID of parent transaction") transaction = relationship("DbTransaction", back_populates='outputs', doc="Link to transaction object") - output_n = Column(Integer, primary_key=True, doc="Sequence number of transaction output") + output_n = Column(BigInteger, primary_key=True, doc="Sequence number of transaction output") key_id = Column(Integer, ForeignKey('keys.id'), index=True, doc="ID of key used in this transaction output") - key = relationship("DbKey", back_populates="transaction_outputs", doc="List of DbKey object used in this output") + key = relationship("DbKey", doc="List of DbKey object used in this output") address = Column(String(255), doc="Address string of output, used if no key is associated. " "An cryptocurrency address is a hash of the public key or a redeemscript") @@ -479,8 +494,9 @@ class DbTransactionOutput(Base): "'nulldata', 'unknown', 'p2wpkh', 'p2wsh', 'p2tr'. Default is p2pkh") value = Column(BigInteger, default=0, doc="Total transaction output value") spent = Column(Boolean, default=False, doc="Indicated if output is already spent in another transaction") - spending_txid = Column(LargeBinary(32), doc="Transaction hash of input which spends this output") + spending_txid = Column(LargeBinary(33), doc="Transaction hash of input which spends this output") spending_index_n = Column(Integer, doc="Index number of transaction input which spends this output") + is_change = Column(Boolean, default=False, doc="Is this a change output / output to own wallet?") __table_args__ = (UniqueConstraint('transaction_id', 'output_n', name='constraint_transaction_output_unique'),) @@ -501,5 +517,13 @@ def db_update(db, version_db, code_version=BITCOINLIB_VERSION): column = Column('witnesses', LargeBinary, doc="Witnesses (signatures) used in Segwit transaction inputs") add_column(db.engine, 'transaction_inputs', column) # version_db = db_update_version_id(db, '0.6.4') + if version_db < '0.7.0' and code_version >= '0.7.0': + raise ValueError("Old database version %s is not supported in version 0.7+. " + "Please copy private keys and recreate wallets" % version_db) + # TODO: write update script to copy private keys from db + # column = Column('witness_type', String(20), doc="Wallet witness type. Can be 'legacy', 'segwit' or " + # "'p2sh-segwit'. Default is segwit.") + # add_column(db.engine, 'keys', column) + version_db = db_update_version_id(db, code_version) return version_db diff --git a/bitcoinlib/db_cache.py b/bitcoinlib/db_cache.py index 36df4cdb..811e6119 100644 --- a/bitcoinlib/db_cache.py +++ b/bitcoinlib/db_cache.py @@ -21,32 +21,12 @@ from sqlalchemy import create_engine from sqlalchemy import Column, Integer, BigInteger, String, Boolean, ForeignKey, DateTime, Enum, LargeBinary from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker, relationship, close_all_sessions -# try: -# import mysql.connector -# 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 -# # compat.register() -# pass # Only necessary when mysql or postgres is used +from sqlalchemy.orm import sessionmaker, relationship, session from urllib.parse import urlparse from bitcoinlib.main import * _logger = logging.getLogger(__name__) -try: - dbcacheurl_obj = urlparse(DEFAULT_DATABASE_CACHE) - if dbcacheurl_obj.netloc: - dbcacheurl = dbcacheurl_obj.netloc.replace(dbcacheurl_obj.password, 'xxx') - else: - dbcacheurl = dbcacheurl_obj.path - _logger.info("Default Cache Database %s" % dbcacheurl) -except Exception: - _logger.warning("Default Cache Database: unable to parse URL") Base = declarative_base() @@ -59,7 +39,7 @@ class DbCache: """ Cache Database object. Initialize database and open session when creating database object. - Create new database if is doesn't exist yet + Create new database if it doesn't exist yet """ def __init__(self, db_uri=None): @@ -77,8 +57,9 @@ def __init__(self, db_uri=None): db_uri += "&" if "?" in db_uri else "?" db_uri += "check_same_thread=False" if self.o.scheme == 'mysql': - db_uri += "&" if "?" in db_uri else "?" - db_uri += 'binary_prefix=true' + raise NotImplementedError("MySQL does not allow indexing on LargeBinary fields, so caching is not possible") + # db_uri += "&" if "?" in db_uri else "?" + # db_uri += 'binary_prefix=true' self.engine = create_engine(db_uri, isolation_level='READ UNCOMMITTED') Session = sessionmaker(bind=self.engine) @@ -90,8 +71,8 @@ def __init__(self, db_uri=None): def drop_db(self): self.session.commit() - # self.session.close_all() - close_all_sessions() + self.session.close() + session.close_all_sessions() Base.metadata.drop_all(self.engine) @@ -155,7 +136,7 @@ class DbCacheTransaction(Base): fee = Column(BigInteger, doc="Transaction fee") nodes = relationship("DbCacheTransactionNode", cascade="all,delete", doc="List of all inputs and outputs as DbCacheTransactionNode objects") - order_n = Column(Integer, doc="Order of transaction in block") + index = Column(Integer, doc="Index of transaction in block") witness_type = Column(Enum(WitnessTypeTransactions), default=WitnessTypeTransactions.legacy, doc="Transaction type enum: legacy or segwit") diff --git a/bitcoinlib/encoding.py b/bitcoinlib/encoding.py index 7196fe81..d6b84b61 100644 --- a/bitcoinlib/encoding.py +++ b/bitcoinlib/encoding.py @@ -33,7 +33,7 @@ try: from Crypto.Hash import RIPEMD160 except ImportError as err: - _logger.warning("Could not import RIPEMD160 from cryptodome, will try do use hashlib but this could lead to errors") + _logger.warning("Could not import RIPEMD160 from cryptodome, will try to use hashlib but this could lead to errors") try: from Crypto.Cipher import AES @@ -521,7 +521,7 @@ def addr_base58_to_pubkeyhash(address, as_hex=False): >>> addr_base58_to_pubkeyhash('142Zp9WZn9Fh4MV8F3H5Dv4Rbg7Ja1sPWZ', as_hex=True) '21342f229392d7c9ed82c932916cee6517fbc9a2' - :param address: Crypto currency address in base-58 format + :param address: Cryptocurrency address in base-58 format :type address: str, bytes :param as_hex: Output as hexstring :type as_hex: bool @@ -772,7 +772,7 @@ def convertbits(data, frombits, tobits, pad=True): if bits: ret.append((acc << (tobits - bits)) & maxv) elif bits >= frombits or ((acc << (tobits - bits)) & maxv): - return None + raise EncodingError("Invalid padding bits") return ret @@ -884,6 +884,23 @@ def double_sha256(string, as_hex=False): return hashlib.sha256(hashlib.sha256(string).digest()).hexdigest() +def sha256(string, as_hex=False): + """ + Get SHA256 hash of string + + :param string: String to be hashed + :type string: bytes + :param as_hex: Return value as hexadecimal string. Default is False + :type as_hex: bool + + :return bytes, str: + """ + if not as_hex: + return hashlib.sha256(string).digest() + else: + return hashlib.sha256(string).hexdigest() + + def ripemd160(string): try: return RIPEMD160.new(string).digest() @@ -946,91 +963,26 @@ def aes_decrypt(encrypted_data, key): ct = encrypted_data[:-16] tag = encrypted_data[-16:] cipher2 = AES.new(key, AES.MODE_SIV) - return cipher2.decrypt_and_verify(ct, tag) + try: + res = cipher2.decrypt_and_verify(ct, tag) + except ValueError as e: + raise EncodingError("Could not decrypt value (password incorrect?): %s" % e) + return res -def bip38_decrypt(encrypted_privkey, password): +def scrypt_hash(password, salt, key_len=64, N=16384, r=8, p=1, buflen=64): """ - BIP0038 non-ec-multiply decryption. Returns WIF private key. - Based on code from https://github.com/nomorecoin/python-bip38-testing - This method is called by Key class init function when importing BIP0038 key. + Wrapper for Scrypt method for scrypt or Cryptodome library - :param encrypted_privkey: Encrypted private key using WIF protected key format - :type encrypted_privkey: str - :param password: Required password for decryption - :type password: str + For documentation see methods in referring libraries - :return tupple (bytes, bytes): (Private Key bytes, 4 byte address hash for verification) - """ - d = change_base(encrypted_privkey, 58, 256)[2:] - flagbyte = d[0:1] - d = d[1:] - if flagbyte == b'\xc0': - compressed = False - elif flagbyte == b'\xe0': - compressed = True - else: - raise EncodingError("Unrecognised password protected key format. Flagbyte incorrect.") - if isinstance(password, str): - password = password.encode('utf-8') - addresshash = d[0:4] - d = d[4:-4] - try: - key = scrypt(password, addresshash, 64, 16384, 8, 8) - except Exception: - key = scrypt.hash(password, addresshash, 16384, 8, 8, 64) - derivedhalf1 = key[0:32] - derivedhalf2 = key[32:64] - encryptedhalf1 = d[0:16] - encryptedhalf2 = d[16:32] - # aes = pyaes.AESModeOfOperationECB(derivedhalf2) - aes = AES.new(derivedhalf2, AES.MODE_ECB) - decryptedhalf2 = aes.decrypt(encryptedhalf2) - decryptedhalf1 = aes.decrypt(encryptedhalf1) - priv = decryptedhalf1 + decryptedhalf2 - priv = (int.from_bytes(priv, 'big') ^ int.from_bytes(derivedhalf1, 'big')).to_bytes(32, 'big') - # if compressed: - # # FIXME: This works but does probably not follow the BIP38 standards (was before: priv = b'\0' + priv) - # priv += b'\1' - return priv, addresshash, compressed - - -def bip38_encrypt(private_hex, address, password, flagbyte=b'\xe0'): """ - BIP0038 non-ec-multiply encryption. Returns BIP0038 encrypted private key - Based on code from https://github.com/nomorecoin/python-bip38-testing - - :param private_hex: Private key in hex format - :type private_hex: str - :param address: Address string - :type address: str - :param password: Required password for encryption - :type password: str - :param flagbyte: Flagbyte prefix for WIF - :type flagbyte: bytes + try: # Try scrypt from Cryptodome + key = scrypt(password, salt, key_len, N, r, p) + except TypeError: # Use scrypt module + key = scrypt.hash(password, salt, N, r, p, key_len) + return key - :return str: BIP38 password encrypted private key - """ - if isinstance(address, str): - address = address.encode('utf-8') - if isinstance(password, str): - password = password.encode('utf-8') - addresshash = double_sha256(address)[0:4] - try: - key = scrypt(password, addresshash, 64, 16384, 8, 8) - except Exception: - key = scrypt.hash(password, addresshash, 16384, 8, 8, 64) - derivedhalf1 = key[0:32] - derivedhalf2 = key[32:64] - aes = AES.new(derivedhalf2, AES.MODE_ECB) - # aes = pyaes.AESModeOfOperationECB(derivedhalf2) - encryptedhalf1 = \ - aes.encrypt((int(private_hex[0:32], 16) ^ int.from_bytes(derivedhalf1[0:16], 'big')).to_bytes(16, 'big')) - encryptedhalf2 = \ - aes.encrypt((int(private_hex[32:64], 16) ^ int.from_bytes(derivedhalf1[16:32], 'big')).to_bytes(16, 'big')) - encrypted_privkey = b'\x01\x42' + flagbyte + addresshash + encryptedhalf1 + encryptedhalf2 - encrypted_privkey += double_sha256(encrypted_privkey)[:4] - return base58encode(encrypted_privkey) class Quantity: diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index 79afd289..d0ea8eaf 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -37,7 +37,6 @@ from fastecdsa import point as fastecdsa_point else: import ecdsa - secp256k1_curve = ecdsa.ellipticcurve.CurveFp(secp256k1_p, secp256k1_a, secp256k1_b) secp256k1_generator = ecdsa.ellipticcurve.Point(secp256k1_curve, secp256k1_Gx, secp256k1_Gy, secp256k1_n) @@ -128,7 +127,7 @@ def get_key_format(key, is_private=None): key_format = "" networks = None script_types = [] - witness_types = ['legacy'] + witness_types = [DEFAULT_WITNESS_TYPE] multisig = [False] # if isinstance(key, bytes) and len(key) in [128, 130]: @@ -150,6 +149,9 @@ def get_key_format(key, is_private=None): elif isinstance(key, bytes) and len(key) == 32: key_format = 'bin' is_private = True + elif isinstance(key, tuple): + key_format = 'point' + is_private = False elif len(key) == 130 and key[:2] == '04' and not is_private: key_format = 'public_uncompressed' is_private = False @@ -293,6 +295,7 @@ def deserialize_address(address, encoding=None, network=None): 'script_type': script_type, 'witness_type': witness_type, 'networks': networks, + 'checksum': checksum, 'witver': None, } if encoding == 'bech32' or encoding is None: @@ -317,6 +320,7 @@ def deserialize_address(address, encoding=None, network=None): 'script_type': script_type, 'witness_type': witness_type, 'networks': networks, + 'checksum': addr_bech32_checksum(address), 'witver': witver, } except EncodingError as err: @@ -355,7 +359,7 @@ def addr_convert(addr, prefix, encoding=None, to_encoding=None): return pubkeyhash_to_addr(pkh, prefix=prefix, encoding=to_encoding) -def path_expand(path, path_template=None, level_offset=None, account_id=0, cosigner_id=0, purpose=44, +def path_expand(path, path_template=None, level_offset=None, account_id=0, cosigner_id=0, purpose=84, address_index=0, change=0, witness_type=DEFAULT_WITNESS_TYPE, multisig=False, network=DEFAULT_NETWORK): """ Create key path. Specify part of key path and path settings @@ -391,11 +395,7 @@ def path_expand(path, path_template=None, level_offset=None, account_id=0, cosig if isinstance(path, TYPE_TEXT): path = path.split('/') if not path_template: - 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 ks: - purpose = ks[0]['purpose'] - path_template = ks[0]['key_path'] + path_template, purpose, _ = get_key_structure_data(witness_type, multisig) if not isinstance(path, list): raise BKeyError("Please provide path as list with at least 1 item. Wallet key path format is %s" % path_template) @@ -456,39 +456,309 @@ def path_expand(path, path_template=None, level_offset=None, account_id=0, cosig return npath -class Address(object): +def bip38_decrypt(encrypted_privkey, password): """ - Class to store, convert and analyse various address types as representation of public keys or scripts hashes + BIP0038 non-ec-multiply decryption. Returns WIF private key. + Based on code from https://github.com/nomorecoin/python-bip38-testing + This method is called by Key class init function when importing BIP0038 key. + + :param encrypted_privkey: Encrypted private key using WIF protected key format + :type encrypted_privkey: str + :param password: Required password for decryption + :type password: str + + :return tuple (bytes, bytes, boolean, dict): (Private Key bytes, 4 byte address hash for verification, compressed?, dictionary with additional info) """ + d = change_base(encrypted_privkey, 58, 256) + identifier = d[0:2] + flagbyte = d[2:3] + address_hash: bytes = d[3:7] + if identifier == BIP38_EC_MULTIPLIED_PRIVATE_KEY_PREFIX: + owner_entropy: bytes = d[7:15] + encrypted_half_1_half_1: bytes = d[15:23] + encrypted_half_2: bytes = d[23:-4] + + lot_and_sequence = None + if flagbyte in [BIP38_MAGIC_LOT_AND_SEQUENCE_UNCOMPRESSED_FLAG, BIP38_MAGIC_LOT_AND_SEQUENCE_COMPRESSED_FLAG, + b'\x0c', b'\x14', b'\x1c', b'\x2c', b'\x34', b'\x3c']: + owner_salt: bytes = owner_entropy[:4] + lot_and_sequence = owner_entropy[4:] + else: + owner_salt: bytes = owner_entropy + + pass_factor = scrypt_hash(password, owner_salt, 32, 16384, 8, 8) + if lot_and_sequence: + pass_factor: bytes = double_sha256(pass_factor + owner_entropy) + if int.from_bytes(pass_factor, 'big') == 0 or int.from_bytes(pass_factor, 'big') >= secp256k1_n: + raise ValueError("Invalid EC encrypted WIF (Wallet Import Format)") + + pre_public_key = HDKey(pass_factor).public_byte + salt = address_hash + owner_entropy + encrypted_seed_b: bytes = scrypt_hash(pre_public_key, salt, 64, 1024, 1, 1) + key: bytes = encrypted_seed_b[32:] + + aes = AES.new(key, AES.MODE_ECB) + encrypted_half_1_half_2_seed_b_last_3 = ( + int.from_bytes(aes.decrypt(encrypted_half_2), 'big') ^ + int.from_bytes(encrypted_seed_b[16:32], 'big')).to_bytes(16, 'big') + encrypted_half_1_half_2: bytes = encrypted_half_1_half_2_seed_b_last_3[:8] + encrypted_half_1: bytes = ( + encrypted_half_1_half_1 + encrypted_half_1_half_2 + ) + + seed_b: bytes = (( + int.from_bytes(aes.decrypt(encrypted_half_1), 'big') ^ + int.from_bytes(encrypted_seed_b[:16], 'big')).to_bytes(16, 'big') + + encrypted_half_1_half_2_seed_b_last_3[8:]) + + factor_b: bytes = double_sha256(seed_b) + if int.from_bytes(factor_b, 'big') == 0 or int.from_bytes(factor_b, 'big') >= secp256k1_n: + raise ValueError("Invalid EC encrypted WIF (Wallet Import Format)") + + private_key = HDKey(pass_factor) * HDKey(factor_b) + compressed = False + public_key = private_key.public_uncompressed_hex + if flagbyte in [BIP38_MAGIC_NO_LOT_AND_SEQUENCE_COMPRESSED_FLAG, BIP38_MAGIC_LOT_AND_SEQUENCE_COMPRESSED_FLAG, + b'\x28', b'\x2c', b'\x30', b'\x34', b'\x38', b'\x3c', b'\xe0', b'\xe8', b'\xf0', b'\xf8']: + public_key: str = private_key.public_compressed_hex + compressed = True + + address = private_key.address(compressed=compressed) + address_hash_check = double_sha256(bytes(address, 'utf8'))[:4] + if address_hash_check != address_hash: + raise ValueError("Address hash has invalid checksum") + wif = private_key.wif() + lot = None + sequence = None + if lot_and_sequence: + sequence = int.from_bytes(lot_and_sequence, 'big') % 4096 + lot = int.from_bytes(lot_and_sequence, 'big') // 4096 + + retdict = dict( + wif=wif, + private_key=private_key.private_hex, + public_key=public_key, + seed=seed_b.hex(), + address=address, + lot=lot, + sequence=sequence + ) + return private_key.private_byte, address_hash, compressed, retdict + elif identifier == BIP38_NO_EC_MULTIPLIED_PRIVATE_KEY_PREFIX: + d = d[3:] + if flagbyte == b'\xc0': + compressed = False + elif flagbyte == b'\xe0' or flagbyte == b'\x20': + compressed = True + else: + raise EncodingError("Unrecognised password protected key format. Flagbyte incorrect.") + if isinstance(password, str): + password = password.encode('utf-8') + addresshash = d[0:4] + d = d[4:-4] + + key = scrypt_hash(password, addresshash, 64, 16384, 8, 8) + derivedhalf1 = key[0:32] + derivedhalf2 = key[32:64] + encryptedhalf1 = d[0:16] + encryptedhalf2 = d[16:32] + + # aes = pyaes.AESModeOfOperationECB(derivedhalf2) + aes = AES.new(derivedhalf2, AES.MODE_ECB) + decryptedhalf2 = aes.decrypt(encryptedhalf2) + decryptedhalf1 = aes.decrypt(encryptedhalf1) + priv = decryptedhalf1 + decryptedhalf2 + priv = (int.from_bytes(priv, 'big') ^ int.from_bytes(derivedhalf1, 'big')).to_bytes(32, 'big') + return priv, addresshash, compressed, {} + else: + raise EncodingError("Unknown BIP38 identifier, value must be 0x0142 (non-EC-multiplied) or " + "0x0143 (EC-multiplied)") - @classmethod - @deprecated - def import_address(cls, address, compressed=None, encoding=None, depth=None, change=None, - address_index=None, network=None, network_overrides=None): - """ - Import an address to the Address class. Specify network if available, otherwise it will be - derived form the address. - :param address: Address to import - :type address: str - :param compressed: Is key compressed or not, default is None - :type compressed: bool - :param encoding: Address encoding. Default is base58 encoding, for native segwit addresses specify bech32 encoding. Leave empty to derive from address - :type encoding: str - :param depth: Level of depth in BIP32 key path - :type depth: int - :param change: Use 0 for normal address/key, and 1 for change address (for returned/change payments) - :type change: int - :param address_index: Index of address. Used in BIP32 key paths - :type address_index: int - :param network: Specify network filter, i.e.: bitcoin, testnet, litecoin, etc. Wil trigger check if address is valid for this network - :type network: str - :param network_overrides: Override network settings for specific prefixes, i.e.: {"prefix_address_p2sh": "32"}. Used by settings in providers.json - :type network_overrides: dict +def bip38_encrypt(private_hex, address, password, flagbyte=b'\xe0'): + """ + BIP0038 non-ec-multiply encryption. Returns BIP0038 encrypted private key + Based on code from https://github.com/nomorecoin/python-bip38-testing - :return Address: - """ - return cls.parse(address, compressed, encoding, depth, change, address_index, network, network_overrides) + :param private_hex: Private key in hex format + :type private_hex: str + :param address: Address string + :type address: str + :param password: Required password for encryption + :type password: str + :param flagbyte: Flagbyte prefix for WIF + :type flagbyte: bytes + + :return str: BIP38 password encrypted private key + """ + if isinstance(address, str): + address = address.encode('utf-8') + if isinstance(password, str): + password = password.encode('utf-8') + addresshash = double_sha256(address)[0:4] + key = scrypt_hash(password, addresshash, 64, 16384, 8, 8) + derivedhalf1 = key[0:32] + derivedhalf2 = key[32:64] + aes = AES.new(derivedhalf2, AES.MODE_ECB) + # aes = pyaes.AESModeOfOperationECB(derivedhalf2) + encryptedhalf1 = \ + aes.encrypt((int(private_hex[0:32], 16) ^ int.from_bytes(derivedhalf1[0:16], 'big')).to_bytes(16, 'big')) + encryptedhalf2 = \ + aes.encrypt((int(private_hex[32:64], 16) ^ int.from_bytes(derivedhalf1[16:32], 'big')).to_bytes(16, 'big')) + encrypted_privkey = b'\x01\x42' + flagbyte + addresshash + encryptedhalf1 + encryptedhalf2 + encrypted_privkey += double_sha256(encrypted_privkey)[:4] + return base58encode(encrypted_privkey) + + +def bip38_intermediate_password(passphrase, lot=None, sequence=None, owner_salt=os.urandom(8)): + """ + Intermediate passphrase generator for EC multiplied BIP38 encrypted private keys. + Source: https://github.com/meherett/python-bip38/blob/master/bip38/bip38.py + + Use intermediate password to create a encrypted WIF key with the :func:`bip38_create_new_encrypted_wif` method. + + :param passphrase: Passphrase or password text + :type passphrase: str + :param lot: Lot number between 100000 <= lot <= 999999 range, default to ``None`` + :type lot: int + :param sequence: Sequence number between 0 <= sequence <= 4095 range, default to ``None`` + :type sequence: int + :param owner_salt: Owner salt, default to ``os.urandom(8)`` + :type owner_salt: str, bytes + + :returns str: Intermediate passphrase + + >>> bip38_intermediate_password(passphrase="TestingOneTwoThree", lot=199999, sequence=1, owner_salt="75ed1cdeb254cb38") + 'passphraseb7ruSN4At4Rb8hPTNcAVezfsjonvUs4Qo3xSp1fBFsFPvVGSbpP2WTJMhw3mVZ' + + """ + + owner_salt = to_bytes(owner_salt) + if len(owner_salt) not in [4, 8]: + raise ValueError(f"Invalid owner salt length (expected: 4 or 8 bytes, got: {len(owner_salt)})") + if len(owner_salt) == 4 and (not lot or not sequence): + raise ValueError(f"Invalid owner salt length for non lot/sequence (expected: 8 bytes, got:" + f" {len(owner_salt)})") + if (lot and not sequence) or (not lot and sequence): + raise ValueError(f"Both lot & sequence are required, got: (lot {lot}) (sequence {sequence})") + + if lot and sequence: + lot, sequence = int(lot), int(sequence) + if not 100000 <= lot <= 999999: + raise ValueError(f"Invalid lot, (expected: 100000 <= lot <= 999999, got: {lot})") + if not 0 <= sequence <= 4095: + raise ValueError(f"Invalid lot, (expected: 0 <= sequence <= 4095, got: {sequence})") + + pre_factor = scrypt_hash(unicodedata.normalize("NFC", passphrase), owner_salt[:4], 32, 16384, 8, 8) + owner_entropy = owner_salt[:4] + int.to_bytes((lot * 4096 + sequence), 4, 'big') + if isinstance(pre_factor, list): + for pf in pre_factor: + print(pf.hex()) + print(len(pre_factor)) + pass_factor = double_sha256(pre_factor + owner_entropy) + magic = BIP38_MAGIC_LOT_AND_SEQUENCE + else: + pass_factor = scrypt_hash(unicodedata.normalize("NFC", passphrase), owner_salt, 32, 16384, 8, 8) + magic = BIP38_MAGIC_NO_LOT_AND_SEQUENCE + owner_entropy = owner_salt + + return pubkeyhash_to_addr_base58(magic + owner_entropy + HDKey(pass_factor).public_byte, prefix=b'') + + +def bip38_create_new_encrypted_wif(intermediate_passphrase, compressed=True, seed=os.urandom(24), + network=DEFAULT_NETWORK): + """ + Create new encrypted WIF BIP38 EC multiplied key. Use :func:`bip38_intermediate_password` to create a + intermediate passphrase first. + + :param intermediate_passphrase: Intermediate passphrase text + :type intermediate_passphrase: str + :param compressed: Compressed or uncompressed key + :type compressed: boolean + :param seed: Seed, default to ``os.urandom(24)`` + :type seed: str, bytes + :param network: Network name + :type network: str + + :returns dict: Dictionary with encrypted WIF key and confirmation code + + """ + + seed_b = to_bytes(seed) + intermediate_password_bytes = change_base(intermediate_passphrase,58, 256) + check = intermediate_password_bytes[-4:] + intermediate_decode = intermediate_password_bytes[:-4] + checksum = double_sha256(intermediate_decode)[0:4] + assert (check == checksum), "Invalid address, checksum incorrect" + if len(intermediate_decode) != 49: + raise ValueError(f"Invalid intermediate passphrase length (expected: 49, got: {len(intermediate_decode)})") + + magic: bytes = intermediate_decode[:8] + owner_entropy: bytes = intermediate_decode[8:16] + pass_point: bytes = intermediate_decode[16:] + + if magic == BIP38_MAGIC_LOT_AND_SEQUENCE: + if compressed: + flag: bytes = BIP38_MAGIC_LOT_AND_SEQUENCE_COMPRESSED_FLAG + else: + flag: bytes = BIP38_MAGIC_LOT_AND_SEQUENCE_UNCOMPRESSED_FLAG + elif magic == BIP38_MAGIC_NO_LOT_AND_SEQUENCE: + if compressed: + flag: bytes = BIP38_MAGIC_NO_LOT_AND_SEQUENCE_COMPRESSED_FLAG + else: + flag: bytes = BIP38_MAGIC_NO_LOT_AND_SEQUENCE_UNCOMPRESSED_FLAG + else: + raise ValueError("Invalid magic bytes, check BIP38 constants") + + factor_b: bytes = double_sha256(seed_b) + if not 0 < int.from_bytes(factor_b, 'big') < secp256k1_n: + raise ValueError("Invalid EC encrypted WIF (Wallet Import Format)") + + pk_point = ec_point_multiplication(HDKey(pass_point).public_point(), int.from_bytes(factor_b, 'big')) + k = HDKey((pk_point[0], pk_point[1]), compressed=compressed, witness_type='legacy', network=network) + public_key = k.public_hex + address = k.address() + address_hash = double_sha256(bytes(address, 'utf8'))[:4] + + salt: bytes = address_hash + owner_entropy + scrypt_hash_bytes: bytes = scrypt_hash(pass_point, salt, 64, 1024, 1, 1) + derived_half_1, derived_half_2, key = scrypt_hash_bytes[:16], scrypt_hash_bytes[16:32], scrypt_hash_bytes[32:] + + aes = AES.new(key, AES.MODE_ECB) + encrypted_half_1 = \ + aes.encrypt((int.from_bytes(seed_b[:16], 'big') ^ int.from_bytes(derived_half_1, 'big')).to_bytes(16, 'big')) + encrypted_half_2 = \ + aes.encrypt((int.from_bytes((encrypted_half_1[8:] + seed_b[16:]), 'big') ^ + int.from_bytes(derived_half_2,'big')).to_bytes(16, 'big')) + encrypted_wif = pubkeyhash_to_addr_base58(flag + address_hash + owner_entropy + encrypted_half_1[:8] + + encrypted_half_2, prefix=BIP38_EC_MULTIPLIED_PRIVATE_KEY_PREFIX) + + point_b = HDKey(factor_b).public_byte + point_b_prefix = (int.from_bytes(scrypt_hash_bytes[63:], 'big') & 1 ^ + int.from_bytes(point_b[:1], 'big')).to_bytes(1, 'big') + point_b_half_1 = aes.encrypt((int.from_bytes(point_b[1:17], 'big') ^ + int.from_bytes(derived_half_1, 'big')).to_bytes(16, 'big')) + point_b_half_2 = aes.encrypt((int.from_bytes(point_b[17:], 'big') ^ + int.from_bytes(derived_half_2, 'big')).to_bytes(16, 'big')) + encrypted_point_b = point_b_prefix + point_b_half_1 + point_b_half_2 + confirmation_code = pubkeyhash_to_addr_base58(flag + address_hash + owner_entropy + encrypted_point_b, + prefix=BIP38_CONFIRMATION_CODE_PREFIX) + + return dict( + encrypted_wif=encrypted_wif, + confirmation_code=confirmation_code, + public_key=public_key, + seed=seed_b, + compressed=compressed, + address=address + ) + + + +class Address(object): + """ + Class to store, convert and analyse various address types as representation of public keys or scripts hashes + """ @classmethod def parse(cls, address, compressed=None, encoding=None, depth=None, change=None, @@ -528,9 +798,10 @@ def parse(cls, address, compressed=None, encoding=None, depth=None, change=None, if network is None: network = addr_dict['network'] script_type = addr_dict['script_type'] + witness_type = addr_dict['witness_type'] return Address(hashed_data=public_key_hash_bytes, prefix=prefix, script_type=script_type, - compressed=compressed, encoding=addr_dict['encoding'], depth=depth, change=change, - address_index=address_index, network=network, network_overrides=network_overrides) + witness_type=witness_type, compressed=compressed, encoding=addr_dict['encoding'], depth=depth, + change=change, address_index=address_index, network=network, network_overrides=network_overrides) def __init__(self, data='', hashed_data='', prefix=None, script_type=None, compressed=None, encoding=None, witness_type=None, witver=0, depth=None, change=None, @@ -581,16 +852,21 @@ def __init__(self, data='', hashed_data='', prefix=None, script_type=None, elif self.script_type == 'p2tr': witness_type = 'taproot' self.witver = 1 if self.witver == 0 else self.witver + elif self.encoding == 'base58': + witness_type = 'legacy' + else: + witness_type = 'segwit' self.witness_type = witness_type self.depth = depth self.change = change self.address_index = address_index if self.encoding is None: - if self.script_type in ['p2wpkh', 'p2wsh', 'p2tr'] or self.witness_type == 'segwit': - self.encoding = 'bech32' - else: + if (self.script_type in ['p2pkh', 'p2sh', 'multisig', 'p2pk'] or self.witness_type == 'legacy' or + self.witness_type == 'p2sh-segwit'): self.encoding = 'base58' + else: + self.encoding = 'bech32' self.hash_bytes = to_bytes(hashed_data) self.prefix = prefix self.redeemscript = b'' @@ -737,7 +1013,7 @@ def __init__(self, import_key=None, network=None, compressed=True, password='', 12127227708610754620337553985245292396444216111803695028419544944213442390363 :param import_key: If specified import given private or public key. If not specified a new private key is generated. - :type import_key: str, int, bytes + :type import_key: str, int, bytes, tuple :param network: Bitcoin, testnet, litecoin or other network :type network: str, Network :param compressed: Is key compressed or not, default is True @@ -807,32 +1083,40 @@ def __init__(self, import_key=None, network=None, compressed=True, password='', if not self.is_private: self.secret = None - pub_key = to_hexstring(import_key) - if len(pub_key) == 130: - self._public_uncompressed_hex = pub_key - self.x_hex = pub_key[2:66] - self.y_hex = pub_key[66:130] - self._y = int(self.y_hex, 16) - self.compressed = False - if self._y % 2: - prefix = '03' - else: - prefix = '02' - self.public_hex = pub_key + if self.key_format == 'point': + self.compressed = compressed + self._x = import_key[0] + self._y = import_key[1] + self.x_bytes = self._x.to_bytes(32, 'big') + self.y_bytes = self._y.to_bytes(32, 'big') + self.x_hex = self.x_bytes.hex() + self.y_hex = self.y_bytes.hex() + prefix = '03' if self._y % 2 else '02' + self._public_uncompressed_hex = '04' + self.x_hex + self.y_hex self.public_compressed_hex = prefix + self.x_hex + self.public_hex = self.public_compressed_hex if compressed else self._public_uncompressed_hex else: - self.public_hex = pub_key - self.x_hex = pub_key[2:66] - self.compressed = True - self._x = int(self.x_hex, 16) - self.public_compressed_hex = pub_key + pub_key = to_hexstring(import_key) + if len(pub_key) == 130: + self._public_uncompressed_hex = pub_key + self.x_hex = pub_key[2:66] + self.y_hex = pub_key[66:130] + self._y = int(self.y_hex, 16) + self.compressed = False + prefix = '03' if self._y % 2 else '02' + self.public_hex = pub_key + self.public_compressed_hex = prefix + self.x_hex + else: + self.public_hex = pub_key + self.x_hex = pub_key[2:66] + self.compressed = True + self._x = int(self.x_hex, 16) + self.public_compressed_hex = pub_key self.public_compressed_byte = bytes.fromhex(self.public_compressed_hex) if self._public_uncompressed_hex: self._public_uncompressed_byte = bytes.fromhex(self._public_uncompressed_hex) - if self.compressed: - self.public_byte = self.public_compressed_byte - else: - self.public_byte = self.public_uncompressed_byte + self.public_byte = self.public_compressed_byte if self.compressed else self.public_uncompressed_byte + elif self.is_private and self.key_format == 'decimal': self.secret = int(import_key) self.private_hex = change_base(self.secret, 10, 16, 64) @@ -930,10 +1214,55 @@ def __bytes__(self): return self.public_byte def __add__(self, other): - return self.public_byte + other + """ + Scalar addition over secp256k1 order of 2 keys secrets. Returns a new private key with network and compressed + attributes from first key. - def __radd__(self, other): - return other + self.public_byte + :param other: Private Key class + :type other: Key + + :return: Key + """ + assert self.is_private + assert isinstance(other, Key) + assert other.is_private + return Key((self.secret + other.secret) % secp256k1_n, self.network, self.compressed) + + def __sub__(self, other): + """ + Scalar substraction over secp256k1 order of 2 keys secrets. Returns a new private key with network and + compressed attributes from first key. + + :param other: Private Key class + :type other: Key + + :return: Key + """ + assert self.is_private + assert isinstance(other, Key) + assert other.is_private + return Key((self.secret - other.secret) % secp256k1_n, self.network, self.compressed) + + def __mul__(self, other): + """ + Scalar multiplication over secp256k1 order of 2 keys secrets. Returns a new private key with network and + compressed attributes from first key. + + :param other: Private Key class + :type other: Key + + :return: Key + """ + assert isinstance(other, Key) + assert self.secret + assert other.is_private + return Key((self.secret * other.secret) % secp256k1_n, self.network, self.compressed) + + def __rmul__(self, other): + return self * other + + def __neg__(self): + return self.inverse() def __len__(self): return len(self.public_byte) @@ -958,6 +1287,18 @@ def __int__(self): else: return None + def inverse(self): + """ + Return inverse of private or public key + + :return Key: + """ + if self.is_private: + return Key(secp256k1_n - self.secret, network=self.network, compressed=self.compressed) + else: + # Inverse y in init: self._y = secp256k1_p - self._y + return Key(('02' if self._y % 2 else '03') + self.x_hex, network=self.network, compressed=self.compressed) + @property def x(self): if not self._x and self.x_hex: @@ -994,6 +1335,32 @@ def public_uncompressed_byte(self): def hex(self): return self.public_hex + def as_hex(self, private=False): + """ + Return hex representation of private or public key + + :param private: Private or public key + + :return str: + """ + if private: + return self.private_byte + else: + return self.public_hex + + def as_bytes(self, private=False): + """ + Return bytes representation of private or public key + + :param private: Private or public key + + :return bytes: + """ + if private: + return self.private_byte + else: + return self.public_byte + def as_dict(self, include_private=False): """ Get current Key class as dictionary. Byte values are represented by hexadecimal strings. @@ -1047,7 +1414,7 @@ def _bip38_decrypt(encrypted_privkey, password, network=DEFAULT_NETWORK): :return str: Private Key WIF """ - priv, addresshash, compressed = bip38_decrypt(encrypted_privkey, password) + priv, addresshash, compressed, _ = bip38_decrypt(encrypted_privkey, password) # Verify addresshash k = Key(priv, compressed=compressed, network=network) @@ -1076,10 +1443,6 @@ def encrypt(self, password): flagbyte = b'\xe0' if self.compressed else b'\xc0' return bip38_encrypt(self.private_hex, self.address(), password, flagbyte) - @deprecated - def bip38_encrypt(self, password): - return self.encrypt(password) - def wif(self, prefix=None): """ Get private Key in Wallet Import Format, steps: @@ -1370,7 +1733,7 @@ def __init__(self, import_key=None, key=None, chain=None, depth=0, parent_finger :param import_key: HD Key to import in WIF format or as byte with key (32 bytes) and chain (32 bytes) - :type import_key: str, bytes, int + :type import_key: str, bytes, int, tuple :param key: Private or public key (length 32) :type key: bytes :param chain: A chain code (length 32) @@ -1401,9 +1764,7 @@ def __init__(self, import_key=None, key=None, chain=None, depth=0, parent_finger :return HDKey: """ - if not encoding and witness_type: - encoding = get_encoding_from_witness(witness_type) - self.script_type = script_type_default(witness_type, multisig) + script_type = None # if (key and not chain) or (not key and chain): # raise BKeyError("Please specify both key and chain, use import_key attribute " @@ -1431,10 +1792,9 @@ def __init__(self, import_key=None, key=None, chain=None, depth=0, parent_finger if kf['format'] == 'address': raise BKeyError("Can not create HDKey object from address") if len(kf['script_types']) == 1: - self.script_type = kf['script_types'][0] + script_type = kf['script_types'][0] if len(kf['witness_types']) == 1 and not witness_type: witness_type = kf['witness_types'][0] - encoding = get_encoding_from_witness(witness_type) if len(kf['multisig']) == 1: multisig = kf['multisig'][0] network = Network(check_network_and_key(import_key, network, kf["networks"])) @@ -1464,6 +1824,9 @@ def __init__(self, import_key=None, key=None, chain=None, depth=0, parent_finger if witness_type is None: witness_type = DEFAULT_WITNESS_TYPE + self.script_type = script_type if script_type else script_type_default(witness_type, multisig) + if not encoding: + encoding = get_encoding_from_witness(witness_type) Key.__init__(self, key, network, compressed, password, is_private) @@ -1481,6 +1844,26 @@ def __repr__(self): return "" % \ (self.public_hex, self.wif_public(), self.network.name) + def __neg__(self): + return self.inverse() + + def inverse(self): + """ + Return inverse of private or public key + + :return Key: + """ + if self.is_private: + return HDKey(secp256k1_n - self.secret, network=self.network.name, compressed=self.compressed, + witness_type=self.witness_type, multisig=self.multisig, encoding=self.encoding) + else: + # Inverse y in init: self._y = secp256k1_p - self._y + if not self.compressed: + return self + return HDKey(('02' if self._y % 2 else '03') + self.x_hex, network=self.network.name, + compressed=self.compressed, witness_type=self.witness_type, multisig=self.multisig, + encoding=self.encoding) + def info(self): """ Prints key information to standard output @@ -1577,7 +1960,7 @@ def _bip38_decrypt(encrypted_privkey, password, network=DEFAULT_NETWORK, witness :return str: Private Key WIF """ - priv, addresshash, compressed = bip38_decrypt(encrypted_privkey, password) + priv, addresshash, compressed, _ = bip38_decrypt(encrypted_privkey, password) # compressed = True if priv[-1:] == b'\1' else False # Verify addresshash @@ -1784,14 +2167,8 @@ def public_master(self, account_id=0, purpose=None, multisig=None, witness_type= self.multisig = multisig if witness_type: self.witness_type = witness_type - 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 BKeyError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for " - "witness_type - multisig combination") - if ks and not purpose: - purpose = ks[0]['purpose'] - path_template = ks[0]['key_path'] + + path_template, purpose, _ = get_key_structure_data(self.witness_type, self.multisig, purpose) # Use last hardened key as public master root pm_depth = path_template.index([x for x in path_template if x[-1:] == "'"][-1]) + 1 @@ -2009,25 +2386,7 @@ def parse_bytes(signature, public_key=None): hash_type=hash_type) @staticmethod - @deprecated - def from_str(signature, public_key=None): - """ - Create a signature from signature string with r and s part. Signature length must be 64 bytes or 128 - character hexstring - - :param signature: Signature string - :type signature: bytes, str - :param public_key: Public key as HDKey or Key object or any other string accepted by HDKey object - :type public_key: HDKey, Key, str, hexstring, bytes - - :return Signature: - """ - - signature = to_bytes(signature) - return Signature(signature, public_key) - - @staticmethod - def create(txid, private, use_rfc6979=True, k=None): + def create(txid, private, use_rfc6979=True, k=None, hash_type=SIGHASH_ALL): """ Sign a transaction hash and create a signature with provided private key. @@ -2049,7 +2408,9 @@ def create(txid, private, use_rfc6979=True, k=None): :type use_rfc6979: bool :param k: Provide own k. Only use for testing or if you know what you are doing. Providing wrong value for k can result in leaking your private key! :type k: int - + :param hash_type: Specific hash type, default is SIGHASH_ALL + :type hash_type: int + :return Signature: """ if isinstance(txid, bytes): @@ -2086,7 +2447,7 @@ def create(txid, private, use_rfc6979=True, k=None): ) if int(s) > secp256k1_n / 2: s = secp256k1_n - int(s) - return Signature(r, s, txid, secret, public_key=pub_key, k=k) + return Signature(r, s, txid, secret, public_key=pub_key, k=k, hash_type=hash_type) else: sk = ecdsa.SigningKey.from_string(private.private_byte, curve=ecdsa.SECP256k1) txid_bytes = to_bytes(txid) @@ -2096,7 +2457,7 @@ def create(txid, private, use_rfc6979=True, k=None): s = int(signature[64:], 16) if s > secp256k1_n / 2: s = secp256k1_n - s - return Signature(r, s, txid, secret, public_key=pub_key, k=k) + return Signature(r, s, txid, secret, public_key=pub_key, k=k, hash_type=hash_type) def __init__(self, r, s, txid=None, secret=None, signature=None, der_signature=None, public_key=None, k=None, hash_type=SIGHASH_ALL): @@ -2233,6 +2594,12 @@ def bytes(self): self._signature = self.r.to_bytes(32, 'big') + self.s.to_bytes(32, 'big') return self._signature + def as_hex(self): + return self.hex() + + def as_bytes(self): + return self.bytes() + def as_der_encoded(self, as_hex=False, include_hash_type=True): """ Get DER encoded signature @@ -2294,7 +2661,7 @@ def verify(self, txid=None, public_key=None): str(secp256k1_Gy) ) else: - transaction_to_sign = to_bytes(self.txid) + transaction_to_sign = bytes.fromhex(self.txid) signature = self.bytes() if len(transaction_to_sign) != 32: transaction_to_sign = double_sha256(transaction_to_sign) @@ -2315,7 +2682,7 @@ def verify(self, txid=None, public_key=None): return True -def sign(txid, private, use_rfc6979=True, k=None): +def sign(txid, private, use_rfc6979=True, k=None, hash_type=SIGHASH_ALL): """ Sign transaction hash or message with secret private key. Creates a signature object. @@ -2335,10 +2702,12 @@ def sign(txid, private, use_rfc6979=True, k=None): :type use_rfc6979: bool :param k: Provide own k. Only use for testing or if you know what you are doing. Providing wrong value for k can result in leaking your private key! :type k: int - + :param hash_type: Specific hash type, default is SIGHASH_ALL + :type hash_type: int + :return Signature: """ - return Signature.create(txid, private, use_rfc6979, k) + return Signature.create(txid, private, use_rfc6979, k, hash_type=hash_type) def verify(txid, signature, public_key=None): @@ -2383,8 +2752,29 @@ def ec_point(m): return fastecdsa_keys.get_public_key(m, fastecdsa_secp256k1) else: point = secp256k1_generator - point *= m - return point + return point * m + + +def ec_point_multiplication(p, m): + """ + Method for elliptic curve multiplication on the secp256k1 curve. Multiply Generator point G by m + + :param p: Point on SECP256k1 curve + :type p: tuple + :param m: A scalar multiplier + :type m: int + + :return tuple: Generator point G multiplied by m as tuple in (x, y) format + """ + m = int(m) + if USE_FASTECDSA: + point = fastecdsa_point.Point(p[0], p[1], fastecdsa_secp256k1) + point_m = point * m + return (point_m.x, point_m.y) + else: + point = ecdsa.ellipticcurve.Point(ecdsa.SECP256k1.curve, p[0], p[1]) + point_m = point * m + return (point_m.x(), point_m.y()) def mod_sqrt(a): diff --git a/bitcoinlib/main.py b/bitcoinlib/main.py index 17beafd3..d1172929 100644 --- a/bitcoinlib/main.py +++ b/bitcoinlib/main.py @@ -105,6 +105,38 @@ def get_encoding_from_witness(witness_type=None): raise ValueError("Unknown witness type %s" % witness_type) +def get_key_structure_data(witness_type, multisig=False, purpose=None, encoding=None): + """ + Get data from wallet key structure. Provide witness_type and multisig to determine key path, purpose (BIP44 + reference) and encoding. + + :param witness_type: Witness type used for transaction validation + :type witness_type: str + :param multisig: Multisig or single keys wallet, default is False: single key / 1-of-1 wallet + :type multisig: bool + :param purpose: Overrule purpose found in wallet structure. Do not use unless you known what you are doing. + :type purpose: int + :param encoding: Overrule encoding found in wallet structure. Do not use unless you known what you are doing. + :type encoding: str + + :return: (key_path, purpose, encoding) + """ + if not witness_type: + return None, 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 ValueError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for " + "witness_type - multisig combination: %s, %s" % (witness_type, multisig)) + if not ks: + raise ValueError("Please check definitions in WALLET_KEY_STRUCTURES. No options found for " + "witness_type - multisig combination: %s, %s" % (witness_type, multisig)) + purpose = ks[0]['purpose'] if not purpose else purpose + path_template = ks[0]['key_path'] + encoding = ks[0]['encoding'] if not encoding else encoding + return path_template, purpose, encoding + + def deprecated(func): """ This is a decorator which can be used to mark functions as deprecated. It will result in a warning being emitted when the function is used. diff --git a/bitcoinlib/networks.py b/bitcoinlib/networks.py index b497559d..e8651a98 100644 --- a/bitcoinlib/networks.py +++ b/bitcoinlib/networks.py @@ -157,7 +157,7 @@ def wif_prefix_search(wif, witness_type=None, multisig=None, network=None): Can return multiple items if no network is specified: >>> [nw['network'] for nw in wif_prefix_search('0488ADE4', multisig=True)] - ['bitcoin', 'regtest', 'dash', 'dogecoin'] + ['bitcoin', 'regtest', 'dogecoin'] :param wif: WIF string or prefix as hexadecimal string :type wif: str @@ -198,30 +198,6 @@ def wif_prefix_search(wif, witness_type=None, multisig=None, network=None): return matches -# Replaced by Value class -@deprecated -def print_value(value, network=DEFAULT_NETWORK, rep='string', denominator=1, decimals=None): - """ - Return the value as string with currency symbol - - Wrapper for the Network().print_value method. - - :param value: Value in the smallest denominator such as Satoshi - :type value: int, float - :param network: Network name as string, default is 'bitcoin' - :type network: str - :param rep: Currency representation: 'string', 'symbol', 'none' or your own custom name - :type rep: str - :param denominator: Unit to use in representation. Default is 1. I.e. 1 = 1 BTC, 0.001 = milli BTC / mBTC, 1e-8 = Satoshi's - :type denominator: float - :param decimals: Number of digits after the decimal point, leave empty for automatic determination based on value. Use integer value between 0 and 8 - :type decimals: int - - :return str: - """ - return Network(network_name=network).print_value(value, rep, denominator, decimals) - - class Network(object): """ Network class with all network definitions. @@ -269,47 +245,6 @@ def __eq__(self, other): def __hash__(self): return hash(self.name) - # Replaced by Value class - @deprecated - def print_value(self, value, rep='string', denominator=1, decimals=None): - """ - Return the value as string with currency symbol - - Print value for 100000 satoshi as string in human-readable format - - >>> Network('bitcoin').print_value(100000) - '0.00100000 BTC' - - :param value: Value in the smallest denominator such as Satoshi - :type value: int, float - :param rep: Currency representation: 'string', 'symbol', 'none' or your own custom name - :type rep: str - :param denominator: Unit to use in representation. Default is 1. I.e. 1 = 1 BTC, 0.001 = milli BTC / mBTC - :type denominator: float - :param decimals: Number of digits after the decimal point, leave empty for automatic determination based on value. Use integer value between 0 and 8 - :type decimals: int - - :return str: - """ - if denominator not in NETWORK_DENOMINATORS: - raise NetworkError("Denominator not found in definitions, use one of the following values: %s" % - NETWORK_DENOMINATORS.keys()) - if value is None: - return "" - symb = rep - if rep == 'string': - symb = NETWORK_DENOMINATORS[denominator] + self.currency_code - elif rep == 'symbol': - symb = NETWORK_DENOMINATORS[denominator] + self.currency_symbol - elif rep == 'none': - symb = '' - decimals = decimals if decimals is not None else -int(math.log10(self.denominator / denominator)) - decimals = 0 if decimals < 0 else decimals - decimals = 8 if decimals > 8 else decimals - balance = round(float(value) * self.denominator / denominator, decimals) - format_str = "%%.%df %%s" % decimals - return (format_str % (balance, symb)).rstrip() - def wif_prefix(self, is_private=False, witness_type='legacy', multisig=False): """ Get WIF prefix for this network and specifications in arguments diff --git a/bitcoinlib/scripts.py b/bitcoinlib/scripts.py index 3e442f4b..720218c8 100644 --- a/bitcoinlib/scripts.py +++ b/bitcoinlib/scripts.py @@ -28,35 +28,6 @@ _logger = logging.getLogger(__name__) -SCRIPT_TYPES = { - # : (, , ) - 'p2pkh': ('locking', [op.op_dup, op.op_hash160, 'data', op.op_equalverify, op.op_checksig], [20]), - 'p2pkh_drop': ('locking', ['data', op.op_drop, op.op_dup, op.op_hash160, 'data', op.op_equalverify, op.op_checksig], - [32, 20]), - 'p2sh': ('locking', [op.op_hash160, 'data', op.op_equal], [20]), - 'p2wpkh': ('locking', [op.op_0, 'data'], [20]), - 'p2wsh': ('locking', [op.op_0, 'data'], [32]), - 'p2tr': ('locking', ['op_n', 'data'], [32]), - 'multisig': ('locking', ['op_n', 'key', 'op_n', op.op_checkmultisig], []), - 'p2pk': ('locking', ['key', op.op_checksig], []), - 'nulldata': ('locking', [op.op_return, 'data'], [0]), - 'nulldata_1': ('locking', [op.op_return, op.op_0], []), - 'nulldata_2': ('locking', [op.op_return], []), - 'sig_pubkey': ('unlocking', ['signature', 'key'], []), - # 'p2sh_multisig': ('unlocking', [op.op_0, 'signature', 'op_n', 'key', 'op_n', op.op_checkmultisig], []), - 'p2sh_multisig': ('unlocking', [op.op_0, 'signature', 'redeemscript'], []), - 'p2tr_unlock': ('unlocking', ['data'], [64]), - 'p2sh_multisig_2?': ('unlocking', [op.op_0, 'signature', op.op_verify, 'redeemscript'], []), - 'p2sh_multisig_3?': ('unlocking', [op.op_0, 'signature', op.op_1add, 'redeemscript'], []), - 'p2sh_p2wpkh': ('unlocking', [op.op_0, op.op_hash160, 'redeemscript', op.op_equal], []), - 'p2sh_p2wsh': ('unlocking', [op.op_0, 'redeemscript'], []), - 'signature': ('unlocking', ['signature'], []), - 'signature_multisig': ('unlocking', [op.op_0, 'signature'], []), - 'locktime_cltv': ('unlocking', ['locktime_cltv', op.op_checklocktimeverify, op.op_drop], []), - 'locktime_csv': ('unlocking', ['locktime_csv', op.op_checksequenceverify, op.op_drop], []), -} - - class ScriptError(Exception): """ Handle Key class Exceptions @@ -70,7 +41,7 @@ def __str__(self): return self.msg -def _get_script_types(blueprint): +def _get_script_types(blueprint, is_locking=None): # Convert blueprint to more generic format bp = [] for item in blueprint: @@ -82,11 +53,20 @@ def _get_script_types(blueprint): bp[-1] = 'key' elif item == 'signature' and len(bp) and bp[-1] == 'signature': bp[-1] = 'signature' + elif isinstance(item, list): + bp.append('redeemscript') else: bp.append(item) - script_types = [key for key, values in SCRIPT_TYPES.items() if values[1] == bp] - if script_types: + if is_locking is None: + locktype = ['locking', 'unlocking'] + elif is_locking: + locktype = ['locking'] + else: + locktype = ['unlocking'] + + script_types = [key for key, values in SCRIPT_TYPES.items() if values[1] == bp and values[0] in locktype] + if len(script_types) == 1: return script_types bp_len = [int(c.split('-')[1]) for c in blueprint if isinstance(c, str) and c[:4] == 'data'] @@ -94,7 +74,7 @@ def _get_script_types(blueprint): while len(bp): # Find all possible matches with blueprint matches = [(key, len(values[1]), values[2]) for key, values in SCRIPT_TYPES.items() if - values[1] == bp[:len(values[1])]] + values[1] == bp[:len(values[1])] and values[0] in locktype] if not matches: script_types.append('unknown') break @@ -109,9 +89,10 @@ def _get_script_types(blueprint): match_id = matches.index(match) break - # Add script type to list + # Add script type to list, if script is p2sh embedded multisig set type to p2sh_multisig script_type = matches[match_id][0] - if script_type == 'multisig' and script_types[-1:] == ['signature_multisig']: + if (script_type == 'multisig' or script_type == 'multisig_redeemscript') \ + and script_types[-1:] == ['signature_multisig']: script_types.pop() script_type = 'p2sh_multisig' script_types.append(script_type) @@ -151,12 +132,14 @@ def get_data_type(data): return 'key_object' elif isinstance(data, Signature): return 'signature_object' + elif isinstance(data, list): + return 'redeemscript' elif data.startswith(b'\x30') and 69 <= len(data) <= 74: return 'signature' elif ((data.startswith(b'\x02') or data.startswith(b'\x03')) and len(data) == 33) or \ (data.startswith(b'\x04') and len(data) == 65): return 'key' - elif len(data) == 20 or len(data) == 32 or len(data) == 64 or 1 < len(data) <= 4: + elif len(data) == 20 or len(data) == 32 or len(data) == 64 or 1 <= len(data) <= 4: return 'data-%d' % len(data) else: return 'other' @@ -165,7 +148,7 @@ def get_data_type(data): class Script(object): def __init__(self, commands=None, message=None, script_types='', is_locking=True, keys=None, signatures=None, - blueprint=None, tx_data=None, public_hash=b'', sigs_required=None, redeemscript=b'', + blueprint=None, env_data=None, public_hash=b'', sigs_required=None, redeemscript=b'', hash_type=SIGHASH_ALL): """ Create a Script object with specified parameters. Use parse() method to create a Script from raw hex @@ -182,6 +165,12 @@ def __init__(self, commands=None, message=None, script_types='', is_locking=True >>> s.stack [] + >>> key1 = '5JruagvxNLXTnkksyLMfgFgf3CagJ3Ekxu5oGxpTm5mPfTAPez3' + >>> key2 = '5JX3qAwDEEaapvLXRfbXRMSiyRgRSW9WjgxeyJQWwBugbudCwsk' + >>> key3 = '5JjHVMwJdjPEPQhq34WMUhzLcEd4SD7HgZktEh8WHstWcCLRceV' + >>> keylist = [Key(k) for k in [key1, key2, key3]] + >>> redeemscript = Script(keys=keylist, sigs_required=2, script_types=['multisig']) + :param commands: List of script language commands :type commands: list :param message: Signed message to verify, normally a transaction hash. Used to validate script @@ -196,8 +185,8 @@ def __init__(self, commands=None, message=None, script_types='', is_locking=True :type signatures: list of Signature :param blueprint: Simplified version of script, normally generated by Script object :type blueprint: list of str - :param tx_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts - :type tx_data: dict + :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts + :type env_data: dict :param public_hash: Public hash of key or redeemscript used to create scripts :type public_hash: bytes :param sigs_required: Nubmer of signatures required to create multisig script @@ -216,13 +205,13 @@ def __init__(self, commands=None, message=None, script_types='', is_locking=True self.keys = keys if keys else [] self.signatures = signatures if signatures else [] self._blueprint = blueprint if blueprint else [] - self.tx_data = {} if not tx_data else tx_data + self.env_data = {} if not env_data else env_data self.sigs_required = sigs_required if sigs_required else len(self.keys) if len(self.keys) else 1 self.redeemscript = redeemscript self.public_hash = public_hash self.hash_type = hash_type - if not self.commands and self.script_types and (self.keys or self.signatures or self.public_hash): + if not self.commands and self.script_types: # and (self.keys or self.signatures or self.public_hash): for st in self.script_types: st_values = SCRIPT_TYPES[st] script_template = st_values[1] @@ -240,11 +229,13 @@ def __init__(self, commands=None, message=None, script_types='', is_locking=True command = [sig_n_and_m.pop() + 80] elif tc == 'redeemscript': command = [self.redeemscript] + elif tc in self.env_data: + command = [env_data[tc]] if not command or command == [b'']: raise ScriptError("Cannot create script, please supply %s" % (tc if tc != 'data' else 'public key hash')) self.commands += command - if not (self.keys and self.signatures and self.blueprint): + if not (self.keys and self.signatures and self._blueprint): self._blueprint = [] for c in self.commands: if isinstance(c, int): @@ -261,7 +252,7 @@ def __init__(self, commands=None, message=None, script_types='', is_locking=True self._blueprint.append('data-%d' % len(c)) @classmethod - def parse(cls, script, message=None, tx_data=None, strict=True, _level=0): + def parse(cls, script, message=None, env_data=None, is_locking=None, strict=True, _level=0): """ Parse raw script and return Script object. Extracts script commands, keys, signatures and other data. @@ -274,8 +265,10 @@ def parse(cls, script, message=None, tx_data=None, strict=True, _level=0): :type script: BytesIO, bytes, str :param message: Signed message to verify, normally a transaction hash :type message: bytes - :param tx_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts - :type tx_data: dict + :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts + :type env_data: dict + :param is_locking: Is this a locking script or not, use None if not known and derive from script. + :param is_locking: bool, None :param strict: Raise exception when script is malformed, incomplete or not understood. Default is True :type strict: bool :param _level: Internal argument used to avoid recursive depth @@ -290,10 +283,10 @@ def parse(cls, script, message=None, tx_data=None, strict=True, _level=0): elif isinstance(script, str): data_length = len(script) script = BytesIO(bytes.fromhex(script)) - return cls.parse_bytesio(script, message, tx_data, data_length, strict, _level) + return cls.parse_bytesio(script, message, env_data, data_length, is_locking, strict, _level) @classmethod - def parse_bytesio(cls, script, message=None, tx_data=None, data_length=0, strict=True, _level=0): + def parse_bytesio(cls, script, message=None, env_data=None, data_length=0, is_locking=None, strict=True, _level=0): """ Parse raw script and return Script object. Extracts script commands, keys, signatures and other data. @@ -301,10 +294,12 @@ def parse_bytesio(cls, script, message=None, tx_data=None, data_length=0, strict :type script: BytesIO :param message: Signed message to verify, normally a transaction hash :type message: bytes - :param tx_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts - :type tx_data: dict + :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts + :type env_data: dict :param data_length: Length of script data if known. Supply if you can to increase efficiency and lower change of incorrect parsing :type data_length: int + :param is_locking: Is this a locking script or not, use None if not known and derive from script. + :param is_locking: bool, None :param strict: Raise exception when script is malformed, incomplete or not understood. Default is True :type strict: bool :param _level: Internal argument used to avoid recursive depth @@ -320,8 +315,8 @@ def parse_bytesio(cls, script, message=None, tx_data=None, data_length=0, strict sigs_required = None # hash_type = SIGHASH_ALL # todo: check hash_type = None - if not tx_data: - tx_data = {} + if not env_data: + env_data = {} chb = script.read(1) ch = int.from_bytes(chb, 'big') @@ -374,8 +369,8 @@ def parse_bytesio(cls, script, message=None, tx_data=None, data_length=0, strict else: s2 = Script.parse_bytes(data, _level=_level+1, strict=strict) commands.pop() - commands += s2.commands - blueprint += s2.blueprint + commands += [s2.commands] + blueprint += [s2.blueprint] keys += s2.keys signatures += s2.signatures redeemscript = s2.redeemscript @@ -408,19 +403,23 @@ def parse_bytesio(cls, script, message=None, tx_data=None, data_length=0, strict chb = script.read(1) ch = int.from_bytes(chb, 'big') - s = cls(commands, message, keys=keys, signatures=signatures, blueprint=blueprint, tx_data=tx_data, + if len(commands) == 1 and isinstance(commands[0], list): + commands = commands[0] + if len(blueprint) == 1 and isinstance(blueprint[0], list): + blueprint = blueprint[0] + s = cls(commands, message, keys=keys, signatures=signatures, blueprint=blueprint, env_data=env_data, hash_type=hash_type) script.seek(0) s._raw = script.read() - s.script_types = _get_script_types(blueprint) + s.script_types = _get_script_types(blueprint, is_locking=is_locking) if 'unknown' in s.script_types: s.script_types = ['unknown'] # Extract extra information from script data for st in s.script_types[:1]: if st == 'multisig': - s.redeemscript = s.raw + s.redeemscript = s.as_bytes() s.sigs_required = s.commands[0] - 80 if s.sigs_required > len(keys): raise ScriptError("Number of signatures required (%d) is higher then number of keys (%d)" % @@ -428,22 +427,22 @@ def parse_bytesio(cls, script, message=None, tx_data=None, data_length=0, strict if len(s.keys) != s.commands[-2] - 80: raise ScriptError("%d keys found but %d keys expected" % (len(s.keys), s.commands[-2] - 80)) - elif st in ['p2wpkh', 'p2wsh', 'p2sh', 'p2tr'] and len(s.commands) > 1: + elif st in ['p2wpkh', 'p2wsh', 'p2sh', 'p2tr', 'p2sh_p2wpkh', 'p2sh_p2wsh'] and len(s.commands) > 1: s.public_hash = s.commands[1] elif st == 'p2tr_unlock': s.public_hash = s.commands[0] elif st == 'p2pkh' and len(s.commands) > 2: s.public_hash = s.commands[2] s.redeemscript = redeemscript if redeemscript else s.redeemscript - if s.redeemscript and 'redeemscript' not in s.tx_data: - s.tx_data['redeemscript'] = s.redeemscript + if s.redeemscript and 'redeemscript' not in s.env_data: + s.env_data['redeemscript'] = s.redeemscript s.sigs_required = sigs_required if sigs_required else s.sigs_required return s @classmethod - def parse_hex(cls, script, message=None, tx_data=None, strict=True, _level=0): + def parse_hex(cls, script, message=None, env_data=None, is_locking=None, strict=True, _level=0): """ Parse raw script and return Script object. Extracts script commands, keys, signatures and other data. @@ -456,8 +455,10 @@ def parse_hex(cls, script, message=None, tx_data=None, strict=True, _level=0): :type script: str :param message: Signed message to verify, normally a transaction hash :type message: bytes - :param tx_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts - :type tx_data: dict + :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts + :type env_data: dict + :param is_locking: Is this a locking script or not, use None if not known and derive from script. + :param is_locking: bool, None :param strict: Raise exception when script is malformed, incomplete or not understood. Default is True :type strict: bool :param _level: Internal argument used to avoid recursive depth @@ -466,10 +467,11 @@ def parse_hex(cls, script, message=None, tx_data=None, strict=True, _level=0): :return Script: """ data_length = len(script) // 2 - return cls.parse_bytesio(BytesIO(bytes.fromhex(script)), message, tx_data, data_length, strict, _level) + return cls.parse_bytesio(BytesIO(bytes.fromhex(script)), message, env_data, data_length, is_locking, strict, + _level) @classmethod - def parse_bytes(cls, script, message=None, tx_data=None, strict=True, _level=0): + def parse_bytes(cls, script, message=None, env_data=None, is_locking=None, strict=True, _level=0): """ Parse raw script and return Script object. Extracts script commands, keys, signatures and other data. @@ -479,8 +481,10 @@ def parse_bytes(cls, script, message=None, tx_data=None, strict=True, _level=0): :type script: bytes :param message: Signed message to verify, normally a transaction hash :type message: bytes - :param tx_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts - :type tx_data: dict + :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts + :type env_data: dict + :param is_locking: Is this a locking script or not, use None if not known and derive from script. + :param is_locking: bool, None :param strict: Raise exception when script is malformed or incomplete :type strict: bool :param _level: Internal argument used to avoid recursive depth @@ -489,29 +493,60 @@ def parse_bytes(cls, script, message=None, tx_data=None, strict=True, _level=0): :return Script: """ data_length = len(script) - return cls.parse_bytesio(BytesIO(script), message, tx_data, data_length, strict, _level) + return cls.parse_bytesio(BytesIO(script), message, env_data, data_length, is_locking, strict, _level) - def __repr__(self): + @classmethod + def parse_str(cls, script, message=None, env_data=None, is_locking=None, strict=True, _level=0): + """ + Parse script in string format and return Script object. + Extracts script commands, keys, signatures and other data. + + >>> s = Script.parse_str("1 98 OP_ADD 99 OP_EQUAL") + >>> s + data-1 data-1 OP_ADD data-1 OP_EQUAL + >>> s.evaluate() + True + + :param script: Raw script to parse in bytes format + :type script: str + :param message: Signed message to verify, normally a transaction hash + :type message: bytes + :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts + :type env_data: dict + :param is_locking: Is this a locking script or not, use None if not known and derive from script. + :param is_locking: bool, None + :param strict: Raise exception when script is malformed or incomplete + :type strict: bool + :param _level: Internal argument used to avoid recursive depth + :type _level: int + + :return Script: + """ + items = script.split(' ') s_items = [] - for command in self.blueprint: - if isinstance(command, int): - s_items.append('op.' + opcodenames.get(command, 'unknown-op-%s' % command).lower()) + for item in items: + if item.isdigit(): + ival = int(item) + if 0 < ival <= 16: + s_items.append(ival.to_bytes(1, 'big')) + else: + s_items.append(int_to_varbyteint(ival)) + elif item.startswith('OP_'): + s_items.append(getattr(op, item.lower(), 'unknown-command-%s' % item)) else: - s_items.append(command) + s_items.append(bytes.fromhex(item)) + return cls(s_items, message, env_data, is_locking=is_locking) + + def __repr__(self): + s_items = self.view(blueprint=True, as_list=True) return '' def __str__(self): - s_items = [] - for command in self.blueprint: - if isinstance(command, int): - s_items.append(opcodenames.get(command, 'unknown-op-%s' % command)) - else: - s_items.append(command) - return ' '.join(s_items) + return self.view(blueprint=True) def __add__(self, other): self.commands += other.commands - self._raw += other.raw + self._raw += other.as_bytes() if other.message and not self.message: self.message = other.message self.is_locking = None @@ -519,8 +554,8 @@ def __add__(self, other): self.signatures += other.signatures self._blueprint += other._blueprint self.script_types = _get_script_types(self._blueprint) - if other.tx_data and not self.tx_data: - self.tx_data = other.tx_data + if other.env_data and not self.env_data: + self.env_data = other.env_data if other.redeemscript and not self.redeemscript: self.redeemscript = other.redeemscript return self @@ -529,19 +564,29 @@ def __bool__(self): return bool(self.commands) def __hash__(self): - return hash160(self.raw) + return hash160(self.as_bytes()) @property def blueprint(self): - # TODO: create blueprint from commands if empty return self._blueprint @property + @deprecated def raw(self): if not self._raw: self._raw = self.serialize() return self._raw + def as_bytes(self): + if not self._raw: + self._raw = self.serialize() + return self._raw + + def as_hex(self): + if not self._raw: + self._raw = self.serialize() + return self._raw.hex() + def serialize(self): """ Serialize script. Return all commands and data as bytes @@ -557,7 +602,7 @@ def serialize(self): if isinstance(cmd, int): raw += bytes([cmd]) else: - raw += data_pack(cmd) + raw += data_pack(bytes(cmd)) self._raw = raw return raw @@ -579,7 +624,48 @@ def serialize_list(self): clist.append(bytes(cmd)) return clist - def evaluate(self, message=None, tx_data=None): + def view(self, blueprint=False, as_list=False, op_code_numbers=False, show_1_byte_data_as_int=True): + """ + View script as string in human-readable format. + + :param blueprint: Show blueprint only, without detailed data. + :type blueprint: bool + :param as_list: Show script as list + :type as_list: bool + :param op_code_numbers: Show opcodes as numbers instead of string. + :type op_code_numbers: bool + :param show_1_byte_data_as_int: Show 1 byte data objects as integers. + :type show_1_byte_data_as_int: bool + + :return str: + """ + s_items = [] + i = 0 + for command in self.commands: + if isinstance(command, int): + if op_code_numbers: + s_items.append(command) + else: + s_items.append(opcodenames.get(command, 'unknown-op-%s' % command)) + elif isinstance(command, list): + s_items.append('redeemscript') + else: + if blueprint: + if self.blueprint and len(self.blueprint) >= i: + s_items.append(self.blueprint[i]) + else: + s_items.append('data-%d' % len(command)) + else: + chex = command.hex() + if len(chex) == 2 and show_1_byte_data_as_int: + s_items.append(int(chex, 16)) + else: + s_items.append(chex) + i += 1 + + return s_items if as_list else ' '.join(str(i) for i in s_items) + + def evaluate(self, message=None, env_data=None, trace=False): """ Evaluate script, run all commands and check if it is valid @@ -600,17 +686,32 @@ def evaluate(self, message=None, tx_data=None): :param message: Signed message to verify, normally a transaction hash. Leave empty to use Script.message. If supplied Script.message will be ignored. :type message: bytes - :param tx_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts. Leave emtpy to use Script.tx_data. If supplied Script.tx_data will be ignored + :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for + :type env_data: dict() + + multisignature scripts and 'blockcount' for time locked scripts. Leave emtpy to use Script.data. If supplied Script.data will be ignored :return bool: Valid or not valid """ self.message = self.message if message is None else message - self.tx_data = self.tx_data if tx_data is None else tx_data + self.env_data = self.env_data if env_data is None else env_data self.stack = Stack() - commands = self.commands[:] + commands = [] + for c in self.commands: + if isinstance(c, list): + commands += c + else: + commands.append(c) while len(commands): command = commands.pop(0) + if trace: + print("----------") + print("Stack:") + [print(f"- {i.hex()}") for i in self.stack] + cmd = opcodenames[command] if isinstance(command, int) else command.hex() + print(f"Command: {cmd}") + print("\n") if isinstance(command, int): if command == op.op_0: # OP_0 self.stack.append(encode_num(0)) @@ -631,13 +732,18 @@ def evaluate(self, message=None, tx_data=None): method = getattr(self.stack, method_name) if method_name == 'op_checksig' or method_name == 'op_checksigverify': res = method(self.message) - elif method_name == 'op_checkmultisig' or method_name == 'op_checkmultisigverify': - res = method(self.message, self.tx_data) + elif method_name == 'op_checkmultisig': + method(self.message, self.env_data) + res = self.stack.op_verify() + self.stack.append(self.env_data['redeemscript']) + elif method_name == 'op_checkmultisigverify': + res = method(self.message, self.env_data) + self.stack.append(self.env_data['redeemscript']) elif method_name == 'op_checklocktimeverify': res = self.stack.op_checklocktimeverify( - self.tx_data['sequence'], self.tx_data.get('locktime')) + self.env_data['sequence'], self.env_data.get('locktime')) elif method_name == 'op_checksequenceverify': - res = self.stack.op_checksequenceverify(self.tx_data['sequence'], self.tx_data['version']) + res = self.stack.op_checksequenceverify(self.env_data['sequence'], self.env_data['version']) else: res = method() if res is False: @@ -1078,10 +1184,7 @@ def op_checkmultisig(self, message, data=None): break if sigcount == len(signatures): - if data and 'redeemscript' in data: - self.append(data['redeemscript']) - else: - self.append(b'\1') + self.append(b'\1') else: self.append(b'') return True diff --git a/bitcoinlib/services/__init__.py b/bitcoinlib/services/__init__.py index 80b291ca..286584c4 100644 --- a/bitcoinlib/services/__init__.py +++ b/bitcoinlib/services/__init__.py @@ -23,7 +23,6 @@ import bitcoinlib.services.bitcoind import bitcoinlib.services.dogecoind import bitcoinlib.services.litecoind -import bitcoinlib.services.dashd import bitcoinlib.services.bitgo import bitcoinlib.services.blockchaininfo import bitcoinlib.services.blockcypher @@ -33,7 +32,6 @@ import bitcoinlib.services.bcoin import bitcoinlib.services.bitaps import bitcoinlib.services.litecoinblockexplorer -import bitcoinlib.services.insightdash import bitcoinlib.services.blockstream import bitcoinlib.services.blocksmurfer import bitcoinlib.services.mempool diff --git a/bitcoinlib/services/baseclient.py b/bitcoinlib/services/baseclient.py index d82dd641..2e7889c2 100644 --- a/bitcoinlib/services/baseclient.py +++ b/bitcoinlib/services/baseclient.py @@ -19,6 +19,7 @@ # import requests +import urllib3 from urllib.parse import urlencode import json from bitcoinlib.main import * @@ -27,6 +28,9 @@ _logger = logging.getLogger(__name__) +# Disable warnings about insecure requests, as we only connect to familiar sources and local nodes +urllib3.disable_warnings() + class ClientError(Exception): def __init__(self, msg=''): @@ -40,12 +44,13 @@ def __str__(self): class BaseClient(object): def __init__(self, network, provider, base_url, denominator, api_key='', provider_coin_id='', - network_overrides=None, timeout=TIMEOUT_REQUESTS, latest_block=None, strict=True): + network_overrides=None, timeout=TIMEOUT_REQUESTS, latest_block=None, strict=True, wallet_name=''): try: self.network = network if not isinstance(network, Network): self.network = Network(network) self.provider = provider + self.base_url = base_url self.resp = None self.units = denominator @@ -57,6 +62,7 @@ def __init__(self, network, provider, base_url, denominator, api_key='', provide if network_overrides is not None: self.network_overrides = network_overrides self.strict = strict + self.wallet_name = wallet_name except Exception: raise ClientError("This Network is not supported by %s Client" % provider) diff --git a/bitcoinlib/services/bcoin.py b/bitcoinlib/services/bcoin.py index b55d70e8..820218dd 100644 --- a/bitcoinlib/services/bcoin.py +++ b/bitcoinlib/services/bcoin.py @@ -59,6 +59,7 @@ def _parse_transaction(self, tx): t.block_height = tx['height'] if tx['height'] > 0 else None t.block_hash = tx['block'] t.status = status + t.index = tx['index'] if not t.coinbase: for i in t.inputs: i.value = tx['inputs'][t.inputs.index(i)]['coin']['value'] diff --git a/bitcoinlib/services/bitaps.py b/bitcoinlib/services/bitaps.py index ebf2bd9a..07b3a791 100644 --- a/bitcoinlib/services/bitaps.py +++ b/bitcoinlib/services/bitaps.py @@ -75,7 +75,7 @@ def _parse_transaction(self, tx): sequence=ti['sequence'], index_n=int(n), value=0) else: t.add_input(prev_txid=ti['txId'], output_n=ti['vOut'], unlocking_script=ti['scriptSig'], - unlocking_script_unsigned=ti['scriptPubKey'], witnesses=ti.get('txInWitness', []), + locking_script=ti['scriptPubKey'], witnesses=ti.get('txInWitness', []), address='' if 'address' not in ti else ti['address'], sequence=ti['sequence'], index_n=int(n), value=ti['amount'], strict=self.strict) @@ -172,6 +172,8 @@ def gettransactions(self, address, after_txid='', limit=MAX_TRANSACTIONS): # def estimatefee def blockcount(self): + if self.network == 'testnet': + raise ClientError('Providers return incorrect blockcount for testnet') return self.compose_request('block', 'last')['data']['height'] # def mempool(self, txid): diff --git a/bitcoinlib/services/bitcoind.py b/bitcoinlib/services/bitcoind.py index b6a3dfb7..9a7410b3 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): """ @@ -125,13 +128,19 @@ def __init__(self, network='bitcoin', base_url='', denominator=100000000, *args) :type: str :param denominator: Denominator for this currency. Should be always 100000000 (Satoshi's) for bitcoin :type: str + :param wallet_name: Name of Bitcoin core wallet to use. Make sure the wallet exists, otherwise connection will fail. + :type str """ 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 + wallet_name = '' if not len(args) > 6 else args[6] + if wallet_name: + base_url = base_url.replace("{wallet_name}", wallet_name) _logger.info("Connect to bitcoind") self.proxy = AuthServiceProxy(base_url) super(self.__class__, self).__init__(network, PROVIDERNAME, base_url, denominator, *args) @@ -223,7 +232,7 @@ def gettransactions(self, address, after_txid='', limit=MAX_TRANSACTIONS): if len(txs_list) >= MAX_WALLET_TRANSACTIONS: raise ClientError("Bitcoind wallet contains too many transactions %d, use other service provider for this " "wallet" % MAX_WALLET_TRANSACTIONS) - txids = list(set([(tx['txid'], tx['blockheight']) for tx in txs_list if tx['address'] == address])) + txids = list(set([(tx['txid'], tx.get('blockheight')) for tx in txs_list if tx['address'] == address])) for (txid, blockheight) in txids: tx_raw = self.proxy.getrawtransaction(txid, 1) t = self._parse_transaction(tx_raw, blockheight) @@ -332,12 +341,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/bitgo.py b/bitcoinlib/services/bitgo.py index 1013b425..eab4c353 100644 --- a/bitcoinlib/services/bitgo.py +++ b/bitcoinlib/services/bitgo.py @@ -166,6 +166,8 @@ def estimatefee(self, blocks): return res['feePerKb'] def blockcount(self): + if self.network == 'testnet': + raise ClientError('Providers return incorrect blockcount for testnet') return self.compose_request('block', 'latest')['height'] # def mempool diff --git a/bitcoinlib/services/blockchair.py b/bitcoinlib/services/blockchair.py index 6133c5e4..dcefecd2 100644 --- a/bitcoinlib/services/blockchair.py +++ b/bitcoinlib/services/blockchair.py @@ -139,7 +139,7 @@ def gettransaction(self, tx_id): else: t.add_input(prev_txid=ti['transaction_hash'], output_n=ti['index'], unlocking_script=ti['spending_signature_hex'], index_n=index_n, value=ti['value'], - address=ti['recipient'], unlocking_script_unsigned=ti['script_hex'], + address=ti['recipient'], locking_script=ti['script_hex'], sequence=ti['spending_sequence'], strict=self.strict) index_n += 1 for to in res['data'][tx_id]['outputs']: diff --git a/bitcoinlib/services/blockcypher.py b/bitcoinlib/services/blockcypher.py index 882ec7ad..f313e496 100644 --- a/bitcoinlib/services/blockcypher.py +++ b/bitcoinlib/services/blockcypher.py @@ -56,39 +56,38 @@ def getbalance(self, addresslist): balance += float(rec['final_balance']) return int(balance * self.units) - # Disabled: Invalid results for https://api.blockcypher.com/v1/ltc/main/addrs/LVqLipGhyQ1nWtPPc8Xp3zn6JxcU1Hi8eG?unspentOnly=1&limit=2000 - # def getutxos(self, address, after_txid='', limit=MAX_TRANSACTIONS): - # address = self._address_convert(address) - # res = self.compose_request('addrs', address.address, variables={'unspentOnly': 1, 'limit': 2000}) - # transactions = [] - # if not isinstance(res, list): - # res = [res] - # for a in res: - # txrefs = a.setdefault('txrefs', []) + a.get('unconfirmed_txrefs', []) - # if len(txrefs) > 500: - # _logger.warning("BlockCypher: Large number of transactions for address %s, " - # "Transaction list may be incomplete" % address) - # for tx in txrefs: - # if tx['tx_hash'] == after_txid: - # break - # tdate = None - # if 'confirmed' in tx: - # try: - # tdate = datetime.strptime(tx['confirmed'], "%Y-%m-%dT%H:%M:%SZ") - # except ValueError: - # tdate = datetime.strptime(tx['confirmed'], "%Y-%m-%dT%H:%M:%S.%fZ") - # transactions.append({ - # 'address': address.address_orig, - # 'txid': tx['tx_hash'], - # 'confirmations': tx['confirmations'], - # 'output_n': tx['tx_output_n'], - # 'index': 0, - # 'value': int(round(tx['value'] * self.units, 0)), - # 'script': '', - # 'block_height': None, - # 'date': tdate - # }) - # return transactions[::-1][:limit] + def getutxos(self, address, after_txid='', limit=MAX_TRANSACTIONS): + address = self._address_convert(address) + res = self.compose_request('addrs', address.address, variables={'unspentOnly': 1, 'limit': 2000}) + transactions = [] + if not isinstance(res, list): + res = [res] + for a in res: + txrefs = a.setdefault('txrefs', []) + a.get('unconfirmed_txrefs', []) + if len(txrefs) > 500: + _logger.warning("BlockCypher: Large number of transactions for address %s, " + "Transaction list may be incomplete" % address) + for tx in txrefs: + if tx['tx_hash'] == after_txid: + break + tdate = None + if 'confirmed' in tx: + try: + tdate = datetime.strptime(tx['confirmed'], "%Y-%m-%dT%H:%M:%SZ") + except ValueError: + tdate = datetime.strptime(tx['confirmed'], "%Y-%m-%dT%H:%M:%S.%fZ") + transactions.append({ + 'address': address.address_orig, + 'txid': tx['tx_hash'], + 'confirmations': tx['confirmations'], + 'output_n': tx['tx_output_n'], + 'index': 0, + 'value': int(round(tx['value'] * self.units, 0)), + 'script': '', + 'block_height': None, + 'date': tdate + }) + return transactions[::-1][:limit] def gettransaction(self, txid): tx = self.compose_request('txs', txid, variables={'includeHex': 'true'}) diff --git a/bitcoinlib/services/blocksmurfer.py b/bitcoinlib/services/blocksmurfer.py index 1bc1f211..6ad1fbb2 100644 --- a/bitcoinlib/services/blocksmurfer.py +++ b/bitcoinlib/services/blocksmurfer.py @@ -100,7 +100,7 @@ def _parse_transaction(self, tx, block_height=None): public_hash=bytes.fromhex(ti['public_hash']), address=ti['address'], witness_type=ti['witness_type'], locktime_cltv=ti['locktime_cltv'], locktime_csv=ti['locktime_csv'], signatures=ti['signatures'], compressed=ti['compressed'], - encoding=ti['encoding'], unlocking_script_unsigned=ti['script_code'], + encoding=ti['encoding'], locking_script=ti['script_code'], sigs_required=ti['sigs_required'], sequence=ti['sequence'], witnesses=[bytes.fromhex(w) for w in ti['witnesses']], script_type=ti['script_type'], strict=self.strict) diff --git a/bitcoinlib/services/blockstream.py b/bitcoinlib/services/blockstream.py index 9f48bc09..499635c8 100644 --- a/bitcoinlib/services/blockstream.py +++ b/bitcoinlib/services/blockstream.py @@ -116,7 +116,7 @@ def _parse_transaction(self, tx): unlocking_script=ti['scriptsig'], value=ti['prevout']['value'], address='' if 'scriptpubkey_address' not in ti['prevout'] else ti['prevout']['scriptpubkey_address'], sequence=ti['sequence'], - unlocking_script_unsigned=ti['prevout']['scriptpubkey'], witnesses=witnesses, strict=self.strict) + locking_script=ti['prevout']['scriptpubkey'], witnesses=witnesses, strict=self.strict) index_n += 1 index_n = 0 if len(tx['vout']) > 101: diff --git a/bitcoinlib/services/cryptoid.py b/bitcoinlib/services/cryptoid.py index a1d31566..cbef6334 100644 --- a/bitcoinlib/services/cryptoid.py +++ b/bitcoinlib/services/cryptoid.py @@ -123,6 +123,8 @@ def gettransaction(self, txid): return t def gettransactions(self, address, after_txid='', limit=MAX_TRANSACTIONS): + if not self.api_key: + raise ClientError("Method gettransactions() is not available for CryptoID without API key") address = self._address_convert(address) txs = [] txids = [] diff --git a/bitcoinlib/services/dashd.py b/bitcoinlib/services/dashd.py deleted file mode 100644 index 467aee6b..00000000 --- a/bitcoinlib/services/dashd.py +++ /dev/null @@ -1,293 +0,0 @@ -# -*- coding: utf-8 -*- -# -# BitcoinLib - Python Cryptocurrency Library -# dashd deamon -# © 2018-2022 Oct - 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 -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -# -# You can connect to a dash testnet deamon by adding the following provider to providers.json -# "dashd.testnet": { -# "provider": "dashd", -# "network": "dash_testnet", -# "client_class": "DashdClient", -# "url": "http://user:password@server_url:19998", -# "api_key": "", -# "priority": 11, -# "denominator": 100000000 -# } - -import configparser -from bitcoinlib.main import * -from bitcoinlib.services.authproxy import AuthServiceProxy -from bitcoinlib.services.baseclient import BaseClient -from bitcoinlib.transactions import Transaction - - -PROVIDERNAME = 'dashd' - -_logger = logging.getLogger(__name__) - - -class ConfigError(Exception): - def __init__(self, msg=''): - self.msg = msg - _logger.info(msg) - - def __str__(self): - return self.msg - - -class DashdClient(BaseClient): - """ - Class to interact with dashd, the Dash deamon - """ - - @staticmethod - def from_config(configfile=None, network='dash', **kargs): - """ - Read settings from dashd config file - - :param configfile: Path to config file. Leave empty to look in default places - :type: str - :param network: Dash mainnet or testnet. Default is dash mainnet - :type: str - - :return DashdClient: - """ - config = configparser.ConfigParser(strict=False) - if not configfile: - cfn = os.path.join(os.path.expanduser("~"), '.bitcoinlib/dash.conf') - if not os.path.isfile(cfn): - cfn = os.path.join(os.path.expanduser("~"), '.dashcore/dash.conf') - if not os.path.isfile(cfn): - raise ConfigError("Please install dash client and specify a path to config file if path is not " - "default. Or place a config file in .bitcoinlib/dash.conf to reference to " - "an external server.") - else: - cfn = os.path.join(BCL_DATA_DIR, 'config', configfile) - if not os.path.isfile(cfn): - raise ConfigError("Config file %s not found" % cfn) - with open(cfn, 'r') as f: - config_string = '[rpc]\n' + f.read() - config.read_string(config_string) - - try: - if int(config.get('rpc', 'testnet')): - network = 'testnet' - except configparser.NoOptionError: - pass - if config.get('rpc', 'rpcpassword') == 'specify_rpc_password': - raise ConfigError("Please update config settings in %s" % cfn) - try: - port = config.get('rpc', 'port') - except configparser.NoOptionError: - if network == 'testnet': - port = 19998 - else: - port = 9998 - server = '127.0.0.1' - if 'bind' in config['rpc']: - server = config.get('rpc', 'bind') - elif 'externalip' in config['rpc']: - server = config.get('rpc', 'externalip') - url = "http://%s:%s@%s:%s" % (config.get('rpc', 'rpcuser'), config.get('rpc', 'rpcpassword'), server, port) - return DashdClient(network, url, **kargs) - - def __init__(self, network='dash', base_url='', denominator=100000000, **kargs): - """ - Open connection to dashcore node - - :param network: Dash mainnet or testnet. Default is dash mainnet - :type: str - :param base_url: Connection URL in format http(s)://user:password@host:port. - :type: str - :param denominator: Denominator for this currency. Should be always 100000000 (satoshis) for Dash - :type: str - """ - if not base_url: - bdc = self.from_config('', network) - base_url = bdc.base_url - network = bdc.network - _logger.info("Connect to dashd") - self.proxy = AuthServiceProxy(base_url) - super(self.__class__, self).__init__(network, PROVIDERNAME, base_url, denominator, **kargs) - - def _parse_transaction(self, tx, block_height=None, get_input_values=True): - t = Transaction.parse_hex(tx['hex'], strict=self.strict, network=self.network) - t.confirmations = None if 'confirmations' not in tx else tx['confirmations'] - if t.confirmations or block_height: - t.status = 'confirmed' - t.verified = True - for i in t.inputs: - if i.prev_txid == b'\x00' * 32: - i.script_type = 'coinbase' - continue - if get_input_values: - txi = self.proxy.getrawtransaction(i.prev_txid.hex(), 1) - i.value = int(round(float(txi['vout'][i.output_n_int]['value']) / self.network.denominator)) - for o in t.outputs: - o.spent = None - t.block_height = block_height - t.version = tx['version'].to_bytes(4, 'big') - t.date = datetime.fromtimestamp(tx['blocktime'], timezone.utc) - t.update_totals() - return t - - def gettransaction(self, txid): - tx = self.proxy.getrawtransaction(txid, 1) - return self._parse_transaction(tx) - - def getrawtransaction(self, txid): - res = self.proxy.getrawtransaction(txid) - return res - - def sendrawtransaction(self, rawtx): - res = self.proxy.sendrawtransaction(rawtx) - return { - 'txid': res, - 'response_dict': res - } - - def estimatefee(self, blocks): - try: - res = self.proxy.estimatesmartfee(blocks)['feerate'] - except KeyError: - res = self.proxy.estimatefee(blocks) - return int(res * self.units) - - def blockcount(self): - return self.proxy.getblockcount() - - def getutxos(self, address, after_txid='', limit=MAX_TRANSACTIONS): - txs = [] - - txs_list = self.proxy.listunspent(0, 99999999, [address]) - for t in sorted(txs_list, key=lambda x: x['confirmations'], reverse=True): - txs.append({ - 'address': t['address'], - 'txid': t['txid'], - 'confirmations': t['confirmations'], - 'output_n': t['vout'], - 'input_n': -1, - 'block_height': None, - 'fee': None, - 'size': 0, - 'value': int(t['amount'] * self.units), - 'script': t['scriptPubKey'], - 'date': None, - }) - if t['txid'] == after_txid: - txs = [] - - return txs - - def getblock(self, blockid, parse_transactions=True, page=1, limit=None): - if isinstance(blockid, int): - blockid = self.proxy.getblockhash(blockid) - if not limit: - limit = 99999 - - txs = [] - if parse_transactions: - bd = self.proxy.getblock(blockid, 2) - for tx in bd['tx'][(page - 1) * limit:page * limit]: - # try: - tx['blocktime'] = bd['time'] - tx['blockhash'] = bd['hash'] - txs.append(self._parse_transaction(tx, block_height=bd['height'], get_input_values=False)) - # except Exception as e: - # _logger.error("Could not parse tx %s with error %s" % (tx['txid'], e)) - # txs += [tx['hash'] for tx in bd['tx'][len(txs):]] - else: - bd = self.proxy.getblock(blockid, 1) - txs = bd['tx'] - - block = { - 'bits': int(bd['bits'], 16), - 'depth': bd['confirmations'], - 'hash': bd['hash'], - 'height': bd['height'], - 'merkle_root': bd['merkleroot'], - 'nonce': bd['nonce'], - 'prev_block': bd['previousblockhash'], - 'time': bd['time'], - 'total_txs': bd['nTx'], - 'txs': txs, - 'version': bd['version'], - 'page': page, - 'pages': None, - 'limit': limit - } - return block - - def getrawblock(self, blockid): - if isinstance(blockid, int): - blockid = self.proxy.getblockhash(blockid) - return self.proxy.getblock(blockid, 0) - - def isspent(self, txid, index): - res = self.proxy.gettxout(txid, index) - if not res: - return 1 - return 0 - - def getinfo(self): - info = self.proxy.getmininginfo() - return { - 'blockcount': info['blocks'], - 'chain': info['chain'], - 'difficulty': int(info['difficulty']), - 'hashrate': int(info['networkhashps']), - 'mempool_size': int(info['pooledtx']), - } - - -if __name__ == '__main__': - # - # SOME EXAMPLES - # - - from pprint import pprint - - # 1. Connect by specifying connection URL - # base_url = 'http://dashrpcuser:passwd@host:9998' - # bdc = DashdClient(base_url=base_url) - - # 2. Or connect using default settings or settings from config file - bdc = DashdClient() - - print("\n=== SERVERINFO ===") - pprint(bdc.proxy.getnetworkinfo()) - - print("\n=== Best Block ===") - blockhash = bdc.proxy.getbestblockhash() - bestblock = bdc.proxy.getblock(blockhash) - bestblock['tx'] = '...' + str(len(bestblock['tx'])) + ' transactions...' - pprint(bestblock) - - print("\n=== Mempool ===") - rmp = bdc.proxy.getrawmempool() - pprint(rmp[:25]) - print('... truncated ...') - print("Mempool Size %d" % len(rmp)) - - print("\n=== Raw Transaction by txid ===") - t = bdc.getrawtransaction('c3d2a934ef8eb9b2291d113b330b9244c1521ef73df0a4b04c39e851112f01af') - pprint(t) - - print("\n=== Current network fees ===") - t = bdc.estimatefee(5) - pprint(t) diff --git a/bitcoinlib/services/insightdash.py b/bitcoinlib/services/insightdash.py deleted file mode 100644 index 5c3d6625..00000000 --- a/bitcoinlib/services/insightdash.py +++ /dev/null @@ -1,196 +0,0 @@ -# -*- coding: utf-8 -*- -# -# BitcoinLib - Python Cryptocurrency Library -# Litecore.io Client -# © 2018-2022 October - 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 -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# - -import logging -from datetime import datetime, timezone -from bitcoinlib.main import MAX_TRANSACTIONS -from bitcoinlib.services.baseclient import BaseClient -from bitcoinlib.transactions import Transaction - -PROVIDERNAME = 'insightdash' -REQUEST_LIMIT = 50 - -_logger = logging.getLogger(__name__) - - -class InsightDashClient(BaseClient): - - def __init__(self, network, base_url, denominator, *args): - super(self.__class__, self).__init__(network, PROVIDERNAME, base_url, denominator, *args) - - def compose_request(self, category, data, cmd='', variables=None, method='get', offset=0): - url_path = category - if data: - url_path += '/' + data + '/' + cmd - if variables is None: - variables = {} - variables.update({'from': offset, 'to': offset+REQUEST_LIMIT}) - return self.request(url_path, variables, method=method) - - def _convert_to_transaction(self, tx): - if tx['confirmations']: - status = 'confirmed' - else: - status = 'unconfirmed' - fees = None if 'fees' not in tx else int(round(float(tx['fees']) * self.units, 0)) - value_in = 0 if 'valueIn' not in tx else tx['valueIn'] - isCoinbase = False - if 'isCoinBase' in tx and tx['isCoinBase']: - isCoinbase = True - txdate = None - if 'blocktime' in tx: - txdate = datetime.fromtimestamp(tx['blocktime'], timezone.utc) - t = Transaction(locktime=tx['locktime'], version=tx['version'], network=self.network, - fee=fees, size=tx['size'], txid=tx['txid'], - date=txdate, confirmations=tx['confirmations'], - block_height=tx['blockheight'], status=status, - input_total=int(round(float(value_in) * self.units, 0)), coinbase=isCoinbase, - output_total=int(round(float(tx['valueOut']) * self.units, 0))) - for ti in tx['vin']: - if isCoinbase: - t.add_input(prev_txid=32 * b'\0', output_n=4*b'\xff', unlocking_script=ti['coinbase'], index_n=ti['n'], - script_type='coinbase', sequence=ti['sequence'], value=0) - else: - value = int(round(float(ti['value']) * self.units, 0)) - t.add_input(prev_txid=ti['txid'], output_n=ti['vout'], unlocking_script=ti['scriptSig']['hex'], - index_n=ti['n'], value=value, sequence=ti['sequence'], - double_spend=False if ti['doubleSpentTxID'] is None else ti['doubleSpentTxID'], - strict=self.strict) - for to in tx['vout']: - value = int(round(float(to['value']) * self.units, 0)) - t.add_output(value=value, lock_script=to['scriptPubKey']['hex'], - spent=True if to['spentTxId'] else False, output_n=to['n'], - spending_txid=None if not to['spentTxId'] else to['spentTxId'], - spending_index_n=None if not to['spentIndex'] else to['spentIndex'], strict=self.strict) - return t - - def getbalance(self, addresslist): - balance = 0 - addresslist = self._addresslist_convert(addresslist) - for a in addresslist: - res = self.compose_request('addr', a.address, 'balance') - balance += res - return balance - - def getutxos(self, address, after_txid='', limit=MAX_TRANSACTIONS): - address = self._address_convert(address) - res = self.compose_request('addrs', address.address, 'utxo') - txs = [] - for tx in res: - if tx['txid'] == after_txid: - break - txs.append({ - 'address': address.address_orig, - 'txid': tx['txid'], - 'confirmations': tx['confirmations'], - 'output_n': tx['vout'], - 'input_n': 0, - 'block_height': tx['height'], - 'fee': None, - 'size': 0, - 'value': tx['satoshis'], - 'script': tx['scriptPubKey'], - 'date': None - }) - return txs[::-1][:limit] - - def gettransaction(self, tx_id): - tx = self.compose_request('tx', tx_id) - return self._convert_to_transaction(tx) - - def gettransactions(self, address, after_txid='', limit=MAX_TRANSACTIONS): - address = self._address_convert(address) - res = self.compose_request('addrs', address.address, 'txs') - txs = [] - txs_dict = res['items'][::-1] - if after_txid: - txs_dict = txs_dict[[t['txid'] for t in txs_dict].index(after_txid) + 1:] - for tx in txs_dict[:limit]: - if tx['txid'] == after_txid: - break - txs.append(self._convert_to_transaction(tx)) - return txs - - def getrawtransaction(self, tx_id): - res = self.compose_request('rawtx', tx_id) - return res['rawtx'] - - def sendrawtransaction(self, rawtx): - res = self.compose_request('tx', 'send', variables={'rawtx': rawtx}, method='post') - return { - 'txid': res['txid'], - 'response_dict': res - } - - # def estimatefee - - def blockcount(self): - res = self.compose_request('status', '', variables={'q': 'getinfo'}) - return res['info']['blocks'] - - def mempool(self, txid): - res = self.compose_request('tx', txid) - if res['confirmations'] == 0: - return res['txid'] - return [] - - def getblock(self, blockid, parse_transactions, page, limit): - bd = self.compose_request('block', str(blockid)) - if parse_transactions: - txs = [] - for txid in bd['tx'][(page-1)*limit:page*limit]: - try: - txs.append(self.gettransaction(txid)) - except Exception as e: - _logger.error("Could not parse tx %s with error %s" % (txid, e)) - else: - txs = bd['tx'] - - block = { - 'bits': bd['bits'], - 'depth': bd['confirmations'], - 'hash': bd['hash'], - 'height': bd['height'], - 'merkle_root': bd['merkleroot'], - 'nonce': bd['nonce'], - 'prev_block': bd['previousblockhash'], - 'time': datetime.fromtimestamp(bd['time'], timezone.utc), - 'total_txs': len(bd['tx']), - 'txs': txs, - 'version': bd['version'], - 'page': page, - 'pages': None if not limit else int(len(bd['tx']) // limit) + (len(bd['tx']) % limit > 0), - 'limit': limit - } - return block - - def isspent(self, txid, output_n): - t = self.gettransaction(txid) - return 1 if t.outputs[output_n].spent else 0 - - def getinfo(self): - info = self.compose_request('status', '')['info'] - return { - 'blockcount': info['blocks'], - 'chain': info['network'], - 'difficulty': int(float(info['difficulty'])), - 'hashrate': 0, - 'mempool_size': 0, - } diff --git a/bitcoinlib/services/mempool.py b/bitcoinlib/services/mempool.py index 2cd6ef9a..e435db9e 100644 --- a/bitcoinlib/services/mempool.py +++ b/bitcoinlib/services/mempool.py @@ -112,7 +112,7 @@ def _parse_transaction(self, tx): t.add_input(prev_txid=ti['txid'], output_n=ti['vout'], unlocking_script=ti['scriptsig'], value=ti['prevout']['value'], address=ti['prevout'].get('scriptpubkey_address', ''), - unlocking_script_unsigned=ti['prevout']['scriptpubkey'], sequence=ti['sequence'], + locking_script=ti['prevout']['scriptpubkey'], sequence=ti['sequence'], witnesses=None if 'witness' not in ti else [bytes.fromhex(w) for w in ti['witness']], strict=self.strict) for to in tx['vout']: @@ -154,7 +154,12 @@ def getrawtransaction(self, txid): return self.compose_request('tx', txid, 'hex') def sendrawtransaction(self, rawtx): - return self.compose_request('tx', post_data=rawtx, method='post') + res = self.compose_request('tx', post_data=rawtx, method='post') + _logger.debug('mempool response: %s', res) + return { + 'txid': res, + 'response_dict': {} + } def estimatefee(self, blocks): estimates = self.compose_request('v1/fees', 'recommended') diff --git a/bitcoinlib/services/services.py b/bitcoinlib/services/services.py index 0b7b747b..522bab08 100644 --- a/bitcoinlib/services/services.py +++ b/bitcoinlib/services/services.py @@ -25,7 +25,7 @@ from sqlalchemy import func from bitcoinlib import services from bitcoinlib.networks import Network -from bitcoinlib.encoding import to_bytes, int_to_varbyteint, varstr, varbyteint_to_int +from bitcoinlib.encoding import to_bytes, int_to_varbyteint, varstr from bitcoinlib.db_cache import * from bitcoinlib.transactions import Transaction, transaction_update_spents from bitcoinlib.blocks import Block @@ -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. @@ -78,6 +78,8 @@ def __init__(self, network=DEFAULT_NETWORK, min_providers=1, max_providers=1, pr :type exclude_providers: list of str :param strict: Strict checks of valid signatures, scripts and transactions. Normally use strict=True for wallets, transaction validations etcetera. For blockchain parsing strict=False should be used, but be sure to check warnings in the log file. Default is True. :type strict: bool + :param wallet_name: Name of wallet if connecting to bitcoin node + :type wallet_name: str """ @@ -113,9 +115,9 @@ def __init__(self, network=DEFAULT_NETWORK, min_providers=1, max_providers=1, pr if (self.providers_defined[p]['network'] == network or self.providers_defined[p]['network'] == '') and \ (not providers or self.providers_defined[p]['provider'] in providers): self.providers.update({p: self.providers_defined[p]}) - for nop in exclude_providers: - if nop in self.providers: - del(self.providers[nop]) + exclude_providers_keys = {pi: self.providers[pi]['provider'] for pi in self.providers if self.providers[pi]['provider'] in exclude_providers}.keys() + for provider_key in exclude_providers_keys: + del(self.providers[provider_key]) if not self.providers: raise ServiceError("No providers found for network %s" % network) @@ -131,6 +133,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: @@ -140,16 +143,11 @@ def __init__(self, network=DEFAULT_NETWORK, min_providers=1, max_providers=1, pr self.ignore_priority = ignore_priority self.strict = strict if self.min_providers > 1: - self._blockcount = Service(network=network, cache_uri=cache_uri).blockcount() + self._blockcount = Service(network=network, cache_uri=cache_uri, providers=providers, + exclude_providers=exclude_providers, timeout=timeout).blockcount() else: self._blockcount = self.blockcount() - def __exit__(self): - try: - self.cache.session.close() - except Exception: - pass - def _reset_results(self): self.results = {} self.errors = {} @@ -167,15 +165,17 @@ def _provider_execute(self, method, *arguments): if self.resultcount >= self.max_providers: break try: - if sp not in ['bitcoind', 'litecoind', 'dashd', 'dogecoind', 'caching'] and not self.providers[sp]['url'] and \ + if sp not in ['bitcoind', 'litecoind', 'dogecoind', 'caching'] and not self.providers[sp]['url'] and \ self.network.name != 'bitcoinlib_test': continue client = getattr(services, self.providers[sp]['provider']) providerclient = getattr(client, self.providers[sp]['client_class']) + pc_instance = providerclient( self.network, self.providers[sp]['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) + self.providers[sp]['network_overrides'], self.timeout, self._blockcount, self.strict, + self.wallet_name) if not hasattr(pc_instance, method): _logger.debug("Method %s not found for provider %s" % (method, sp)) continue @@ -388,11 +388,11 @@ def gettransactions(self, address, after_txid='', limit=MAX_TRANSACTIONS): if len(txs): last_txid = bytes.fromhex(txs[-1:][0].txid) if len(self.results): - order_n = 0 + index = 0 for t in txs: if t.confirmations != 0: - res = self.cache.store_transaction(t, order_n, commit=False) - order_n += 1 + res = self.cache.store_transaction(t, index, commit=False) + index += 1 # Failure to store transaction: stop caching transaction and store last tx block height - 1 if res is False: if t.block_height: @@ -439,12 +439,12 @@ def sendrawtransaction(self, rawtx): """ return self._provider_execute('sendrawtransaction', rawtx) - def estimatefee(self, blocks=3, priority=''): + def estimatefee(self, blocks=5, priority=''): """ Estimate fee per kilobyte for a transaction for this network with expected confirmation within a certain amount of blocks - :param blocks: Expected confirmation time in blocks. Default is 3. + :param blocks: Expected confirmation time in blocks. :type blocks: int :param priority: Priority for transaction: can be 'low', 'medium' or 'high'. Overwrites value supplied in 'blocks' argument :type priority: str @@ -454,9 +454,9 @@ def estimatefee(self, blocks=3, priority=''): self.results_cache_n = 0 if priority: if priority == 'low': - blocks = 10 + blocks = 25 elif priority == 'high': - blocks = 1 + blocks = 2 if self.min_providers <= 1: # Disable cache if comparing providers fee = self.cache.estimatefee(blocks) if fee: @@ -562,11 +562,11 @@ def getblock(self, blockid, parse_transactions=True, page=1, limit=None): block.page = page if parse_transactions and self.min_providers <= 1: - order_n = (page-1)*limit + index = (page-1)*limit for tx in block.transactions: if isinstance(tx, Transaction): - self.cache.store_transaction(tx, order_n, commit=False) - order_n += 1 + self.cache.store_transaction(tx, index, commit=False) + index += 1 self.cache.commit() self.complete = True if len(block.transactions) == block.tx_count else False self.cache.store_block(block) @@ -694,12 +694,6 @@ def __init__(self, network, db_uri=''): self.session = DbCache(db_uri=db_uri).session self.network = network - def __exit__(self): - try: - self.session.close() - except Exception: - pass - def cache_enabled(self): """ Check if caching is enabled. Returns False if SERVICE_CACHING_ENABLED is False or no session is defined. @@ -728,7 +722,8 @@ def commit(self): def _parse_db_transaction(db_tx): t = Transaction(locktime=db_tx.locktime, version=db_tx.version, network=db_tx.network_name, fee=db_tx.fee, txid=db_tx.txid.hex(), date=db_tx.date, confirmations=db_tx.confirmations, - block_height=db_tx.block_height, status='confirmed', witness_type=db_tx.witness_type.value) + block_height=db_tx.block_height, status='confirmed', witness_type=db_tx.witness_type.value, + index=db_tx.index) for n in db_tx.nodes: if n.is_input: if n.ref_txid == b'\00' * 32: @@ -805,7 +800,7 @@ def gettransactions(self, address, after_txid='', limit=MAX_TRANSACTIONS): filter(DbCacheTransactionNode.address == address, DbCacheTransaction.block_height >= after_tx.block_height, DbCacheTransaction.block_height <= db_addr.last_block).\ - order_by(DbCacheTransaction.block_height, DbCacheTransaction.order_n).all() + order_by(DbCacheTransaction.block_height, DbCacheTransaction.index).all() db_txs2 = [] for d in db_txs: db_txs2.append(d) @@ -817,7 +812,7 @@ def gettransactions(self, address, after_txid='', limit=MAX_TRANSACTIONS): else: db_txs = self.session.query(DbCacheTransaction).join(DbCacheTransactionNode). \ filter(DbCacheTransactionNode.address == address). \ - order_by(DbCacheTransaction.block_height, DbCacheTransaction.order_n).all() + order_by(DbCacheTransaction.block_height, DbCacheTransaction.index).all() for db_tx in db_txs: t = self._parse_db_transaction(db_tx) if t: @@ -847,8 +842,8 @@ def getblocktransactions(self, height, page, limit): n_from = (page-1) * limit n_to = page * limit db_txs = self.session.query(DbCacheTransaction).\ - filter(DbCacheTransaction.block_height == height, DbCacheTransaction.order_n >= n_from, - DbCacheTransaction.order_n < n_to).all() + filter(DbCacheTransaction.block_height == height, DbCacheTransaction.index >= n_from, + DbCacheTransaction.index < n_to).all() txs = [] for db_tx in db_txs: t = self._parse_db_transaction(db_tx) @@ -877,7 +872,7 @@ def getutxos(self, address, after_txid=''): """ Get list of unspent outputs (UTXO's) for specified address from database cache. - Sorted from old to new, so highest number of confirmations first. + Sorted from old to new, so the highest number of confirmations first. :param address: Address string :type address: str @@ -892,7 +887,7 @@ def getutxos(self, address, after_txid=''): DbCacheTransactionNode.value, DbCacheTransaction.confirmations, DbCacheTransaction.block_height, DbCacheTransaction.fee, DbCacheTransaction.date, DbCacheTransaction.txid).join(DbCacheTransaction). \ - order_by(DbCacheTransaction.block_height, DbCacheTransaction.order_n). \ + order_by(DbCacheTransaction.block_height, DbCacheTransaction.index). \ filter(DbCacheTransactionNode.address == address, DbCacheTransactionNode.is_input == False, DbCacheTransaction.network_name == self.network.name).all() utxos = [] @@ -1002,14 +997,14 @@ def store_blockcount(self, blockcount): self.session.merge(dbvar) self.commit() - def store_transaction(self, t, order_n=None, commit=True): + def store_transaction(self, t, index=None, commit=True): """ Store transaction in cache. Use order number to determine order in a block :param t: Transaction :type t: Transaction - :param order_n: Order in block - :type order_n: int + :param index: Order in block + :type index: int :param commit: Commit transaction to database. Default is True. Can be disabled if a larger number of transactions are added to cache, so you can commit outside this method. :type commit: bool @@ -1034,7 +1029,7 @@ def store_transaction(self, t, order_n=None, commit=True): return new_tx = DbCacheTransaction(txid=txid, date=t.date, confirmations=t.confirmations, block_height=t.block_height, network_name=t.network.name, - fee=t.fee, order_n=order_n, version=t.version_int, + fee=t.fee, index=index, version=t.version_int, locktime=t.locktime, witness_type=t.witness_type) self.session.add(new_tx) for i in t.inputs: diff --git a/tests/benchmark.py b/bitcoinlib/tools/benchmark.py similarity index 61% rename from tests/benchmark.py rename to bitcoinlib/tools/benchmark.py index c59ca846..facac8b6 100644 --- a/tests/benchmark.py +++ b/bitcoinlib/tools/benchmark.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- # # BitcoinLib - Python Cryptocurrency Library -# Benchmark - test library speed -# © 2020 October - 1200 Web Development +# Benchmark - Test library speed +# © 2020 - 2024 Februari - 1200 Web Development # import time import random +import json from bitcoinlib.keys import * from bitcoinlib.wallets import * from bitcoinlib.transactions import * @@ -21,26 +22,43 @@ try: BITCOINLIB_VERSION except: - BITCOINLIB_VERSION = '<0.4.10' + BITCOINLIB_VERSION = '0.4.10 and below' class Benchmark: def __init__(self): wallet_delete_if_exists('wallet_multisig_huge', force=True) + wallet_delete_if_exists('wallet_large', force=True) @staticmethod def benchmark_bip38(): # Encrypt and decrypt BIP38 key k = Key() - bip38_key = k.encrypt(password='satoshi') - k2 = Key(bip38_key, password='satoshi') + if BITCOINLIB_VERSION > '0.6.0': + bip38_key = k.encrypt(password='satoshi') + else: + bip38_key = k.bip38_encrypt('satoshi') + if BITCOINLIB_VERSION > '0.5.1': + k2 = Key(bip38_key, password='satoshi') + else: + k2 = Key(bip38_key, passphrase='satoshi') assert(k.wif() == k2.wif()) + @staticmethod + def benchmark_create_key(): + for i in range(0, 1000): + k = Key() + + @staticmethod + def benchmark_create_hdkey(): + for i in range(0, 1000): + k = HDKey() + @staticmethod def benchmark_encoding(): # Convert very large numbers to and from base58 / bech32 - pk = random.randint(0, 10 ** 10240) + pk = random.randint(0, 10 ** 4000) large_b58 = change_base(pk, 10, 58, 6000) large_b32 = change_base(pk, 10, 32, 7000) assert(change_base(large_b58, 58, 10) == pk) @@ -51,14 +69,17 @@ def benchmark_mnemonic(): # Generate Mnemonic passphrases for i in range(100): m = Mnemonic().generate(256) - Mnemonic().to_entropy(m) + Mnemonic().to_entropy(m, includes_checksum=False) @staticmethod def benchmark_transactions(): # Deserialize transaction and verify raw_hex = "02000000000101b7006080d9d1d2928f70be1140d4af199d6ba4f9a7b0096b6461d7d4d16a96470600000000fdffffff11205c0600000000001976a91416e7a7d921edff13eaf5831eefd6aaca5728d7fb88acad960700000000001600140dd69a4ce74f03342cd46748fc40a877c7ccef0e808b08000000000017a914bd27a59ba92179389515ecea6b87824a42e002ee873efb0b0000000000160014b4a3a8da611b66123c19408c289faa04c71818d178b21100000000001976a914496609abfa498b6edbbf83e93fd45c1934e05b9888ac34d01900000000001976a9144d1ce518b35e19f413963172bd2c84bd90f8f23488ace06e1f00000000001976a914440d99e9e2879c1b0f8e9a1d5a288a4b6cfcc15288acff762c000000000016001401429b4b17e97f8d4419b4594ffe9f54e85037e7241e4500000000001976a9146083df8eb862f759ea0f1c04d3f13a3dfa9aff5888acf09056000000000017a9144fcaf4edac9da6890c09a819d0d7b8f300edbe478740fa97000000000017a9147431dcb6061217b0c80c6fa0c0256c1221d74b4a87208e9c000000000017a914a3e1e764fefa92fc5befa179b2b80afd5a9c20bd87ecf09f000000000017a9142ca7dc95f76530521a1edfc439586866997a14828754900101000000001976a9142e6c1941e2f9c47b535d0cf5dc4be5038e02336588acc0996d01000000001976a91492268fb9d7b8a3c825a4efc486a0679dbf006fae88acd790ae0300000000160014fe350625e2887e9bc984a69a7a4f60439e7ee7152182c81300000000160014f60834ef165253c571b11ce9fa74e46692fc5ec10248304502210081cb31e1b53a36409743e7c785e00d5df7505ca2373a1e652fec91f00c15746b02203167d7cc1fa43e16d411c620b90d9516cddac31d9e44e452651f50c950dc94150121026e5628506ecd33242e5ceb5fdafe4d3066b5c0f159b3c05a621ef65f177ea28600000000" for i in range(100): - t = Transaction.import_raw(raw_hex) + if BITCOINLIB_VERSION >= '0.5.3': + t = Transaction.parse(raw_hex) + else: + t = Transaction.import_raw(raw_hex) t.inputs[0].value = 485636658 t.verify() assert(t.verified is True) @@ -74,10 +95,17 @@ def benchmark_wallets_multisig(): key_list_cosigners = [k.public_master(multisig=True) for k in key_list if k is not key_list[pk_n]] key_list_wallet = [key_list[pk_n]] + key_list_cosigners w = wallet_method.create('wallet_multisig_huge', keys=key_list_wallet, sigs_required=sigs_req, network=network) - w.get_keys(number_of_keys=2) + + if BITCOINLIB_VERSION >= '0.5.0': + w.get_keys(number_of_keys=2) + else: + w.get_key(number_of_keys=2) w.utxos_update() to_address = HDKey(network=network).address() - t = w.sweep(to_address, offline=True) + if BITCOINLIB_VERSION >= '0.7.0': + t = w.sweep(to_address, broadcast=False) + else: + t = w.sweep(to_address, offline=True) key_pool = [i for i in range(0, n_keys - 1) if i != pk_n] while len(t.inputs[0].signatures) < sigs_req: co_id = random.choice(key_pool) @@ -85,20 +113,41 @@ def benchmark_wallets_multisig(): key_pool.remove(co_id) assert(t.verify() is True) - def run(self): + @staticmethod + def benchmark_wallets_large(): + # Create large wallet with many keys + network = 'bitcoinlib_test' + n_keys = 250 + w = wallet_method.create('wallet_large', network=network) + if BITCOINLIB_VERSION >= '0.5.0': + w.get_keys(number_of_keys=n_keys) + else: + w.get_key(number_of_keys=n_keys) + + def run(self, only_dict=False): start_time = time.time() - print("Running BitcoinLib benchmarks speed test for version %s" % BITCOINLIB_VERSION) + bench_dict = {'version': BITCOINLIB_VERSION} + only_dict or print("Running BitcoinLib benchmarks speed test for version %s" % BITCOINLIB_VERSION) benchmark_methods = [m for m in dir(self) if callable(getattr(self, m)) if m.startswith('benchmark_')] for method in benchmark_methods: m_start_time = time.time() - getattr(self, method)() - m_duration = time.time() - m_start_time - print("%s, %.5f seconds" % (method, m_duration)) + try: + getattr(self, method)() + except Exception as e: + only_dict or print("Error occured running test: %s" % str(e)) + m_duration = 0 + else: + m_duration = time.time() - m_start_time + only_dict or print("%s, %.5f seconds" % (method, m_duration)) + bench_dict.update({method: m_duration}) duration = time.time() - start_time - print("Total running time: %.5f seconds" % duration) + only_dict or print("Total running time: %.5f seconds" % duration) + bench_dict.update({'duration': duration}) + return bench_dict if __name__ == '__main__': - Benchmark().run() + res = Benchmark().run(bool(sys.argv[1:])) + print(json.dumps(res)) diff --git a/bitcoinlib/tools/clw.py b/bitcoinlib/tools/clw.py index 84279944..78263455 100644 --- a/bitcoinlib/tools/clw.py +++ b/bitcoinlib/tools/clw.py @@ -2,13 +2,14 @@ # # BitcoinLib - Python Cryptocurrency Library # -# CLW - Command Line Wallet manager. -# Create and manage BitcoinLib legacy/segwit single and multisignatures wallets from the commandline +# CLW - Command Line Wallet manager +# Create and manage BitcoinLib legacy, segwit single and multi-signature wallets from the commandline # -# © 2019 November - 1200 Web Development +# © 2019 - 2024 January - 1200 Web Development # import sys +import os import argparse import ast from pprint import pprint @@ -16,7 +17,7 @@ from bitcoinlib.mnemonic import Mnemonic from bitcoinlib.keys import HDKey from bitcoinlib.main import BITCOINLIB_VERSION - +from bitcoinlib.config.config import DEFAULT_NETWORK try: import pyqrcode QRCODES_AVAILABLE = True @@ -24,385 +25,403 @@ QRCODES_AVAILABLE = False -DEFAULT_NETWORK = 'bitcoin' +# Show all errors in simple format without tracelog +def exception_handler(exception_type, exception, traceback): + print("%s: %s" % (exception_type.__name__, exception)) + + +sys.excepthook = exception_handler def parse_args(): parser = argparse.ArgumentParser(description='BitcoinLib command line wallet') - parser.add_argument('wallet_name', nargs='?', default='', - help="Name of wallet to create or open. Used to store your all your wallet keys " - "and will be printed on each paper wallet") + parser.add_argument('--list-wallets', '-l', action='store_true', + help="List all known wallets in database") + parser.add_argument('--generate-key', '-g', action='store_true', help="Generate a new masterkey, and" + " show passphrase, WIF and public account key. Can be used to create a new (multisig) wallet") + parser.add_argument('--passphrase-strength', type=int, default=128, + help="Number of bits for passphrase key. Default is 128, lower is not advised but can " + "be used for testing. Set to 256 bits for more future-proof passphrases") + parser.add_argument('--database', '-d', + help="URI of the database to use",) + parser.add_argument('--wallet_name', '-w', nargs='?', default='', + help="Name of wallet to open. Provide wallet name or number when running wallet actions") + parser.add_argument('--network', '-n', + help="Specify 'bitcoin', 'litecoin', 'testnet' or other supported network") + parser.add_argument('--witness-type', '-j', metavar='WITNESS_TYPE', default=None, + help='Witness type of wallet: legacy, p2sh-segwit or segwit (default)') + parser.add_argument('--yes', '-y', action='store_true', default=False, + help='Non-interactive mode, does not prompt for confirmation') + parser.add_argument('--quiet', '-q', action='store_true', + help='Quiet mode, no output writen to console') + + subparsers = parser.add_subparsers(required=False, dest='subparser_name') + parser_new = subparsers.add_parser('new', description="Create new wallet") + parser_new.add_argument('--wallet_name', '-w', nargs='?', default='', required=True, + help="Name of wallet to create or open. Provide wallet name or number when running wallet " + "actions") + parser_new.add_argument('--password', + help='Password for BIP38 encrypted key. Use to create a wallet from a protected key') + parser_new.add_argument('--network', '-n', + help="Specify 'bitcoin', 'litecoin', 'testnet' or other supported network") + parser_new.add_argument('--passphrase', default=None, metavar="PASSPHRASE", + help="Passphrase to recover or create a wallet. Usually 12 or 24 words") + parser_new.add_argument('--create-from-key', '-c', metavar='KEY', + help="Create a new wallet from specified key") + parser_new.add_argument('--create-multisig', '-m', nargs='*', metavar='.', + help='[NUMBER_OF_SIGNATURES_REQUIRED, NUMBER_OF_SIGNATURES, KEY-1, KEY-2, ... KEY-N]' + 'Specify number of signatures followed by the number of signatures required and ' + 'then a list of public or private keys for this wallet. Private keys will be ' + 'created if not provided in key list.' + '\nExample, create a 2-of-2 multisig wallet and provide 1 key and create another ' + 'key: -m 2 2 tprv8ZgxMBicQKsPd1Q44tfDiZC98iYouKRC2CzjT3HGt1yYw2zuX2awTotzGAZQ' + 'EAU9bi2M5MCj8iedP9MREPjUgpDEBwBgGi2C8eK5zNYeiX8 tprv8ZgxMBicQKsPeUbMS6kswJc11zgV' + 'EXUnUZuGo3bF6bBrAg1ieFfUdPc9UHqbD5HcXizThrcKike1c4z6xHrz6MWGwy8L6YKVbgJMeQHdWDp') + parser_new.add_argument('--witness-type', '-j', metavar='WITNESS_TYPE', default=None, + help='Witness type of wallet: legacy, p2sh-segwit or segwit (default)') + parser_new.add_argument('--cosigner-id', '-o', type=int, default=None, + help='Set this if wallet contains only public keys, more then one private key or if ' + 'you would like to create keys for other cosigners.') + parser_new.add_argument('--database', '-d', + help="URI of the database to use",) + parser_new.add_argument('--receive', '-r', action='store_true', + help="Show unused address to receive funds.") + parser_new.add_argument('--yes', '-y', action='store_true', default=False, + help='Non-interactive mode, does not prompt for confirmation') + parser_new.add_argument('--quiet', '-q', action='store_true', + help='Quiet mode, no output writen to console.') + parser_new.add_argument('--disable-anti-fee-sniping', action='store_true', default=False, + help='Disable anti-fee-sniping, and set locktime in all transaction to zero.') group_wallet = parser.add_argument_group("Wallet Actions") group_wallet.add_argument('--wallet-remove', action='store_true', help="Name or ID of wallet to remove, all keys and transactions will be deleted") - group_wallet.add_argument('--list-wallets', '-l', action='store_true', - help="List all known wallets in BitcoinLib database") - group_wallet.add_argument('--wallet-info', '-w', action='store_true', + group_wallet.add_argument('--wallet-info', '-i', action='store_true', help="Show wallet information") group_wallet.add_argument('--update-utxos', '-x', action='store_true', help="Update unspent transaction outputs (UTXO's) for this wallet") group_wallet.add_argument('--update-transactions', '-u', action='store_true', help="Update all transactions and UTXO's for this wallet") - group_wallet.add_argument('--wallet-recreate', '-z', action='store_true', - help="Delete all keys and transactions and recreate wallet, except for the masterkey(s)." - " Use when updating fails or other errors occur. Please backup your database and " - "masterkeys first.") - group_wallet.add_argument('--receive', '-r', nargs='?', type=int, - help="Show unused address to receive funds. Specify cosigner-id to generate address for " - "specific cosigner. Default is -1 for own wallet", - const=-1, metavar='COSIGNER_ID') - group_wallet.add_argument('--generate-key', '-g', action='store_true', help="Generate a new masterkey, and show" - " passphrase, WIF and public account key. Can be used to create a multisig wallet") + group_wallet.add_argument('--wallet-empty', '-z', action='store_true', + help="Delete all keys and transactions from wallet, except for the masterkey(s). " + "Use when updating fails or other errors occur. Please backup your database and " + "masterkeys first. Update empty wallet again to restore your wallet.") + group_wallet.add_argument('--receive', '-r', action='store_true', + help="Show unused address to receive funds.") + group_wallet.add_argument('--cosigner-id', '-o', type=int, default=None, + help='Set this if wallet contains only public keys, more then one private key or if ' + 'you would like to create keys for other cosigners.') group_wallet.add_argument('--export-private', '-e', action='store_true', help="Export private key for this wallet and exit") - group_wallet.add_argument('--import-private', '-k', + group_wallet.add_argument('--import-private', '-v', help="Import private key in this wallet") - group_wallet2 = parser.add_argument_group("Wallet Setup") - group_wallet2.add_argument('--passphrase', nargs="*", default=None, - help="Passphrase to recover or create a wallet. Usually 12 or 24 words") - group_wallet2.add_argument('--passphrase-strength', type=int, default=128, - help="Number of bits for passphrase key. Default is 128, lower is not adviced but can " - "be used for testing. Set to 256 bits for more future proof passphrases") - group_wallet2.add_argument('--network', '-n', - help="Specify 'bitcoin', 'litecoin', 'testnet' or other supported network") - group_wallet2.add_argument('--database', '-d', - help="URI of the database to use",) - group_wallet2.add_argument('--create-from-key', '-c', metavar='KEY', - help="Create a new wallet from specified key") - group_wallet2.add_argument('--create-multisig', '-m', nargs='*', - metavar='.', - help='[NUMBER_OF_SIGNATURES, NUMBER_OF_SIGNATURES_REQUIRED, [KEY1, KEY2, ... KEY3]]' - 'Specificy number of signatures followed by the number of signatures required and ' - 'then a list of public or private keys for this wallet. Private keys will be ' - 'created if not provided in key list.' - '\nExample, create a 2-of-2 multisig wallet and provide 1 key and create another ' - 'key: -m 2 2 tprv8ZgxMBicQKsPd1Q44tfDiZC98iYouKRC2CzjT3HGt1yYw2zuX2awTotzGAZQ' - 'EAU9bi2M5MCj8iedP9MREPjUgpDEBwBgGi2C8eK5zNYeiX8 tprv8ZgxMBicQKsPeUbMS6kswJc11zgV' - 'EXUnUZuGo3bF6bBrAg1ieFfUdPc9UHqbD5HcXizThrcKike1c4z6xHrz6MWGwy8L6YKVbgJMeQHdWDp') - group_wallet2.add_argument('--witness-type', '-y', metavar='WITNESS_TYPE', default=None, - help='Witness type of wallet: lecacy (default), p2sh-segwit or segwit') - group_wallet2.add_argument('--cosigner-id', '-s', type=int, default=0, - help='Set this if wallet contains only public keys, more then one private key or if ' - 'you would like to create keys for other cosigners.') group_transaction = parser.add_argument_group("Transactions") - group_transaction.add_argument('--create-transaction', '-t', metavar=('ADDRESS_1', 'AMOUNT_1'), - help="Create transaction. Specify address followed by amount in satoshis. Repeat for multiple " - "outputs", nargs='*') + group_transaction.add_argument('--send', '-s', metavar=('ADDRESS', 'AMOUNT'), nargs=2, + action='append', + help="Create transaction to send amount to specified address. To send to " + "multiple addresses, argument can be used multiple times.") + group_transaction.add_argument('--number-of-change-outputs', type=int, default=1, + help="Number of change outputs. Default is 1, increase for more privacy or " + "to split funds") + group_transaction.add_argument('--input-key-id', '-k', type=int, default=None, + help="Use to create transaction with 1 specific key ID") group_transaction.add_argument('--sweep', metavar="ADDRESS", help="Sweep wallet, transfer all funds to specified address") group_transaction.add_argument('--fee', '-f', type=int, help="Transaction fee") - group_transaction.add_argument('--fee-per-kb', type=int, - help="Transaction fee in sathosis (or smallest denominator) per kilobyte") - group_transaction.add_argument('--push', '-p', action='store_true', help="Push created transaction to the network") - group_transaction.add_argument('--import-tx', '-i', metavar="TRANSACTION", + group_transaction.add_argument('--fee-per-kb', '-b', type=int, + help="Transaction fee in satoshi per kilobyte") + group_transaction.add_argument('--push', '-p', action='store_true', + help="Push created transaction to the network") + group_transaction.add_argument('--import-tx', metavar="TRANSACTION", help="Import raw transaction hash or transaction dictionary in wallet and sign " "it with available key(s)") group_transaction.add_argument('--import-tx-file', '-a', metavar="FILENAME_TRANSACTION", help="Import transaction dictionary or raw transaction string from specified " "filename and sign it with available key(s)") + group_transaction.add_argument('--rbf', action='store_true', + help="Enable replace-by-fee flag. Allow to replace transaction with a new one " + "with higher fees, to avoid transactions taking to long to confirm.") pa = parser.parse_args() - if pa.receive and pa.create_transaction: - parser.error("Please select receive or create transaction option not both") - if pa.wallet_name: - pa.wallet_info = True - else: + + if not pa.wallet_name: pa.list_wallets = True return pa -def get_passphrase(args): - inp_passphrase = Mnemonic('english').generate(args.passphrase_strength) - print("\nYour mnemonic private key sentence is: %s" % inp_passphrase) - print("\nPlease write down on paper and backup. With this key you can restore your wallet and all keys") - passphrase = inp_passphrase.split(' ') - inp = input("\nType 'yes' if you understood and wrote down your key: ") - if inp not in ['yes', 'Yes', 'YES']: - clw_exit("Exiting...") +def get_passphrase(strength, interactive=False, quiet=False): + passphrase = Mnemonic().generate(strength) + if not quiet: + print("Passphrase: %s" % passphrase) + print("Please write down on paper and backup. With this key you can restore your wallet and all keys") + if not interactive and input("\nType 'yes' if you understood and wrote down your key: ") not in ['yes', 'Yes', 'YES']: + print("Exiting...") + sys.exit() return passphrase -def create_wallet(wallet_name, args, db_uri): +def create_wallet(wallet_name, args, db_uri, output_to): if args.network is None: args.network = DEFAULT_NETWORK - print("\nCREATE wallet '%s' (%s network)" % (wallet_name, args.network)) + print("CREATE wallet '%s' (%s network)" % (wallet_name, args.network), file=output_to) if args.create_multisig: if not isinstance(args.create_multisig, list) or len(args.create_multisig) < 2: - clw_exit("Please enter multisig creation parameter in the following format: " - " " - " [ ... ]") + raise WalletError("Please enter multisig creation parameter in the following format: " + " " + " [ ... ]") try: - sigs_total = int(args.create_multisig[0]) + sigs_required = int(args.create_multisig[0]) except ValueError: - clw_exit("Number of total signatures (first argument) must be a numeric value. %s" % + raise WalletError("Number of signatures required (first argument) must be a numeric value. %s" % args.create_multisig[0]) try: - sigs_required = int(args.create_multisig[1]) + sigs_total = int(args.create_multisig[1]) except ValueError: - clw_exit("Number of signatures required (second argument) must be a numeric value. %s" % + raise WalletError("Number of total signatures (second argument) must be a numeric value. %s" % args.create_multisig[1]) key_list = args.create_multisig[2:] keys_missing = sigs_total - len(key_list) - assert(keys_missing >= 0) + if keys_missing < 0: + raise WalletError("Invalid number of keys (%d required)" % sigs_total) if keys_missing: - print("Not all keys provided, creating %d additional key(s)" % keys_missing) + print("Not all keys provided, creating %d additional key(s)" % keys_missing, file=output_to) for _ in range(keys_missing): - passphrase = get_passphrase(args) - passphrase = ' '.join(passphrase) - seed = Mnemonic().to_seed(passphrase).hex() - key_list.append(HDKey.from_seed(seed, network=args.network)) + passphrase = get_passphrase(args.passphrase_strength, args.yes, args.quiet) + key_list.append(HDKey.from_passphrase(passphrase, network=args.network)) return Wallet.create(wallet_name, key_list, sigs_required=sigs_required, network=args.network, - cosigner_id=args.cosigner_id, db_uri=db_uri, witness_type=args.witness_type) + cosigner_id=args.cosigner_id, db_uri=db_uri, witness_type=args.witness_type, + anti_fee_sniping=not(args.disable_anti_fee_sniping)) elif args.create_from_key: from bitcoinlib.keys import get_key_format import_key = args.create_from_key kf = get_key_format(import_key) if kf['format'] == 'wif_protected': - password = input('Key password? ') - import_key, _ = HDKey._bip38_decrypt(import_key, password) + if not args.password: + raise WalletError("This is a WIF protected key, please provide a password with the --password argument.") + import_key, _ = HDKey._bip38_decrypt(import_key, args.password, args.network, args.witness_type) return Wallet.create(wallet_name, import_key, network=args.network, db_uri=db_uri, witness_type=args.witness_type) else: passphrase = args.passphrase if passphrase is None: - passphrase = get_passphrase(args) - elif not passphrase: - passphrase = input("Enter Passphrase: ") - if not isinstance(passphrase, list): - passphrase = passphrase.split(' ') - elif len(passphrase) == 1: - passphrase = passphrase[0].split(' ') - if len(passphrase) < 12: - clw_exit("Please specify passphrase with 12 words or more") - passphrase = ' '.join(passphrase) - seed = Mnemonic().to_seed(passphrase).hex() - hdkey = HDKey.from_seed(seed, network=args.network) + passphrase = get_passphrase(args.passphrase_strength, args.yes, args.quiet) + if len(passphrase.split(' ')) < 3: + raise WalletError("Please specify passphrase with 3 words or more. However less than 12 words is insecure!") + hdkey = HDKey.from_passphrase(passphrase, network=args.network) return Wallet.create(wallet_name, hdkey, network=args.network, witness_type=args.witness_type, - db_uri=db_uri) + password=args.password, db_uri=db_uri) def create_transaction(wlt, send_args, args): - output_arr = [] - while send_args: - if len(send_args) == 1: - raise ValueError("Invalid number of transaction inputs. Use ... ") - try: - amount = int(send_args[1]) - except ValueError: - clw_exit("Amount must be in satoshis, an integer value: %s" % send_args[1]) - output_arr.append((send_args[0], amount)) - send_args = send_args[2:] - return wlt.transaction_create(output_arr=output_arr, network=args.network, fee=args.fee, min_confirms=0) + output_arr = [(address, value) for [address, value] in send_args] + return wlt.transaction_create(output_arr=output_arr, network=args.network, fee=args.fee, min_confirms=0, + input_key_id=args.input_key_id, + number_of_change_outputs=args.number_of_change_outputs, + replace_by_fee=args.rbf) def print_transaction(wt): - tx_dict = { - 'network': wt.network.name, 'fee': wt.fee, 'raw': wt.raw_hex(), 'outputs': [{ - 'address': o.address, 'value': o.value - } for o in wt.outputs], 'inputs': [{ - 'prev_hash': i.prev_txid.hex(), 'output_n': int.from_bytes(i.output_n, 'big'), - 'address': i.address, 'signatures': [{ - 'signature': s.hex(), 'sig_der': s.as_der_encoded(as_hex=True), - 'pub_key': s.public_key.public_hex, - } for s in i.signatures], 'value': i.value - } for i in wt.inputs] - } - pprint(tx_dict) - - -def clw_exit(msg=None): - if msg: - print(msg) - sys.exit() + pprint(wt.as_dict()) def main(): - print("Command Line Wallet - BitcoinLib %s\n" % BITCOINLIB_VERSION) - # --- Parse commandline arguments --- args = parse_args() db_uri = args.database + output_to = open(os.devnull, 'w') if args.quiet else sys.stdout + wlt = None + + print("Command Line Wallet - BitcoinLib %s\n" % BITCOINLIB_VERSION, file=output_to) + # --- General arguments --- + # Generate key if args.generate_key: - passphrase = get_passphrase(args) - passphrase = ' '.join(passphrase) - seed = Mnemonic().to_seed(passphrase).hex() - hdkey = HDKey.from_seed(seed, network=args.network) - print("Private Master key, to create multisig wallet on this machine:\n%s" % hdkey.wif_private()) + passphrase = get_passphrase(args.passphrase_strength, args.yes, args.quiet) + hdkey = HDKey.from_passphrase(passphrase, witness_type=args.witness_type, network=args.network) + if args.quiet: + print(passphrase) + else: + print("Private Master key, to create multisig wallet on this machine:\n%s" % hdkey.wif_private()) print("Public Master key, to share with other cosigner multisig wallets:\n%s" % - hdkey.public_master(witness_type=args.witness_type, multisig=True).wif()) - print("Network: %s" % hdkey.network.name) - clw_exit() - - # List wallets, then exit - if args.list_wallets: - print("BitcoinLib wallets:") - for w in wallets_list(db_uri=db_uri): + hdkey.public_master(witness_type=args.witness_type, multisig=True).wif(), file=output_to) + print("Network: %s" % hdkey.network.name, file=output_to) + + # List wallets + elif args.list_wallets: + print("BitcoinLib wallets:", file=sys.stdout) + wallets = wallets_list(db_uri=db_uri) + if not wallets: + print("No wallets defined yet, use 'new' argument to create a new wallet. See clw new --help " + "for more info.") + for w in wallets: if 'parent_id' in w and w['parent_id']: continue print("[%d] %s (%s) %s" % (w['id'], w['name'], w['network'], w['owner'])) - clw_exit() - - # Delete specified wallet, then exit - if args.wallet_remove: - if not wallet_exists(args.wallet_name, db_uri=db_uri): - clw_exit("Wallet '%s' not found" % args.wallet_name) - inp = input("\nWallet '%s' with all keys and will be removed, without private key it cannot be restored." - "\nPlease retype exact name of wallet to proceed: " % args.wallet_name) - if inp == args.wallet_name: - if wallet_delete(args.wallet_name, force=True, db_uri=db_uri): - clw_exit("\nWallet %s has been removed" % args.wallet_name) - else: - clw_exit("\nError when deleting wallet") + + # Delete specified wallet + elif args.wallet_remove: + wallet_name = args.wallet_name + if args.wallet_name.isdigit(): + wallet_name = int(args.wallet_name) + if not wallet_exists(wallet_name, db_uri=db_uri): + print("Wallet '%s' not found" % args.wallet_name, file=output_to) else: - clw_exit("\nSpecified wallet name incorrect") + inp = wallet_name if (args.quiet or args.yes) else ( + input("Wallet '%s' with all keys and will be removed, without private key it cannot be restored." + "\nPlease retype exact name of wallet to proceed: " % args.wallet_name)) + if str(inp) == str(wallet_name): + if wallet_delete(wallet_name, force=True, db_uri=db_uri): + print("Wallet %s has been removed" % wallet_name, file=output_to) + else: + print("Error when deleting wallet", file=output_to) + else: + print("Specified wallet name incorrect", file=output_to) - wlt = None - if args.wallet_name and not args.wallet_name.isdigit() and not wallet_exists(args.wallet_name, - db_uri=db_uri): - if not args.create_from_key and input( - "Wallet %s does not exist, create new wallet [yN]? " % args.wallet_name).lower() != 'y': - clw_exit('Aborted') - wlt = create_wallet(args.wallet_name, args, db_uri) - args.wallet_info = True - else: - try: - wlt = Wallet(args.wallet_name, db_uri=db_uri) - if args.passphrase is not None: - print("WARNING: Using passphrase option for existing wallet ignored") - if args.create_from_key is not None: - print("WARNING: Using create_from_key option for existing wallet ignored") - except WalletError as e: - clw_exit("Error: %s" % e.msg) + # Create or open wallet + elif args.wallet_name: + if args.subparser_name == 'new': + if wallet_exists(args.wallet_name, db_uri=db_uri): + print("Wallet with name '%s' already exists" % args.wallet_name, file=output_to) + else: + wlt = create_wallet(args.wallet_name, args, db_uri, output_to) + args.wallet_info = True + else: + try: + wlt = Wallet(args.wallet_name, db_uri=db_uri) + except WalletError as e: + print("Error: %s" % e.msg, file=output_to) if wlt is None: - clw_exit("Could not open wallet %s" % args.wallet_name) - - if args.import_private: - if wlt.import_key(args.import_private): - clw_exit("Private key imported") - else: - clw_exit("Failed to import key") - - if args.wallet_recreate: - wallet_empty(args.wallet_name) - print("Removed transactions and generated keys from this wallet") - if args.update_utxos: - wlt.utxos_update() - if args.update_transactions: - wlt.scan(scan_gap_limit=5) - - if args.export_private: - if wlt.scheme == 'multisig': - for w in wlt.cosigner: - if w.main_key and w.main_key.is_private: - print(w.main_key.wif) - elif not wlt.main_key or not wlt.main_key.is_private: - print("No private key available for this wallet") - else: - print(wlt.main_key.wif) - clw_exit() + sys.exit() if args.network is None: args.network = wlt.network.name tx_import = None - if args.import_tx_file: - try: - fn = args.import_tx_file - f = open(fn, "r") - except FileNotFoundError: - clw_exit("File %s not found" % args.import_tx_file) - try: - tx_import = ast.literal_eval(f.read()) - except (ValueError, SyntaxError): - tx_import = f.read() - if args.import_tx: - try: - tx_import = ast.literal_eval(args.import_tx) - except (ValueError, SyntaxError): - tx_import = args.import_tx - if tx_import: - if isinstance(tx_import, dict): - wt = wlt.transaction_import(tx_import) - else: - wt = wlt.transaction_import_raw(tx_import, network=args.network) - wt.sign() - if args.push: - res = wt.send() - if res: - print("Transaction pushed to network. Transaction ID: %s" % wt.txid) + if not args.subparser_name: + if args.import_private: + if wlt.import_key(args.import_private): + print("Private key imported", file=output_to) else: - print("Error creating transaction: %s" % wt.error) - wt.info() - print("Signed transaction:") - print_transaction(wt) - clw_exit() - - if args.receive: - cosigner_id = args.receive - if args.receive == -1: - cosigner_id = None - key = wlt.get_key(network=args.network, cosigner_id=cosigner_id) - print("Receive address: %s" % key.address) - if QRCODES_AVAILABLE: - qrcode = pyqrcode.create(key.address) - print(qrcode.terminal()) - else: - print("Install qr code module to show QR codes: pip install pyqrcode") - clw_exit() - if args.create_transaction == []: - clw_exit("Missing arguments for --create-transaction/-t option") - if args.create_transaction: - if args.fee_per_kb: - clw_exit("Fee-per-kb option not allowed with --create-transaction") - try: - wt = create_transaction(wlt, args.create_transaction, args) - except WalletError as e: - clw_exit("Cannot create transaction: %s" % e.msg) - wt.sign() - print("Transaction created") - wt.info() - if args.push: - wt.send() - if wt.pushed: - print("Transaction pushed to network. Transaction ID: %s" % wt.txid) + print("Failed to import key", file=output_to) + elif args.wallet_empty: + wallet_empty(args.wallet_name, args.database) + print("Removed transactions and emptied wallet. Use --update-wallet option to update again.", + file=output_to) + elif args.update_utxos: + print("Updating wallet utxo's", file=output_to) + wlt.utxos_update() + elif args.update_transactions: + print("Updating wallet transactions", file=output_to) + wlt.scan(scan_gap_limit=3) + elif args.export_private: + if wlt.scheme == 'multisig': + for w in wlt.cosigner: + if w.main_key and w.main_key.is_private: + print(w.main_key.wif) + elif not wlt.main_key or not wlt.main_key.is_private: + print("No private key available for this wallet", file=output_to) else: - print("Error creating transaction: %s" % wt.error) - else: - print("\nTransaction created but not sent yet. Transaction dictionary for export: ") - print_transaction(wt) - clw_exit() - if args.sweep: - if args.fee: - clw_exit("Fee option not allowed with --sweep") - offline = True - print("Sweep wallet. Send all funds to %s" % args.sweep) - if args.push: - offline = False - wt = wlt.sweep(args.sweep, offline=offline, network=args.network, fee_per_kb=args.fee_per_kb) - if not wt: - clw_exit("Error occurred when sweeping wallet: %s. Are UTXO's available and updated?" % wt) - wt.info() - if args.push: - if wt.pushed: - print("Transaction pushed to network. Transaction ID: %s" % wt.txid) - elif not wt: - print("Cannot sweep wallet, are UTXO's updated and available?") + print(wlt.main_key.wif) + elif args.import_tx_file or args.import_tx: + if args.import_tx_file: + try: + fn = args.import_tx_file + f = open(fn, "r") + except FileNotFoundError: + print("File %s not found" % args.import_tx_file, file=output_to) + sys.exit() + try: + tx_import = ast.literal_eval(f.read()) + except (ValueError, SyntaxError): + tx_import = f.read() + elif args.import_tx: + try: + tx_import = ast.literal_eval(args.import_tx) + except (ValueError, SyntaxError): + tx_import = args.import_tx + if tx_import: + if isinstance(tx_import, dict): + wt = wlt.transaction_import(tx_import) + else: + wt = wlt.transaction_import_raw(tx_import, network=args.network) + wt.sign() + if args.push: + res = wt.send() + if res: + if args.quiet: + print(wt.txid) + else: + print("Transaction pushed to network. Transaction ID: %s" % wt.txid) + else: + print("Error creating transaction: %s" % wt.error, file=output_to) + wt.info() + print("Signed transaction:", file=output_to) + if not args.quiet: + print_transaction(wt) + elif args.send: + if args.fee_per_kb: + raise WalletError("Fee-per-kb option not allowed with --send") + try: + wt = create_transaction(wlt, args.send, args) + except WalletError as e: + raise WalletError("Cannot create transaction: %s" % e.msg) + wt.sign() + print("Transaction created", file=output_to) + wt.info() + if args.push: + wt.send() + if wt.pushed: + if args.quiet: + print(wt.txid) + else: + print("Transaction pushed to network. Transaction ID: %s" % wt.txid) + else: + print("Error creating transaction: %s" % wt.error, file=output_to) + else: + print("\nTransaction created but not sent yet. Transaction dictionary for export: ", file=output_to) + if not args.quiet: + print_transaction(wt) + elif args.sweep: + broadcast = False + print("Sweep wallet. Send all funds to %s" % args.sweep, file=output_to) + if args.push: + broadcast = True + wt = wlt.sweep(args.sweep, broadcast=broadcast, network=args.network, fee_per_kb=args.fee_per_kb, fee=args.fee, + replace_by_fee=args.rbf) + if not wt: + raise WalletError("Error occurred when sweeping wallet: %s. Are UTXO's available and updated?" % wt) + wt.info() + if args.push: + if wt.pushed: + if args.quiet: + print(wt.txid) + else: + print("Transaction pushed to network. Transaction ID: %s" % wt.txid) + elif not wt: + print("Cannot sweep wallet, are UTXO's updated and available?", file=output_to) + else: + print("Error sweeping wallet: %s" % wt.error, file=output_to) else: - print("Error sweeping wallet: %s" % wt.error) + print("\nTransaction created but not sent yet. Transaction dictionary for export: ", file=output_to) + print_transaction(wt) + + if args.receive and not (args.send or args.sweep): + key = wlt.get_key(network=args.network, cosigner_id=args.cosigner_id) + if args.quiet: + print(key.address) + else: + print("Receive address: %s" % key.address) + if QRCODES_AVAILABLE: + qrcode = pyqrcode.create(key.address) + print(qrcode.terminal(), file=output_to) else: - print("\nTransaction created but not sent yet. Transaction dictionary for export: ") - print_transaction(wt) - clw_exit() - - # print("Updating wallet") - if args.network == 'bitcoinlib_test': - wlt.utxos_update() - print("Wallet info for %s" % wlt.name) - wlt.info() + print("Install qr code module to show QR codes: pip install pyqrcode", file=output_to) + elif args.wallet_info: + print("Wallet info for %s" % wlt.name, file=output_to) + if not args.quiet: + wlt.info() if __name__ == '__main__': diff --git a/bitcoinlib/tools/import_database.py b/bitcoinlib/tools/import_database.py new file mode 100644 index 00000000..e02d3904 --- /dev/null +++ b/bitcoinlib/tools/import_database.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# +# BitcoinLib - Python Cryptocurrency Library +# IMPORT DATABASE - Extract wallet keys and information from old Bitcoinlib database and move to actual database +# © 2024 Februari - 1200 Web Development +# +# TODO: Currently skips multisig wallets + + +import sqlalchemy as sa +from sqlalchemy.sql import text +from bitcoinlib.main import * +from bitcoinlib.wallets import Wallet, wallet_create_or_open + + +DATABASE_TO_IMPORT = 'sqlite:///' + os.path.join(str(BCL_DATABASE_DIR), 'bitcoinlib_test.sqlite') + +def import_database(): + print(DATABASE_TO_IMPORT) + engine = sa.create_engine(DATABASE_TO_IMPORT) + con = engine.connect() + + wallets = con.execute(text( + 'SELECT w.name, k.private, w.owner, w.network_name, k.account_id, k.address, w.witness_type FROM wallets AS w ' + 'INNER JOIN keys AS k ON w.main_key_id = k.id WHERE multisig=0')).fetchall() + + for wallet in wallets: + print("Import wallet %s" % wallet[0]) + w = wallet_create_or_open(wallet[0], wallet[1], wallet[2], wallet[3], wallet[4], witness_type=wallet[6]) + + +if __name__ == '__main__': + import_database() diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index 5006690d..a9cf0413 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -46,512 +46,6 @@ def __str__(self): return self.msg -@deprecated # Replaced by Transaction.parse() in version 0.6 -def transaction_deserialize(rawtx, network=DEFAULT_NETWORK, check_size=True): # pragma: no cover - """ - Deserialize a raw transaction - - Returns a dictionary with list of input and output objects, locktime and version. - - Will raise an error if wrong number of inputs are found or if there are no output found. - - :param rawtx: Raw transaction as hexadecimal string or bytes - :type rawtx: str, bytes - :param network: Network code, i.e. 'bitcoin', 'testnet', 'litecoin', etc. Leave emtpy for default network - :type network: str, Network - :param check_size: Check if not bytes are left when parsing is finished. Disable when parsing list of transactions, such as the transactions in a raw block. Default is True - :type check_size: bool - - :return Transaction: - """ - - rawtx = to_bytes(rawtx) - coinbase = False - flag = None - witness_type = 'legacy' - - version = rawtx[0:4][::-1] - cursor = 4 - if rawtx[4:5] == b'\0': - flag = rawtx[5:6] - if flag == b'\1': - witness_type = 'segwit' - cursor += 2 - n_inputs, size = varbyteint_to_int(rawtx[cursor:cursor + 9]) - cursor += size - inputs = [] - if not isinstance(network, Network): - network = Network(network) - for n in range(0, n_inputs): - inp_hash = rawtx[cursor:cursor + 32][::-1] - if not len(inp_hash): - raise TransactionError("Input transaction hash not found. Probably malformed raw transaction") - if inp_hash == 32 * b'\0': - coinbase = True - output_n = rawtx[cursor + 32:cursor + 36][::-1] - cursor += 36 - unlocking_script_size, size = varbyteint_to_int(rawtx[cursor:cursor + 9]) - cursor += size - unlocking_script = rawtx[cursor:cursor + unlocking_script_size] - inp_type = 'legacy' - if witness_type == 'segwit' and not unlocking_script_size: - inp_type = 'segwit' - cursor += unlocking_script_size - sequence_number = rawtx[cursor:cursor + 4] - cursor += 4 - inputs.append(Input(prev_txid=inp_hash, output_n=output_n, unlocking_script=unlocking_script, - witness_type=inp_type, sequence=sequence_number, index_n=n, network=network)) - if len(inputs) != n_inputs: - raise TransactionError("Error parsing inputs. Number of tx specified %d but %d found" % (n_inputs, len(inputs))) - - outputs = [] - n_outputs, size = varbyteint_to_int(rawtx[cursor:cursor + 9]) - cursor += size - output_total = 0 - for n in range(0, n_outputs): - value = int.from_bytes(rawtx[cursor:cursor + 8][::-1], 'big') - cursor += 8 - lock_script_size, size = varbyteint_to_int(rawtx[cursor:cursor + 9]) - cursor += size - lock_script = rawtx[cursor:cursor + lock_script_size] - cursor += lock_script_size - outputs.append(Output(value=value, lock_script=lock_script, network=network, output_n=n)) - output_total += value - if not outputs: - raise TransactionError("Error no outputs found in this transaction") - - if witness_type == 'segwit': - for n in range(0, len(inputs)): - n_items, size = varbyteint_to_int(rawtx[cursor:cursor + 9]) - cursor += size - witnesses = [] - for m in range(0, n_items): - witness = b'\0' - item_size, size = varbyteint_to_int(rawtx[cursor:cursor + 9]) - if item_size: - witness = rawtx[cursor + size:cursor + item_size + size] - cursor += item_size + size - witnesses.append(witness) - if witnesses and not coinbase: - script_type = inputs[n].script_type - witness_script_type = 'sig_pubkey' - signatures = [] - keys = [] - sigs_required = 1 - public_hash = b'' - for witness in witnesses: - if witness == b'\0': - continue - if 69 <= len(witness) <= 74 and witness[0:1] == b'\x30': # witness is DER encoded signature - signatures.append(witness) - elif len(witness) == 33 and len(signatures) == 1: # key from sig_pk - keys.append(witness) - else: - rsds = script_deserialize(witness, script_types=['multisig']) - if not rsds['script_type'] == 'multisig': - _logger.warning("Could not parse witnesses in transaction. Multisig redeemscript expected") - witness_script_type = 'unknown' - script_type = 'unknown' - else: - keys = rsds['signatures'] - sigs_required = rsds['number_of_sigs_m'] - witness_script_type = 'p2sh' - script_type = 'p2sh_multisig' - - inp_witness_type = inputs[n].witness_type - usd = script_deserialize(inputs[n].unlocking_script, locking_script=True) - - if usd['script_type'] == "p2wpkh" and witness_script_type == 'sig_pubkey': - inp_witness_type = 'p2sh-segwit' - script_type = 'p2sh_p2wpkh' - elif usd['script_type'] == "p2wsh" and witness_script_type == 'p2sh': - inp_witness_type = 'p2sh-segwit' - script_type = 'p2sh_p2wsh' - inputs[n] = Input(prev_txid=inputs[n].prev_txid, output_n=inputs[n].output_n, keys=keys, - unlocking_script_unsigned=inputs[n].unlocking_script_unsigned, - unlocking_script=inputs[n].unlocking_script, sigs_required=sigs_required, - signatures=signatures, witness_type=inp_witness_type, script_type=script_type, - sequence=inputs[n].sequence, index_n=inputs[n].index_n, public_hash=public_hash, - network=inputs[n].network, witnesses=witnesses) - if len(rawtx[cursor:]) != 4 and check_size: - raise TransactionError("Error when deserializing raw transaction, bytes left for locktime must be 4 not %d" % - len(rawtx[cursor:])) - locktime = int.from_bytes(rawtx[cursor:cursor + 4][::-1], 'big') - - return Transaction(inputs, outputs, locktime, version, network, size=cursor + 4, output_total=output_total, - coinbase=coinbase, flag=flag, witness_type=witness_type, rawtx=rawtx) - - -@deprecated # Replaced by Script class in version 0.6 -def script_deserialize(script, script_types=None, locking_script=None, size_bytes_check=True): # pragma: no cover - """ - Deserialize a script: determine type, number of signatures and script data. - - :param script: Raw script - :type script: str, bytes - :param script_types: Limit script type determination to this list. Leave to default None to search in all script types. - :type script_types: list - :param locking_script: Only deserialize locking scripts. Specify False to only deserialize for unlocking scripts. Default is None for both - :type locking_script: bool - :param size_bytes_check: Check if script or signature starts with size bytes and remove size bytes before parsing. Default is True - :type size_bytes_check: bool - - :return list: With this items: [script_type, data, number_of_sigs_n, number_of_sigs_m] - """ - - def _parse_data(scr, max_items=None, redeemscript_expected=False, item_length=0): - # scr = to_bytes(scr) - items = [] - total_length = 0 - if 69 <= len(scr) <= 74 and scr[:1] == b'\x30': - return [scr], len(scr) - while len(scr) and (max_items is None or max_items > len(items)): - itemlen, size = varbyteint_to_int(scr[0:9]) - if item_length and itemlen != item_length: - break - if not item_length and itemlen not in [20, 33, 65, 70, 71, 72, 73]: - break - if redeemscript_expected and len(scr[itemlen + 1:]) < 20: - break - items.append(scr[1:itemlen + 1]) - total_length += itemlen + size - scr = scr[itemlen + 1:] - return items, total_length - - def _get_empty_data(): - return {'script_type': '', 'keys': [], 'signatures': [], 'hashes': [], 'redeemscript': b'', - 'number_of_sigs_n': 1, 'number_of_sigs_m': 1, 'locktime_cltv': None, 'locktime_csv': None, 'result': ''} - - def _parse_script(script): - found = False - cur = 0 - data = _get_empty_data() - for script_type in script_types: - cur = 0 - try: - ost = SCRIPT_TYPES_UNLOCKING[script_type] - except KeyError: - ost = SCRIPT_TYPES_LOCKING[script_type] - data = _get_empty_data() - data['script_type'] = script_type - found = True - for ch in ost: - if cur >= len(script): - found = False - break - cur_char = script[cur] - if ch[:4] == 'hash': - hash_length = 0 - if len(ch) > 5: - hash_length = int(ch.split("-")[1]) - s, total_length = _parse_data(script[cur:], 1, item_length=hash_length) - if not s: - found = False - break - data['hashes'] += s - cur += total_length - elif ch == 'signature': - signature_length = 0 - s, total_length = _parse_data(script[cur:], 1, item_length=signature_length) - if not s: - found = False - break - data['signatures'] += s - cur += total_length - elif ch == 'public_key': - pk_size, size = varbyteint_to_int(script[cur:cur + 9]) - key = script[cur + size:cur + size + pk_size] - if not key: - found = False - break - data['keys'].append(key) - cur += size + pk_size - elif ch == 'OP_RETURN': - if cur_char == op.op_return and cur == 0: - data.update({'op_return': script[cur + 1:]}) - cur = len(script) - found = True - break - else: - found = False - break - elif ch == 'multisig': # one or more signatures - redeemscript_expected = False - if 'redeemscript' in ost: - redeemscript_expected = True - s, total_length = _parse_data(script[cur:], redeemscript_expected=redeemscript_expected) - if not s: - found = False - break - data['signatures'] += s - cur += total_length - elif ch == 'redeemscript': - size_byte = 0 - if script[cur:cur + 1] == b'\x4c': - size_byte = 1 - elif script[cur:cur + 1] == b'\x4d': - size_byte = 2 - elif script[cur:cur + 1] == b'\x4e': - size_byte = 3 - data['redeemscript'] = script[cur + 1 + size_byte:] - data2 = script_deserialize(data['redeemscript'], locking_script=True) - if 'signatures' not in data2 or not data2['signatures']: - found = False - break - data['keys'] = data2['signatures'] - data['number_of_sigs_m'] = data2['number_of_sigs_m'] - data['number_of_sigs_n'] = data2['number_of_sigs_n'] - cur = len(script) - elif ch == 'push_size': - push_size, size = varbyteint_to_int(script[cur:cur + 9]) - found = bool(len(script[cur:]) - size == push_size) - if not found: - break - elif ch == 'op_m': - if cur_char in OP_N_CODES: - data['number_of_sigs_m'] = cur_char - op.op_1 + 1 - else: - found = False - break - cur += 1 - elif ch == 'op_n': - if cur_char in OP_N_CODES: - data['number_of_sigs_n'] = cur_char - op.op_1 + 1 - else: - found = False - break - if data['number_of_sigs_m'] > data['number_of_sigs_n']: - raise TransactionError("Number of signatures to sign (%s) is higher then actual " - "amount of signatures (%s)" % - (data['number_of_sigs_m'], data['number_of_sigs_n'])) - if len(data['signatures']) > int(data['number_of_sigs_n']): - raise TransactionError("%d signatures found, but %s sigs expected" % - (len(data['signatures']), data['number_of_sigs_n'])) - cur += 1 - elif ch == 'SIGHASH_ALL': - pass - # if cur_char != SIGHASH_ALL: - # found = False - # break - elif ch == 'locktime_cltv': - if len(script) < 4: - found = False - break - data['locktime_cltv'] = int.from_bytes(script[cur:cur + 4], 'little') - cur += 4 - elif ch == 'locktime_csv': - if len(script) < 4: - found = False - break - data['locktime_csv'] = int.from_bytes(script[cur:cur + 4], 'little') - cur += 4 - else: - try: - if opcodenames.get(cur_char) == ch: - cur += 1 - else: - found = False - data = _get_empty_data() - break - except IndexError: - raise TransactionError("Opcode %s not found [type %s]" % (ch, script_type)) - if found and not len(script[cur:]): # Found is True and no remaining script to parse - break - - if found and not len(script[cur:]): - return data, script[cur:] - data = _get_empty_data() - data['result'] = 'Script not recognised' - return data, '' - - data = _get_empty_data() - script = to_bytes(script) - if not script: - data.update({'result': 'Empty script'}) - return data - - # Check if script starts with size byte - if size_bytes_check: - script_size, size = varbyteint_to_int(script[0:9]) - if len(script[1:]) == script_size: - data = script_deserialize(script[1:], script_types, locking_script, size_bytes_check=False) - if 'result' in data and data['result'][:22] not in \ - ['Script not recognised', 'Empty script', 'Could not parse script']: - return data - - if script_types is None: - if locking_script is None: - script_types = dict(SCRIPT_TYPES_UNLOCKING, **SCRIPT_TYPES_LOCKING) - elif locking_script: - script_types = SCRIPT_TYPES_LOCKING - else: - script_types = SCRIPT_TYPES_UNLOCKING - elif not isinstance(script_types, list): - script_types = [script_types] - - locktime_cltv = 0 - locktime_csv = 0 - while len(script): - begin_script = script - data, script = _parse_script(script) - if begin_script == script: - break - if script and data['script_type'] == 'locktime_cltv': - locktime_cltv = data['locktime_cltv'] - if script and data['script_type'] == 'locktime_csv': - locktime_csv = data['locktime_csv'] - if data and data['result'] != 'Script not recognised': - data['locktime_cltv'] = locktime_cltv - data['locktime_csv'] = locktime_csv - return data - - wrn_msg = "Could not parse script, unrecognized script" - # _logger.debug(wrn_msg) - data = _get_empty_data() - data['result'] = wrn_msg - return data - - -@deprecated # Replaced by Script class in version 0.6 -def script_to_string(script, name_data=False): # pragma: no cover - """ - Convert script to human-readable string format with OP-codes, signatures, keys, etc - - :param script: A locking or unlocking script - :type script: bytes, str - :param name_data: Replace signatures and keys strings with name - :type name_data: bool - - :return str: - """ - - # script = to_bytes(script) - data = script_deserialize(script) - if not data or data['script_type'] == 'empty': - return "" - if name_data: - name = 'signature' - if data['signatures'] and len(data['signatures'][0]) in [33, 65]: - name = 'key' - sigs = ' '.join(['%s-%d' % (name, i) for i in range(1, len(data['signatures']) + 1)]) - else: - sigs = ' '.join([i.hex() for i in data['signatures']]) - - try: - scriptstr = SCRIPT_TYPES_LOCKING[data['script_type']] - except KeyError: - scriptstr = SCRIPT_TYPES_UNLOCKING[data['script_type']] - scriptstr = [sigs if x in ['signature', 'multisig', 'return_data'] else x for x in scriptstr] - if 'redeemscript' in data and data['redeemscript']: - redeemscript_str = script_to_string(data['redeemscript'], name_data=name_data) - scriptstr = [redeemscript_str if x == 'redeemscript' else x for x in scriptstr] - scriptstr = [opcodenames[80 + int(data['number_of_sigs_m'])] if x == 'op_m' else x for x in scriptstr] - scriptstr = [opcodenames[80 + int(data['number_of_sigs_n'])] if x == 'op_n' else x for x in scriptstr] - - return ' '.join(scriptstr) - - -@deprecated # Replaced by Script class in version 0.6 -def _serialize_multisig_redeemscript(public_key_list, n_required=None): # pragma: no cover - # Serialize m-to-n multisig script. Needs a list of public keys - for key in public_key_list: - if not isinstance(key, (str, bytes)): - raise TransactionError("Item %s in public_key_list is not of type string or bytes") - if n_required is None: - n_required = len(public_key_list) - - script = int_to_varbyteint(op.op_1 + n_required - 1) - for key in public_key_list: - script += varstr(key) - script += int_to_varbyteint(op.op_1 + len(public_key_list) - 1) - script += b'\xae' # 'OP_CHECKMULTISIG' - - return script - - -@deprecated # Replaced by Script class in version 0.6 -def serialize_multisig_redeemscript(key_list, n_required=None, compressed=True): # pragma: no cover - """ - Create a multisig redeemscript used in a p2sh. - - Contains the number of signatures, followed by the list of public keys and the OP-code for the number of signatures required. - - :param key_list: List of public keys - :type key_list: Key, list - :param n_required: Number of required signatures - :type n_required: int - :param compressed: Use compressed public keys? - :type compressed: bool - - :return bytes: A multisig redeemscript - """ - - if not key_list: - return b'' - if not isinstance(key_list, list): - raise TransactionError("Argument public_key_list must be of type list") - if len(key_list) > 15: - raise TransactionError("Redeemscripts with more then 15 keys are non-standard and could result in " - "locked up funds") - public_key_list = [] - for k in key_list: - if isinstance(k, Key): - if compressed: - public_key_list.append(k.public_byte) - else: - public_key_list.append(k.public_uncompressed_byte) - elif len(k) == 65 and k[0:1] == b'\x04' or len(k) == 33 and k[0:1] in [b'\x02', b'\x03']: - public_key_list.append(k) - elif len(k) == 132 and k[0:2] == '04' or len(k) == 66 and k[0:2] in ['02', '03']: - public_key_list.append(bytes.fromhex(k)) - else: - kobj = Key(k) - if compressed: - public_key_list.append(kobj.public_byte) - else: - public_key_list.append(kobj.public_uncompressed_byte) - - return _serialize_multisig_redeemscript(public_key_list, n_required) - - -@deprecated # Replaced by Script class in version 0.6 -def _p2sh_multisig_unlocking_script(sigs, redeemscript, hash_type=None, as_list=False): # pragma: no cover - usu = b'\x00' - if as_list: - usu = [usu] - if not isinstance(sigs, list): - sigs = [sigs] - for sig in sigs: - s = sig - if hash_type: - s += hash_type.to_bytes(1, 'big') - if as_list: - usu.append(s) - else: - usu += varstr(s) - - rs_size = b'' - size_byte = b'' - if not as_list: - rs_size = int_to_varbyteint(len(redeemscript)) - if len(rs_size) > 1: - rs_size = rs_size[1:] - if len(redeemscript) >= 76: - if len(rs_size) == 1: - size_byte = b'\x4c' - elif len(rs_size) == 2: - size_byte = b'\x4d' - else: - size_byte = b'\x4e' - - redeemscript_str = size_byte + rs_size + redeemscript - if as_list: - usu.append(redeemscript_str) - else: - usu += redeemscript_str - return usu - - @deprecated # Replaced by Script class in version 0.6 def script_add_locktime_cltv(locktime_cltv, script): # pragma: no cover lockbytes = bytes([op.op_checklocktimeverify, op.op_drop]) @@ -652,10 +146,10 @@ class Input(object): """ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash=b'', unlocking_script=b'', - unlocking_script_unsigned=None, script=None, script_type=None, address='', - sequence=0xffffffff, compressed=None, sigs_required=None, sort=False, index_n=0, - value=0, double_spend=False, locktime_cltv=None, locktime_csv=None, key_path='', witness_type=None, - witnesses=None, encoding=None, strict=True, network=DEFAULT_NETWORK): + locking_script=None, redeemscript=None, script_type=None, address='', sequence=0xffffffff, + compressed=None, sigs_required=None, sort=False, index_n=0, value=0, double_spend=False, + locktime_cltv=None, locktime_csv=None, key_path='', witness_type=None, witnesses=None, encoding=None, + strict=True, network=DEFAULT_NETWORK): """ Create a new transaction input @@ -664,20 +158,22 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= :param output_n: Output number in previous transaction. :type output_n: bytes, int :param keys: A list of Key objects or public / private key string in various formats. If no list is provided but a bytes or string variable, a list with one item will be created. Optional - :type keys: list (bytes, str, Key) + :type keys: list (bytes, str, Key, HDKey) :param signatures: Specify optional signatures :type signatures: list (bytes, str, Signature) :param public_hash: Public key hash or script hash. Specify if key is not available :type public_hash: bytes :param unlocking_script: Unlocking script (scriptSig) to prove ownership. Optional :type unlocking_script: bytes, hexstring - :param unlocking_script_unsigned: Unlocking script for signing transaction - :type unlocking_script_unsigned: bytes, hexstring + :param locking_script: Unlocking script for signing transaction + :type locking_script: bytes, hexstring + :param redeemscript: Redeem script for p2sh transaction. Will be automatically created for standard scripts + :type redeemscript: bytes, hexstring :param script_type: Type of unlocking script used, i.e. p2pkh or p2sh_multisig. Default is p2pkh :type script_type: str :param address: Address string or object for input :type address: str, Address - :param sequence: Sequence part of input, you normally do not have to touch this + :param sequence: Sequence part of input, used for locktime setting and replace by fee. No need to set directly normally. :type sequence: bytes, int :param compressed: Use compressed or uncompressed public keys. Default is compressed :type compressed: bool @@ -691,8 +187,6 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= :type value: int, Value, str :param double_spend: Is this input also spend in another transaction :type double_spend: bool - :param locktime_cltv: Check Lock Time Verify value. Script level absolute time lock for this input - :type locktime_cltv: int :param locktime_csv: Check Sequence Verify value :type locktime_csv: int :param key_path: Key path of input key as BIP32 string or list @@ -718,8 +212,7 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= self.output_n_int = int.from_bytes(output_n, 'big') self.output_n = output_n self.unlocking_script = b'' if unlocking_script is None else to_bytes(unlocking_script) - self.unlocking_script_unsigned = b'' if unlocking_script_unsigned is None \ - else to_bytes(unlocking_script_unsigned) + self.locking_script = b'' if locking_script is None else to_bytes(locking_script) self.script = None self.hash_type = SIGHASH_ALL if isinstance(sequence, numbers.Number): @@ -743,14 +236,23 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= if not isinstance(signatures, list): signatures = [signatures] self.sort = sort + + self.address = '' + self.address_obj = None if isinstance(address, Address): + self.address_obj = address self.address = address.address self.encoding = address.encoding self.network = address.network - else: + self.witness_type = address.witness_type + elif address: self.address = address + self.address_obj = Address.parse(address) + if self.address_obj: + encoding = self.address_obj.encoding + witness_type = self.address_obj.witness_type if self.address_obj.witness_type else witness_type self.signatures = [] - self.redeemscript = b'' + self.redeemscript = b'' if not redeemscript else redeemscript self.script_type = script_type if self.prev_txid == b'\0' * 32: self.script_type = 'coinbase' @@ -759,9 +261,9 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= self.locktime_csv = locktime_csv self.witness_type = witness_type if encoding is None: - self.encoding = 'base58' - if self.witness_type == 'segwit': - self.encoding = 'bech32' + self.encoding = 'bech32' + if self.witness_type == 'legacy' or self.witness_type == 'p2sh-segwit': + self.encoding = 'base58' else: self.encoding = encoding self.valid = None @@ -779,31 +281,19 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= self.witnesses.append(witness) elif witnesses: self.witnesses = [bytes.fromhex(w) if isinstance(w, str) else w for w in witnesses] - self.script_code = b'' - self.script = script # If unlocking script is specified extract keys, signatures, type from script - if self.unlocking_script and self.script_type != 'coinbase' and not (signatures and keys) and not script: - self.script = Script.parse_bytes(self.unlocking_script, strict=strict) - self.keys = self.script.keys - self.signatures = self.script.signatures + if self.unlocking_script and self.script_type != 'coinbase' and not (signatures and keys): + script = Script.parse_bytes(self.unlocking_script, is_locking=False, strict=strict) + self.keys = script.keys + self.signatures = script.signatures if len(self.signatures): self.hash_type = self.signatures[0].hash_type - sigs_required = self.script.sigs_required - self.redeemscript = self.script.redeemscript if self.script.redeemscript else self.redeemscript - if len(self.script.script_types) == 1 and not self.script_type: - self.script_type = self.script.script_types[0] - elif self.script.script_types == ['signature_multisig', 'multisig']: - self.script_type = 'p2sh_multisig' - # TODO: Check if this if is necessary - if 'p2wpkh' in self.script.script_types: - self.script_type = 'p2sh_p2wpkh' - self.witness_type = 'p2sh-segwit' - elif 'p2wsh' in self.script.script_types: - self.script_type = 'p2sh_p2wsh' - self.witness_type = 'p2sh-segwit' - if self.unlocking_script_unsigned and not self.signatures: - ls = Script.parse_bytes(self.unlocking_script_unsigned, strict=strict) + sigs_required = script.sigs_required + if len(script.script_types) == 1 and not self.script_type: + self.script_type = script.script_types[0] + if self.locking_script and not self.signatures: + ls = Script.parse_bytes(self.locking_script, is_locking=True, strict=strict) self.public_hash = self.public_hash if not ls.public_hash else ls.public_hash if ls.script_types[0] in ['p2wpkh', 'p2wsh']: self.witness_type = 'segwit' @@ -812,13 +302,14 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= if self.script_type is None and self.witness_type is None and self.witnesses: self.witness_type = 'segwit' if self.witness_type is None or self.witness_type == 'legacy': - # if self.script_type in ['p2wpkh', 'p2wsh', 'p2sh_p2wpkh', 'p2sh_p2wsh']: - if self.script_type in ['p2wpkh', 'p2wsh']: - self.witness_type = 'segwit' - elif self.script_type in ['p2sh_p2wpkh', 'p2sh_p2wsh']: + if self.script_type in ['p2sh_p2wpkh', 'p2sh_p2wsh']: self.witness_type = 'p2sh-segwit' - else: - self.witness_type = 'legacy' + self.encoding = 'base58' + elif not self.witness_type: + if not self.witnesses: + self.witness_type = 'legacy' + else: + self.witness_type = 'segwit' elif self.witness_type == 'segwit' and self.script_type == 'sig_pubkey' and encoding is None: self.encoding = 'bech32' if not self.script_type: @@ -830,9 +321,6 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= else: kobj = key if kobj not in self.keys: - # if self.compressed is not None and kobj.compressed != self.compressed: - # _logger.warning("Key compressed is %s but Input class compressed argument is %s " % - # (kobj.compressed, self.compressed)) self.compressed = kobj.compressed self.keys.append(kobj) if self.compressed is None: @@ -859,7 +347,7 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= b'\0' not in self.witnesses: self.signatures = [Signature.parse_bytes(self.witnesses[0])] self.hash_type = self.signatures[0].hash_type - self.keys = [Key(self.witnesses[1], network=self.network)] + self.keys = [Key(self.witnesses[1], network=self.network, strict=self.strict)] self.update_scripts(hash_type=self.hash_type) @@ -887,9 +375,12 @@ def parse(cls, raw, witness_type='segwit', index_n=0, strict=True, network=DEFAU output_n = raw.read(4)[::-1] unlocking_script_size = read_varbyteint(raw) unlocking_script = raw.read(unlocking_script_size) + script_type = None + # TODO - handle non-standard input script b'\1\0', # see tx 38cf5779d1c5ca32b79cd5052b54e824102e878f041607d3b962038f5a8cf1ed - # if unlocking_script_size == 1 and unlocking_script == b'\0': + if unlocking_script_size == 1 and unlocking_script == b'\0': + script_type = 'nonstandard_0001' inp_type = 'legacy' if witness_type == 'segwit' and not unlocking_script_size: @@ -897,7 +388,8 @@ def parse(cls, raw, witness_type='segwit', index_n=0, strict=True, network=DEFAU sequence_number = raw.read(4) return Input(prev_txid=prev_hash, output_n=output_n, unlocking_script=unlocking_script, - witness_type=inp_type, sequence=sequence_number, index_n=index_n, strict=strict, network=network) + witness_type=inp_type, sequence=sequence_number, index_n=index_n, strict=strict, network=network, + script_type=script_type) def update_scripts(self, hash_type=SIGHASH_ALL): """ @@ -914,13 +406,12 @@ def update_scripts(self, hash_type=SIGHASH_ALL): addr_data = b'' unlock_script = b'' - if self.script_type in ['sig_pubkey', 'p2sh_p2wpkh', 'p2wpkh']: # fixme: p2wpkh == p2sh_p2wpkh + if self.script_type in ['sig_pubkey', 'p2sh_p2wpkh']: if not self.public_hash and self.keys: self.public_hash = self.keys[0].hash160 if not self.keys and not self.public_hash: return - self.script_code = b'\x76\xa9\x14' + self.public_hash + b'\x88\xac' - self.unlocking_script_unsigned = self.script_code + self.locking_script = b'\x76\xa9\x14' + self.public_hash + b'\x88\xac' addr_data = self.public_hash if self.signatures and self.keys: self.witnesses = [self.signatures[0].as_der_encoded() if hash_type else b'', self.keys[0].public_byte] @@ -932,17 +423,16 @@ def update_scripts(self, hash_type=SIGHASH_ALL): self.unlocking_script = b'' elif unlock_script != b'': self.unlocking_script = unlock_script - elif self.script_type in ['p2sh_multisig', 'p2sh_p2wsh', 'p2wsh']: # fixme: p2sh_p2wsh == p2wsh + elif self.script_type in ['p2sh_multisig', 'p2sh_p2wsh']: if not self.redeemscript and self.keys: self.redeemscript = Script(script_types=['multisig'], keys=self.keys, sigs_required=self.sigs_required).serialize() - if self.redeemscript: + if self.redeemscript and not self.public_hash: if self.witness_type == 'segwit' or self.witness_type == 'p2sh-segwit': self.public_hash = hashlib.sha256(self.redeemscript).digest() else: self.public_hash = hash160(self.redeemscript) addr_data = self.public_hash - self.unlocking_script_unsigned = self.redeemscript if self.redeemscript and self.keys: n_tag = self.redeemscript[0:1] @@ -962,44 +452,43 @@ def update_scripts(self, hash_type=SIGHASH_ALL): else: unlock_script = unlock_script_obj.serialize() if self.witness_type == 'segwit': - script_code = b'' + self.locking_script = b'' for k in self.keys: - script_code += varstr(k.public_byte) + b'\xad\xab' - if len(script_code) > 3: - script_code = script_code[:-2] + b'\xac' - self.script_code = script_code + self.locking_script += varstr(k.public_byte) + b'\xad\xab' + if len(self.locking_script) > 3: + self.locking_script = self.locking_script[:-2] + b'\xac' if signatures: self.witnesses = unlock_script elif self.witness_type == 'p2sh-segwit': self.unlocking_script = varstr(b'\0' + varstr(self.public_hash)) - self.script_code = self.unlocking_script if signatures: self.witnesses = unlock_script - elif unlock_script != b'' and self.strict: + elif unlock_script != b'': # and self.strict: self.unlocking_script = unlock_script elif self.script_type == 'signature': if self.keys: - self.script_code = varstr(self.keys[0].public_byte) + b'\xac' - self.unlocking_script_unsigned = self.script_code - addr_data = self.keys[0].public_byte + self.locking_script = varstr(self.keys[0].public_byte) + b'\xac' + addr_data = hash160(self.keys[0].public_byte) if self.signatures and not self.unlocking_script: self.unlocking_script = varstr(self.signatures[0].as_der_encoded()) elif self.script_type == 'p2tr': # segwit_v1 self.redeemscript = self.witnesses[0] # FIXME: Address cannot be known without looking at previous transaction - elif self.script_type not in ['coinbase', 'unknown'] and self.strict: + elif self.script_type[:11] not in ['coinbase', 'unknown', 'nonstandard'] and self.strict: raise TransactionError("Unknown unlocking script type %s for input %d" % (self.script_type, self.index_n)) if addr_data and not self.address: self.address = Address(hashed_data=addr_data, encoding=self.encoding, network=self.network, script_type=self.script_type, witness_type=self.witness_type).address - - if self.locktime_cltv: - self.unlocking_script_unsigned = script_add_locktime_cltv(self.locktime_cltv, - self.unlocking_script_unsigned) - self.unlocking_script = script_add_locktime_cltv(self.locktime_cltv, self.unlocking_script) - elif self.locktime_csv: - self.unlocking_script_unsigned = script_add_locktime_csv(self.locktime_csv, self.unlocking_script_unsigned) - self.unlocking_script = script_add_locktime_csv(self.locktime_csv, self.unlocking_script) + # FIXME: need to add locktime script to redeemscript + # if self.locktime_cltv: + # self.locking_script = script_add_locktime_cltv(self.locktime_cltv, self.locking_script) + # # if self.unlocking_script: + # # self.unlocking_script = script_add_locktime_cltv(self.locktime_cltv, self.unlocking_script) + # # if self.witness_type == 'segwit': + # # self.witnesses.insert(0, script_add_locktime_cltv(self.locktime_cltv, b'')) + # if self.locktime_csv: + # self.locking_script = script_add_locktime_csv(self.locktime_csv, self.locking_script) + # self.unlocking_script = script_add_locktime_csv(self.locktime_csv, self.unlocking_script) return True def verify(self, transaction_hash): @@ -1075,12 +564,11 @@ def as_dict(self): 'sequence': self.sequence, 'signatures': [s.hex() for s in self.signatures], 'sigs_required': self.sigs_required, - 'locktime_cltv': self.locktime_cltv, + # 'locktime_cltv': self.locktime_cltv, 'locktime_csv': self.locktime_csv, 'public_hash': self.public_hash.hex(), - 'script_code': self.script_code.hex(), 'unlocking_script': self.unlocking_script.hex(), - 'unlocking_script_unsigned': self.unlocking_script_unsigned.hex(), + 'locking_script': self.locking_script.hex(), 'witness_type': self.witness_type, 'witness': b''.join(self.witnesses).hex(), 'sort': self.sort, @@ -1101,7 +589,7 @@ class Output(object): def __init__(self, value, address='', public_hash=b'', public_key=b'', lock_script=b'', spent=False, output_n=0, script_type=None, witver=0, encoding=None, spending_txid='', spending_index_n=None, - strict=True, network=DEFAULT_NETWORK): + strict=True, change=None, witness_type=None, network=DEFAULT_NETWORK): """ Create a new transaction output @@ -1138,6 +626,10 @@ def __init__(self, value, address='', public_hash=b'', public_key=b'', lock_scri :type spending_index_n: int :param strict: Raise exception when output is malformed, incomplete or not understood :type strict: bool + :param change: Is this a change output back to own wallet or not? Used for replace-by-fee. + :type change: bool + :param witness_type: Specify witness type: 'segwit' or 'legacy'. Determine from script, address or encoding if not specified. + :type witness_type: str :param network: Network, leave empty for default :type network: str, Network """ @@ -1146,6 +638,7 @@ def __init__(self, value, address='', public_hash=b'', public_key=b'', lock_scri raise TransactionError("Please specify address, lock_script, public key or public key hash when " "creating output") + self.change = change self.network = network if not isinstance(network, Network): self.network = Network(network) @@ -1161,33 +654,33 @@ def __init__(self, value, address='', public_hash=b'', public_key=b'', lock_scri public_key = address.public_byte if not script_type: script_type = script_type_default(address.witness_type, address.multisig, True) - self.public_hash = address.hash160 + # self.public_hash = address.hash160 + # self.witness_type = address.witness_type else: self._address = address self._address_obj = None self.public_key = to_bytes(public_key) self.compressed = True - self.k = None self.versionbyte = self.network.prefix_address self.script_type = script_type self.encoding = encoding - if not self._address and self.encoding is None: - self.encoding = 'base58' self.spent = spent self.output_n = output_n - self.script = Script.parse_bytes(self.lock_script, strict=strict) + self.script = Script.parse_bytes(self.lock_script, strict=strict, is_locking=True) self.witver = witver + self.witness_type = witness_type if self._address_obj: self.script_type = self._address_obj.script_type if script_type is None else script_type + # if not script_type: + # script_type = script_type_default(address.witness_type, address.multisig, True) self.public_hash = self._address_obj.hash_bytes self.network = self._address_obj.network self.encoding = self._address_obj.encoding + self.witness_type = self._address_obj.witness_type if self.script: self.script_type = self.script_type if not self.script.script_types else self.script.script_types[0] - if self.script_type in ['p2wpkh', 'p2wsh', 'p2tr']: - self.encoding = 'bech32' self.public_hash = self.script.public_hash if self.script.keys: self.public_key = self.script.keys[0].public_byte @@ -1195,8 +688,7 @@ def __init__(self, value, address='', public_hash=b'', public_key=b'', lock_scri self.witver = self.script.commands[0] - 80 if self.public_key and not self.public_hash: - k = Key(self.public_key, is_private=False, network=network) - self.public_hash = k.hash160 + self.public_hash = hash160(self.public_key) elif self._address and (not self.public_hash or not self.script_type or not self.encoding): address_dict = deserialize_address(self._address, self.encoding, self.network.name) if address_dict['script_type'] and not script_type: @@ -1212,10 +704,13 @@ def __init__(self, value, address='', public_hash=b'', public_key=b'', lock_scri raise TransactionError("Network for output address %s is different from transaction network. %s not " "in %s" % (self._address, self.network.name, network_guesses)) self.public_hash = address_dict['public_key_hash_bytes'] + self.witness_type = address_dict['witness_type'] if not self.encoding: - self.encoding = 'base58' - if self.script_type in ['p2wpkh', 'p2wsh', 'p2tr']: - self.encoding = 'bech32' + self.encoding = 'bech32' + if self.script_type in ['p2pkh', 'p2sh', 'p2pk'] or self.witness_type == 'legacy': + self.encoding = 'base58' + else: + self.witness_type = 'segwit' if self.script_type is None: self.script_type = 'p2pkh' @@ -1374,29 +869,6 @@ class Transaction(object): Each input in the transaction can be signed with the sign method provided a valid private key. """ - @staticmethod - @deprecated # Replaced by Transaction.parse() in version 0.6 - def import_raw(rawtx, network=DEFAULT_NETWORK, check_size=True): # pragma: no cover - """ - Import a raw transaction and create a Transaction object - - Uses the transaction_deserialize method to parse the raw transaction and then calls the init method of - this transaction class to create the transaction object - - REPLACED BY THE PARSE() METHOD - - :param rawtx: Raw transaction string - :type rawtx: bytes, str - :param network: Network, leave empty for default - :type network: str, Network - :param check_size: Check if no bytes are left when parsing is finished. Disable when parsing list of transactions, such as the transactions in a raw block. Default is True - :type check_size: bool - - :return Transaction: - """ - - return transaction_deserialize(rawtx, network=network, check_size=check_size) - @classmethod def parse(cls, rawtx, strict=True, network=DEFAULT_NETWORK): """ @@ -1411,24 +883,31 @@ def parse(cls, rawtx, strict=True, network=DEFAULT_NETWORK): :return Transaction: """ + raw_bytes = b'' if isinstance(rawtx, bytes): + raw_bytes = rawtx rawtx = BytesIO(rawtx) elif isinstance(rawtx, str): + raw_bytes = bytes.fromhex(rawtx) rawtx = BytesIO(bytes.fromhex(rawtx)) - return cls.parse_bytesio(rawtx, strict, network) + return cls.parse_bytesio(rawtx, strict, network, raw_bytes=raw_bytes) @classmethod - def parse_bytesio(cls, rawtx, strict=True, network=DEFAULT_NETWORK): + def parse_bytesio(cls, rawtx, strict=True, network=DEFAULT_NETWORK, index=None, raw_bytes=b''): """ Parse a raw transaction and create a Transaction object - :param rawtx: Raw transaction string + :param rawtx: Raw transaction bytes stream :type rawtx: BytesIO :param strict: Raise exception when transaction is malformed, incomplete or not understood :type strict: bool :param network: Network, leave empty for default network :type network: str, Network + :param index: index position in block + :type index: int + :param raw_bytes: Raw transaction as bytes if available + :param raw_bytes: bytes :return Transaction: """ @@ -1438,7 +917,6 @@ def parse_bytesio(cls, rawtx, strict=True, network=DEFAULT_NETWORK): network = network if not isinstance(network, Network): cls.network = Network(network) - raw_bytes = b'' try: pos_start = rawtx.tell() @@ -1487,7 +965,7 @@ def parse_bytesio(cls, rawtx, strict=True, network=DEFAULT_NETWORK): witness = rawtx.read(item_size) inputs[n].witnesses.append(witness) if not is_taproot: - s = Script.parse_bytes(witness, strict=strict) + s = Script.parse_bytes(witness, strict=strict, is_locking=False) if s.script_types == ['p2tr_unlock']: # FIXME: Support Taproot unlocking scripts _logger.warning("Taproot is not supported at the moment, rest of parsing input transaction " @@ -1498,7 +976,9 @@ def parse_bytesio(cls, rawtx, strict=True, network=DEFAULT_NETWORK): inputs[n].script = script if not inputs[n].script else inputs[n].script + script inputs[n].keys = script.keys inputs[n].signatures = script.signatures - if script.script_types[0][:13] == 'p2sh_multisig' or script.script_types[0] =='signature_multisig': + if not script.script_types: + inputs[n].script_type = 'unknown' + elif script.script_types[0][:13] == 'p2sh_multisig' or script.script_types[0] =='signature_multisig': inputs[n].script_type = 'p2sh_multisig' inputs[n].redeemscript = inputs[n].witnesses[-1] elif script.script_types[0] == 'p2tr_unlock': @@ -1515,7 +995,11 @@ def parse_bytesio(cls, rawtx, strict=True, network=DEFAULT_NETWORK): inputs[n].update_scripts() - locktime = int.from_bytes(rawtx.read(4)[::-1], 'big') + locktime_bytes = rawtx.read(4)[::-1] + if len(locktime_bytes) != 4 and strict: + raise TransactionError("Invalid transaction size, locktime bytes incomplete") + + locktime = int.from_bytes(locktime_bytes, 'big') raw_len = len(raw_bytes) if not raw_bytes: pos_end = rawtx.tell() @@ -1524,7 +1008,7 @@ def parse_bytesio(cls, rawtx, strict=True, network=DEFAULT_NETWORK): raw_bytes = rawtx.read(raw_len) return Transaction(inputs, outputs, locktime, version, network, size=raw_len, output_total=output_total, - coinbase=coinbase, flag=flag, witness_type=witness_type, rawtx=raw_bytes) + coinbase=coinbase, flag=flag, witness_type=witness_type, rawtx=raw_bytes, index=index) @classmethod def parse_hex(cls, rawtx, strict=True, network=DEFAULT_NETWORK): @@ -1542,7 +1026,8 @@ def parse_hex(cls, rawtx, strict=True, network=DEFAULT_NETWORK): :return Transaction: """ - return cls.parse_bytesio(BytesIO(bytes.fromhex(rawtx)), strict, network) + raw_bytes = bytes.fromhex(rawtx) + return cls.parse_bytesio(BytesIO(raw_bytes), strict, network, raw_bytes=raw_bytes) @classmethod def parse_bytes(cls, rawtx, strict=True, network=DEFAULT_NETWORK): @@ -1560,7 +1045,7 @@ def parse_bytes(cls, rawtx, strict=True, network=DEFAULT_NETWORK): :return Transaction: """ - return cls.parse(BytesIO(rawtx), strict, network) + return cls.parse_bytesio(BytesIO(rawtx), strict, network, raw_bytes=rawtx) @staticmethod def load(txid=None, filename=None): @@ -1592,7 +1077,8 @@ def load(txid=None, filename=None): def __init__(self, inputs=None, outputs=None, locktime=0, version=None, network=DEFAULT_NETWORK, fee=None, fee_per_kb=None, size=None, txid='', txhash='', date=None, confirmations=None, block_height=None, block_hash=None, input_total=0, output_total=0, rawtx=b'', - status='new', coinbase=False, verified=False, witness_type='legacy', flag=None): + status='new', coinbase=False, verified=False, witness_type='segwit', flag=None, replace_by_fee=False, + index=None): """ Create a new transaction class with provided inputs and outputs. @@ -1611,9 +1097,9 @@ def __init__(self, inputs=None, outputs=None, locktime=0, version=None, :type version: bytes, int :param network: Network, leave empty for default network :type network: str, Network - :param fee: Fee in smallest denominator (ie Satoshi) for complete transaction + :param fee: Fee in the smallest denominator (ie Satoshi) for complete transaction :type fee: int - :param fee_per_kb: Fee in smallest denominator per kilobyte. Specify when exact transaction size is not known. + :param fee_per_kb: Fee in the smallest denominator per kilobyte. Specify when exact transaction size is not known. :type fee_per_kb: int :param size: Transaction size in bytes :type size: int @@ -1645,6 +1131,8 @@ def __init__(self, inputs=None, outputs=None, locktime=0, version=None, :type witness_type: str :param flag: Transaction flag to indicate version, for example for SegWit :type flag: bytes, str + :param index: Index of transaction in block. Used when parsing blocks + :type index: int """ @@ -1703,7 +1191,9 @@ def __init__(self, inputs=None, outputs=None, locktime=0, version=None, self.status = status self.verified = verified self.witness_type = witness_type + self.replace_by_fee = replace_by_fee self.change = 0 + self.index = index self.calc_weight_units() if self.witness_type not in ['legacy', 'segwit']: raise TransactionError("Please specify a valid witness type: legacy or segwit") @@ -1784,13 +1274,29 @@ def as_dict(self): def as_json(self): """ - Get current key as json formatted string + Get current transaction as json formatted string :return str: """ adict = self.as_dict() return json.dumps(adict, indent=4, default=str) + def as_bytes(self): + """ + Return raw serialized transaction as bytes string + + :return bytes: + """ + return self.raw() + + def as_hex(self): + """ + Return raw hex string of transaction as hex string + + :return: + """ + return self.raw_hex() + def info(self): """ Prints transaction information to standard output @@ -1827,12 +1333,12 @@ def info(self): print(" Relative timelock for %d seconds" % (512 * (ti.sequence - SEQUENCE_LOCKTIME_TYPE_FLAG))) else: print(" Relative timelock for %d blocks" % ti.sequence) - if ti.locktime_cltv: - if ti.locktime_cltv & SEQUENCE_LOCKTIME_TYPE_FLAG: - print(" Check Locktime Verify (CLTV) for %d seconds" % - (512 * (ti.locktime_cltv - SEQUENCE_LOCKTIME_TYPE_FLAG))) - else: - print(" Check Locktime Verify (CLTV) for %d blocks" % ti.locktime_cltv) + # if ti.locktime_cltv: + # if ti.locktime_cltv & SEQUENCE_LOCKTIME_TYPE_FLAG: + # print(" Check Locktime Verify (CLTV) for %d seconds" % + # (512 * (ti.locktime_cltv - SEQUENCE_LOCKTIME_TYPE_FLAG))) + # else: + # print(" Check Locktime Verify (CLTV) for %d blocks" % ti.locktime_cltv) if ti.locktime_csv: if ti.locktime_csv & SEQUENCE_LOCKTIME_TYPE_FLAG: print(" Check Sequence Verify Timelock (CSV) for %d seconds" % @@ -1850,6 +1356,8 @@ def info(self): spent_str = 'S' elif to.spent is False: spent_str = 'U' + if to.change: + spent_str += 'C' print("-", to.address, Value.from_satoshi(to.value, network=self.network).str(1), to.script_type, spent_str) if replace_by_fee: @@ -1860,7 +1368,7 @@ def info(self): print("Confirmations: %s" % self.confirmations) print("Block: %s" % self.block_height) - def set_locktime_relative_blocks(self, blocks, input_index_n=0): + def set_locktime_relative_blocks(self, blocks, input_index_n=0, locktime=0): """ Set nSequence relative locktime for this transaction. The transaction will only be valid if the specified number of blocks has been mined since the previous UTXO is confirmed. @@ -1872,6 +1380,8 @@ def set_locktime_relative_blocks(self, blocks, input_index_n=0): :type blocks: int :param input_index_n: Index number of input for nSequence locktime :type input_index_n: int + :param locktime: Overwrite default locktime, must be lower than current network blockcount. If anti-fee-sniping is used in a Wallet this value is already filled in. + :type locktime: int :return: """ @@ -1882,10 +1392,11 @@ def set_locktime_relative_blocks(self, blocks, input_index_n=0): if blocks > SEQUENCE_LOCKTIME_MASK: raise TransactionError("Number of nSequence timelock blocks exceeds %d" % SEQUENCE_LOCKTIME_MASK) self.inputs[input_index_n].sequence = blocks - self.version_int = 2 + self.version_int = 2 if self.version_int < 2 else self.version_int + self.locktime = locktime if locktime else self.locktime self.sign_and_update(index_n=input_index_n) - def set_locktime_relative_time(self, seconds, input_index_n=0): + def set_locktime_relative_time(self, seconds, input_index_n=0, locktime=0): """ Set nSequence relative locktime for this transaction. The transaction will only be valid if the specified amount of seconds have been passed since the previous UTXO is confirmed. @@ -1899,6 +1410,8 @@ def set_locktime_relative_time(self, seconds, input_index_n=0): :type seconds: int :param input_index_n: Index number of input for nSequence locktime :type input_index_n: int + :param locktime: Overwrite default locktime, must be lower than current network blockcount. If anti-fee-sniping is used in a Wallet this value is already filled in. + :type locktime: int :return: """ @@ -1911,7 +1424,8 @@ def set_locktime_relative_time(self, seconds, input_index_n=0): elif (seconds // 512) > SEQUENCE_LOCKTIME_MASK: raise TransactionError("Number of relative nSeqence timelock seconds exceeds %d" % SEQUENCE_LOCKTIME_MASK) self.inputs[input_index_n].sequence = seconds // 512 + SEQUENCE_LOCKTIME_TYPE_FLAG - self.version_int = 2 + self.version_int = 2 if self.version_int < 2 else self.version_int + self.locktime = locktime if locktime else self.locktime self.sign_and_update(index_n=input_index_n) def set_locktime_blocks(self, blocks): @@ -1920,6 +1434,8 @@ def set_locktime_blocks(self, blocks): So for example if you set this value to 600000 the transaction will only be valid after block 600000. + You can also pass the locktime value directly to a Transaction object, or when sending from a wallet. + :param blocks: Transaction is valid after supplied block number. Value must be between 0 and 500000000. Zero means no locktime. :type blocks: int @@ -1936,13 +1452,15 @@ def set_locktime_blocks(self, blocks): if blocks != 0 and blocks != 0xffffffff: for i in self.inputs: if i.sequence == 0xffffffff: - i.sequence = 0xfffffffd + i.sequence = SEQUENCE_ENABLE_LOCKTIME self.sign_and_update() def set_locktime_time(self, timestamp): """ Set nLocktime, a transaction level absolute lock time in timestamp using the transaction sequence field. + You can also pass the locktime value directly to a Transaction object, or when sending from a wallet. + :param timestamp: Transaction is valid after the given timestamp. Value must be between 500000000 and 0xfffffffe :return: """ @@ -1961,7 +1479,7 @@ def set_locktime_time(self, timestamp): # Input sequence value must be below 0xffffffff for i in self.inputs: if i.sequence == 0xffffffff: - i.sequence = 0xfffffffd + i.sequence = SEQUENCE_ENABLE_LOCKTIME self.sign_and_update() def signature_hash(self, sign_id=None, hash_type=SIGHASH_ALL, witness_type=None, as_hex=False): @@ -2046,7 +1564,7 @@ def signature_segwit(self, sign_id, hash_type=SIGHASH_ALL): sign_id) if not self.inputs[sign_id].redeemscript: - self.inputs[sign_id].redeemscript = self.inputs[sign_id].script_code + self.inputs[sign_id].redeemscript = self.inputs[sign_id].locking_script if (not self.inputs[sign_id].redeemscript or self.inputs[sign_id].redeemscript == b'\0') and \ self.inputs[sign_id].redeemscript != 'unknown' and not is_coinbase: @@ -2093,9 +1611,14 @@ def raw(self, sign_id=None, hash_type=SIGHASH_ALL, witness_type=None): else: r_witness += b'\0' if sign_id is None: + if i.script_type == 'nonstandard_0001': + r += b'\1' r += varstr(i.unlocking_script) elif sign_id == i.index_n: - r += varstr(i.unlocking_script_unsigned) + if i.script_type == 'p2sh_multisig': + r += varstr(i.redeemscript) + else: + r += varstr(i.locking_script) else: r += b'\0' r += i.sequence.to_bytes(4, 'little') @@ -2193,6 +1716,9 @@ def sign(self, keys=None, index_n=None, multisig_key_n=None, hash_type=SIGHASH_A :return None: """ + if hash_type != SIGHASH_ALL: + raise TransactionError("Hash type othen than SIGHASH_ALL are not supported at the moment") + if index_n is None: tids = range(len(self.inputs)) else: @@ -2220,7 +1746,7 @@ def sign(self, keys=None, index_n=None, multisig_key_n=None, hash_type=SIGHASH_A n_total_sigs = len(self.inputs[tid].keys) sig_domain = [''] * n_total_sigs - txid = self.signature_hash(tid, witness_type=self.inputs[tid].witness_type) + txid = self.signature_hash(tid, hash_type, self.inputs[tid].witness_type) for key in tid_keys: # Check if signature signs known key and is not already in list if key.public_byte not in pub_key_list: @@ -2235,7 +1761,7 @@ def sign(self, keys=None, index_n=None, multisig_key_n=None, hash_type=SIGHASH_A if not key.private_byte: raise TransactionError("Please provide a valid private key to sign the transaction") - sig = sign(txid, key) + sig = sign(txid, key, hash_type=hash_type) newsig_pos = pub_key_list.index(key.public_byte) sig_domain[newsig_pos] = sig n_signs += 1 @@ -2284,9 +1810,9 @@ def sign_and_update(self, index_n=None): self.fee_per_kb = int((self.fee / float(self.vsize)) * 1000) def add_input(self, prev_txid, output_n, keys=None, signatures=None, public_hash=b'', unlocking_script=b'', - unlocking_script_unsigned=None, script_type=None, address='', + locking_script=None, script_type=None, address='', sequence=0xffffffff, compressed=True, sigs_required=None, sort=False, index_n=None, - value=None, double_spend=False, locktime_cltv=None, locktime_csv=None, + value=None, double_spend=False,locktime_cltv=None, locktime_csv=None, key_path='', witness_type=None, witnesses=None, encoding=None, strict=True): """ Add input to this transaction @@ -2298,15 +1824,15 @@ def add_input(self, prev_txid, output_n, keys=None, signatures=None, public_hash :param output_n: Output number in previous transaction. :type output_n: bytes, int :param keys: Public keys can be provided to construct an Unlocking script. Optional - :type keys: bytes, str + :type keys: list (bytes, str, Key, HDKey) :param signatures: Add signatures to input if already known :type signatures: bytes, str :param public_hash: Specify public hash from key or redeemscript if key is not available :type public_hash: bytes :param unlocking_script: Unlocking script (scriptSig) to prove ownership. Optional :type unlocking_script: bytes, hexstring - :param unlocking_script_unsigned: TODO: find better name... - :type unlocking_script_unsigned: bytes, str + :param locking_script: Locking script (scriptPubKey) of previous output if known + :type locking_script: bytes, hexstring :param script_type: Type of unlocking script used, i.e. p2pkh or p2sh_multisig. Default is p2pkh :type script_type: str :param address: Specify address of input if known, default is to derive from key or scripts @@ -2325,8 +1851,6 @@ def add_input(self, prev_txid, output_n, keys=None, signatures=None, public_hash :type value: int :param double_spend: True if double spend is detected, depends on which service provider is selected :type double_spend: bool - :param locktime_cltv: Check Lock Time Verify value. Script level absolute time lock for this input - :type locktime_cltv: int :param locktime_csv: Check Sequency Verify value. :type locktime_csv: int :param key_path: Key path of input key as BIP32 string or list @@ -2345,6 +1869,8 @@ def add_input(self, prev_txid, output_n, keys=None, signatures=None, public_hash if index_n is None: index_n = len(self.inputs) + if self.replace_by_fee and sequence == 0xffffffff: + sequence = SEQUENCE_REPLACE_BY_FEE sequence_int = sequence if isinstance(sequence, bytes): sequence_int = int.from_bytes(sequence, 'little') @@ -2353,7 +1879,7 @@ def add_input(self, prev_txid, output_n, keys=None, signatures=None, public_hash self.version_int = 2 self.inputs.append( Input(prev_txid=prev_txid, output_n=output_n, keys=keys, signatures=signatures, public_hash=public_hash, - unlocking_script=unlocking_script, unlocking_script_unsigned=unlocking_script_unsigned, + unlocking_script=unlocking_script, locking_script=locking_script, script_type=script_type, address=address, sequence=sequence, compressed=compressed, sigs_required=sigs_required, sort=sort, index_n=index_n, value=value, double_spend=double_spend, locktime_cltv=locktime_cltv, locktime_csv=locktime_csv, key_path=key_path, witness_type=witness_type, @@ -2361,7 +1887,8 @@ def add_input(self, prev_txid, output_n, keys=None, signatures=None, public_hash return index_n def add_output(self, value, address='', public_hash=b'', public_key=b'', lock_script=b'', spent=False, - output_n=None, encoding=None, spending_txid=None, spending_index_n=None, strict=True): + output_n=None, encoding=None, spending_txid=None, spending_index_n=None, strict=True, + change=None): """ Add an output to this transaction @@ -2389,6 +1916,8 @@ def add_output(self, value, address='', public_hash=b'', public_key=b'', lock_sc :type spending_index_n: int :param strict: Raise exception when output is malformed or incomplete :type strict: bool + :param change: Is this a change output back to own wallet or not? Used for replace-by-fee. + :type change: bool :return int: Transaction output number (output_n) """ @@ -2404,7 +1933,7 @@ def add_output(self, value, address='', public_hash=b'', public_key=b'', lock_sc self.outputs.append(Output(value=int(value), address=address, public_hash=public_hash, public_key=public_key, lock_script=lock_script, spent=spent, output_n=output_n, encoding=encoding, spending_txid=spending_txid, spending_index_n=spending_index_n, - strict=strict, network=self.network.name)) + strict=strict, change=change, network=self.network.name)) return output_n def merge_transaction(self, transaction): @@ -2558,6 +2087,19 @@ def update_totals(self): if self.vsize: self.fee_per_kb = int((self.fee / float(self.vsize)) * 1000) + def update_inputs(self, input_n=None): + """ + Update input scripts to reflect changes you made to one of more inputs. All inputs will be updated unless + you specificy a specific input. + + :param input_n: Input to update, leave empty to update all input scripts + + :return: + """ + input_list = range(0, len(self.inputs)) if input_n is None else [input_n] + for inp in input_list: + self.inputs[inp].update_scripts() + def save(self, filename=None): """ Store transaction object as file, so it can be imported in bitcoinlib later with the :func:`load` method. @@ -2605,3 +2147,62 @@ def shuffle(self): """ self.shuffle_inputs() self.shuffle_outputs() + + def bumpfee(self, fee=0, extra_fee=0): + """ + Increase fee for this transaction. If replace-by-fee is signaled in this transaction the fee can be + increased to speed up inclusion on the blockchain. + + If not fee or extra_fee is provided the extra fee will be increased by the formule you can find in the code + below using the BUMPFEE_DEFAULT_MULTIPLIER from the config settings. + + The extra fee will be deducted from change output. This method fails if there are not enough change outputs + to cover fees. + + :param fee: New fee for this transaction + :type fee: int + :param extra_fee: Extra fee to add to current transaction fee + :type extra_fee: int + + :return None: + """ + if not self.fee: + raise TransactionError("Current transaction fee is zero, cannot increase fee") + if not self.vsize: + self.estimate_size() + + minimal_required_fee = self.vsize + if fee: + if fee < self.fee + minimal_required_fee: + raise TransactionError("Fee cannot be less than minimal required fee") + extra_fee = fee - self.fee + elif extra_fee: + if extra_fee < minimal_required_fee: + raise TransactionError("Extra fee cannot be less than minimal required fee") + fee = self.fee + extra_fee + else: + fee = int(self.fee * (1.03 ** BUMPFEE_DEFAULT_MULTIPLIER) + + (minimal_required_fee * BUMPFEE_DEFAULT_MULTIPLIER)) + extra_fee = fee - self.fee + + remaining_fee = extra_fee + outputs_to_delete = [] + for outp in [o for o in self.outputs if o.change]: + if not remaining_fee: + break + if outp.value > remaining_fee * 2: + outp.value -= extra_fee + remaining_fee = 0 + elif outp.value < remaining_fee: + remaining_fee -= outp.value + outputs_to_delete.append(outp) + else: + outputs_to_delete.append(outp) + remaining_fee = 0 + + if remaining_fee: + raise TransactionError("Not enough unspent outputs to bump transaction fee") + self.fee = fee + for o in outputs_to_delete: + self.outputs.remove(o) + self.sign_and_update() diff --git a/bitcoinlib/values.py b/bitcoinlib/values.py index e1a507d3..4f59ac7d 100644 --- a/bitcoinlib/values.py +++ b/bitcoinlib/values.py @@ -34,7 +34,10 @@ def value_to_satoshi(value, network=None): :return int: """ if isinstance(value, str): - value = Value(value) + if network: + value = Value(value, network=network) + else: + value = Value(value) if isinstance(value, Value): if network and value.network != network: raise ValueError("Value uses different network (%s) then supplied network: %s" % (value.network.name, network)) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 9a611273..24f69b72 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # BitcoinLib - Python Cryptocurrency Library # WALLETS - HD wallet Class for Key and Transaction management -# © 2016 - 2023 May - 1200 Web Development +# © 2016 - 2024 February - 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,6 +23,7 @@ from operator import itemgetter import numpy as np import pickle +from datetime import timedelta from bitcoinlib.db import * from bitcoinlib.encoding import * from bitcoinlib.keys import Address, BKeyError, HDKey, check_network_and_key, path_expand @@ -30,7 +31,7 @@ from bitcoinlib.networks import Network from bitcoinlib.values import Value, value_to_satoshi from bitcoinlib.services.services import Service -from bitcoinlib.transactions import Input, Output, Transaction, get_unlocking_script_type +from bitcoinlib.transactions import Input, Output, Transaction, get_unlocking_script_type, TransactionError from bitcoinlib.scripts import Script from sqlalchemy import func, or_ @@ -56,11 +57,9 @@ def wallets_list(db_uri=None, include_cosigners=False, db_password=None): :param db_uri: URI of the database :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 @@ -110,7 +109,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, db_uri=None, db_cache_uri=None, db_password=None): + key_path=None, anti_fee_sniping=True, db_uri=None, db_cache_uri=None, db_password=None): """ Create a wallet with specified options if it doesn't exist, otherwise just open @@ -125,7 +124,8 @@ 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, db_uri=db_uri, db_cache_uri=db_cache_uri, db_password=db_password) + key_path, anti_fee_sniping, 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): @@ -213,6 +213,7 @@ def wallet_empty(wallet, db_uri=None, db_password=None): else: w = session.query(DbWallet).filter_by(name=wallet) if not w or not w.first(): + session.close() raise WalletError("Wallet '%s' not found" % wallet) wallet_id = w.first().id @@ -301,9 +302,9 @@ class WalletKey(object): """ @staticmethod - def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0, purpose=44, parent_id=0, + def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0, purpose=84, parent_id=0, path='m', key_type=None, encoding=None, witness_type=DEFAULT_WITNESS_TYPE, multisig=False, - cosigner_id=None): + cosigner_id=None, new_key_id=None): """ Create WalletKey from a HDKey object or key. @@ -311,7 +312,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 @@ -331,7 +332,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 44 + :param purpose: BIP0044 purpose field, default is 84 :type purpose: int :param parent_id: Key ID of parent, default is 0 (no parent) :type parent_id: int @@ -347,6 +348,8 @@ 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 """ @@ -358,6 +361,7 @@ 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 @@ -368,12 +372,24 @@ 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) + k = HDKey(import_key=key, network=network, witness_type=witness_type) 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: + 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) + keyexists = session.query(DbKey).\ filter(DbKey.wallet_id == wallet_id, DbKey.wif == k.wif(witness_type=witness_type, multisig=multisig, is_private=True)).first() @@ -381,52 +397,53 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0 _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) - 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, + if commit: + 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, 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) + key_type=key_type, network_name=network, encoding=encoding, cosigner_id=cosigner_id, + witness_type=witness_type) else: keyexists = session.query(DbKey).\ filter(DbKey.wallet_id == wallet_id, DbKey.address == k.address).first() if keyexists: - _logger.warning("Key with ID %s already exists" % keyexists.id) + _logger.warning("Key %s with ID %s already exists" % (k.address, keyexists.id)) return WalletKey(keyexists.id, session, k) - nk = DbKey(name=name[:80], wallet_id=wallet_id, purpose=purpose, + nk = DbKey(id=new_key_id, 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) + key_type=key_type, network_name=network, encoding=encoding, cosigner_id=cosigner_id, + witness_type=witness_type) - session.merge(DbNetwork(name=network)) + if commit: + session.merge(DbNetwork(name=network)) session.add(nk) - session.commit() + if 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): @@ -442,7 +459,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 @@ -477,9 +494,13 @@ 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) @@ -507,18 +528,33 @@ def name(self, value): self._dbkey.name = value self._commit() + @property + def keys_public(self): + if self.key_type == 'multisig': + return [k.public_byte for k in self.key()] + else: + return [self.key_public] + + @property + def keys_private(self): + if self.key_type == 'multisig': + return [k.private_byte for k in self.key() if k.private_byte] + else: + return [self.key_private] if self.key_private else [] + def key(self): """ Get HDKey object for current WalletKey - :return HDKey: + :return HDKey, list of HDKey: """ self._hdkey_object = None if self.key_type == 'multisig': self._hdkey_object = [] for kc in self._dbkey.multisig_children: - self._hdkey_object.append(HDKey.from_wif(kc.child_key.wif, network=kc.child_key.network_name, compressed=self.compressed)) + self._hdkey_object.append(HDKey.from_wif(kc.child_key.wif, network=kc.child_key.network_name, + compressed=self.compressed)) if self._hdkey_object is None and self.wif: self._hdkey_object = HDKey.from_wif(self.wif, network=self.network_name, compressed=self.compressed) return self._hdkey_object @@ -673,7 +709,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)) @@ -719,7 +755,7 @@ def from_txid(cls, hdwallet, txid): public_key = key.key().public_hex outputs.append(Output(value=out.value, address=address, public_key=public_key, lock_script=out.script, spent=out.spent, output_n=out.output_n, - script_type=out.script_type, network=network)) + script_type=out.script_type, network=network, change=out.is_change)) return cls(hdwallet=hdwallet, inputs=inputs, outputs=outputs, locktime=db_tx.locktime, version=db_tx.version, network=network, fee=db_tx.fee, fee_per_kb=fee_per_kb, @@ -782,12 +818,12 @@ def sign(self, keys=None, index_n=0, multisig_key_n=None, hash_type=SIGHASH_ALL, self.verify() self.error = "" - def send(self, offline=False): + def send(self, broadcast=True): """ Verify and push transaction to network. Update UTXO's in database after successful send - :param offline: Just return the transaction object and do not send it when offline = True. Default is False - :type offline: bool + :param broadcast: Verify transaction and broadcast, if set to False the transaction is verified but not broadcasted, i. Default is True + :type broadcast: bool :return None: @@ -798,10 +834,10 @@ def send(self, offline=False): self.error = "Cannot verify transaction" return None - if offline: + if not broadcast: return None - srv = Service(network=self.network.name, providers=self.hdwallet.providers, + srv = Service(network=self.network.name, wallet_name=self.hdwallet.name, providers=self.hdwallet.providers, cache_uri=self.hdwallet.db_cache_uri) res = srv.sendrawtransaction(self.raw_hex()) if not res: @@ -819,7 +855,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() @@ -838,7 +874,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)) @@ -855,7 +891,8 @@ 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) + raw=self.rawtx, verified=self.verified, account_id=self.account_id, locktime=self.locktime, + version=self.version_int, coinbase=self.coinbase, index=self.index) sess.add(new_tx) self.hdwallet._commit() txidn = new_tx.id @@ -871,6 +908,7 @@ 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 @@ -913,7 +951,7 @@ def store(self): if not tx_output: new_tx_item = DbTransactionOutput( transaction_id=txidn, output_n=to.output_n, key_id=key_id, address=to.address, value=to.value, - spent=spent, script=to.lock_script, script_type=to.script_type) + spent=spent, script=to.lock_script, script_type=to.script_type, is_change=to.change) sess.add(new_tx_item) elif key_id: tx_output.key_id = key_id @@ -994,17 +1032,119 @@ 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() session.query(DbTransactionOutput).filter_by(transaction_id=tx.id).delete() + for inp in tx.inputs: + prev_utxos = session.query(DbTransactionOutput).join(DbTransaction).\ + filter(DbTransaction.txid == inp.prev_txid, DbTransactionOutput.output_n == inp.output_n, + DbTransactionOutput.spent.is_(True), DbTransaction.wallet_id == self.hdwallet.wallet_id).all() + for u in prev_utxos: + # Check if output is spent in another transaction + if session.query(DbTransactionInput).filter(DbTransactionInput.transaction_id == + inp.transaction_id).first(): + u.spent = False session.query(DbTransactionInput).filter_by(transaction_id=tx.id).delete() - session.query(DbKey).filter_by(latest_txid=txid).update({DbKey.latest_txid: None}) + qr = session.query(DbKey).filter_by(latest_txid=txid) + qr.update({DbKey.latest_txid: None, DbKey.used: False}) res = tx_query.delete() + key = qr.scalar() + if key: + self.hdwallet._balance_update(key_id=key.id) self.hdwallet._commit() return res + def bumpfee(self, fee=0, extra_fee=0, broadcast=False): + """ + Increase fee for this transaction. If replace-by-fee is signaled in this transaction the fee can be + increased to speed up inclusion on the blockchain. + + If not fee or extra_fee is provided the extra fee will be increased by the formule you can find in the code + below using the BUMPFEE_DEFAULT_MULTIPLIER from the config settings. + + The extra fee will be deducted from change output. This method fails if there are not enough change outputs + to cover fees. + + If this transaction does not have enough inputs to cover extra fee, an extra wallet utxo will be aaded to + inputs if available. + + Previous broadcasted transaction will be removed from wallet with this replace-by-fee transaction and wallet + information updated. + + :param fee: New fee for this transaction + :type fee: int + :param extra_fee: Extra fee to add to current transaction fee + :type extra_fee: int + :param broadcast: Increase fee and directly broadcast transaction to the network + :type broadcast: bool + + :return None: + """ + fees_not_provided = not (fee or extra_fee) + old_txid = self.txid + try: + super(WalletTransaction, self).bumpfee(fee, extra_fee) + except TransactionError as e: + if str(e) != "Not enough unspent outputs to bump transaction fee": + raise TransactionError(str(e)) + else: + # Add extra input to cover fee + if fees_not_provided: + extra_fee = int(self.fee * (0.03 ** BUMPFEE_DEFAULT_MULTIPLIER) + + (self.vsize * BUMPFEE_DEFAULT_MULTIPLIER)) + new_inp = self.add_input_from_wallet(amount_min=extra_fee) + # Add value of extra input to change output + change_outputs = [o for o in self.outputs if o.change] + if change_outputs: + change_outputs[0].value += self.inputs[new_inp].value + else: + self.add_output(self.inputs[new_inp].value, self.hdwallet.get_key().address, change=True) + if fees_not_provided: + extra_fee += 25 * BUMPFEE_DEFAULT_MULTIPLIER + super(WalletTransaction, self).bumpfee(fee, extra_fee) + # remove previous transaction and update wallet + if self.pushed: + self.hdwallet.transaction_delete(old_txid) + if broadcast: + self.send() + + def add_input_from_wallet(self, amount_min=0, key_id=None, min_confirms=1): + """ + Add a new input from an utxo of this wallet. If not key_id is specified it adds the first input it finds with + the minimum amount and minimum confirms specified. + + WARNING: Change output and fees are not updated, so you risk overpaying fees! + + :param amount_min: Minimum value of new input + :type amount_min: int + :param key_id: Filter by this key id + :type key_id: int + :param min_confirms: Minimum confirms of utxo + :type min_confirms: int + + :return int: Index number of new input + """ + if not amount_min: + amount_min = self.network.dust_amount + utxos = self.hdwallet.utxos(self.account_id, network=self.network.name, min_confirms=min_confirms, + key_id=key_id) + current_inputs = [(i.prev_txid.hex(), i.output_n_int) for i in self.inputs] + unused_inputs = [u for u in utxos + if (u['txid'], u['output_n']) not in current_inputs and u['value'] >= amount_min] + if not unused_inputs: + raise TransactionError("Not enough unspent inputs found for transaction %s" % + self.txid) + # take first input + utxo = unused_inputs[0] + inp_keys, key = self.hdwallet._objects_by_key_id(utxo['key_id']) + unlock_script_type = get_unlocking_script_type(utxo['script_type'], self.witness_type, + multisig=self.hdwallet.multisig) + return self.add_input(utxo['txid'], utxo['output_n'], keys=inp_keys, script_type=unlock_script_type, + sigs_required=self.hdwallet.multisig_n_required, sort=self.hdwallet.sort_keys, + compressed=key.compressed, value=utxo['value'], address=utxo['address'], + witness_type=key.witness_type) class Wallet(object): """ @@ -1022,8 +1162,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, db_uri, db_cache_uri, - db_password): + witness_type, encoding, multisig, sigs_required, cosigner_id, key_path, + anti_fee_sniping, db_uri, db_cache_uri, db_password): db = Db(db_uri, db_password) session = db.session @@ -1063,7 +1203,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) + key_path=key_path, anti_fee_sniping=anti_fee_sniping) session.add(new_wallet) session.commit() new_wallet_id = new_wallet.id @@ -1078,7 +1218,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([0, 0], account_id=account_id, cosigner_id=cosigner_id) + w.key_for_path([], account_id=account_id, cosigner_id=cosigner_id, change=0, address_index=0) else: # scheme == 'single': if not key: key = HDKey(network=network, depth=key_depth) @@ -1094,15 +1234,16 @@ 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() - raise + self.session.rollback() + raise WalletError("Could not commit to database, rollback performed!") @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, db_uri=None, db_cache_uri=None, db_password=None): + cosigner_id=None, key_path=None, anti_fee_sniping=True, db_uri=None, db_cache_uri=None, + db_password=None): """ Create Wallet and insert in database. Generate masterkey or import key when specified. @@ -1156,7 +1297,8 @@ 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 'legacy'. Use 'segwit' for native segregated witness wallet, or 'p2sh-segwit' for legacy compatible wallets + :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 :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 @@ -1172,6 +1314,8 @@ 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) @@ -1224,14 +1368,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: - raise WalletError("Please specify network when using passphrase to create a key") - key = HDKey.from_seed(Mnemonic().to_seed(key, password), network=network) + network = DEFAULT_NETWORK + key = HDKey.from_seed(Mnemonic().to_seed(key, password), network=network, witness_type=witness_type) else: try: if isinstance(key, WalletKey): key = key._hdkey_object else: - key = HDKey(key, password=password, network=network) + key = HDKey(key, password=password, witness_type=witness_type, network=network) except BKeyError: try: scheme = 'single' @@ -1248,7 +1392,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 ['dash', 'dash_testnet', 'dogecoin', 'dogecoin_testnet'] and witness_type != 'legacy': + if network in ['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. " @@ -1259,16 +1403,7 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 key_path = ['m'] purpose = 0 else: - 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'] + key_path, purpose, encoding = get_key_structure_data(witness_type, multisig, purpose, encoding) else: if purpose is None: purpose = 0 @@ -1299,7 +1434,8 @@ 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, - key_path=main_key_path, db_uri=db_uri, db_cache_uri=db_cache_uri, db_password=db_password) + 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) if multisig: wlt_cos_id = 0 @@ -1317,13 +1453,14 @@ 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, - db_uri=db_uri, db_cache_uri=db_cache_uri, db_password=db_password) + anti_fee_sniping=anti_fee_sniping, 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 @@ -1346,18 +1483,17 @@ 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 - else: - dbinit = Db(db_uri=db_uri, password=db_password) - self._session = dbinit.session - self._engine = dbinit.engine + self._db_password = db_password 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 @@ -1372,12 +1508,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: @@ -1404,25 +1540,26 @@ 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 def __repr__(self): - db_uri = self.db_uri.split('?')[0] + db_uri = '' if not self.db_uri else self.db_uri.split('?')[0] if DEFAULT_DATABASE in db_uri: return "" % self.name return "" % \ @@ -1454,7 +1591,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: @@ -1477,7 +1614,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() @@ -1503,7 +1640,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() @@ -1531,14 +1668,23 @@ 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: + logger.info("Opening database session %s" % self.db_uri) + 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() @@ -1566,7 +1712,6 @@ 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") @@ -1574,18 +1719,19 @@ 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'] + # 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) 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): @@ -1593,11 +1739,10 @@ def import_master_key(self, hdkey, name='Masterkey (imported)'): if kp and kp[0] == 'M': kp = self.key_path[:self.depth_public_master+1] + kp[1:] self.key_for_path(kp, recreate=True) - self._commit() return self.main_key - def import_key(self, key, account_id=0, name='', network=None, purpose=44, key_type=None): + def import_key(self, key, account_id=0, name='', network=None, purpose=84, key_type=None): """ Add new single key to wallet. @@ -1609,7 +1754,7 @@ def import_key(self, key, account_id=0, name='', network=None, purpose=44, 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: BIP definition used, default is BIP44 + :param purpose: BIP44 definition used, default is 84 (segwit) :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 @@ -1636,7 +1781,7 @@ def import_key(self, key, account_id=0, name='', network=None, purpose=44, 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) + hdkey = HDKey(key, network=network, key_type=key_type, witness_type=self.witness_type) if not self.multisig: if self.main_key and self.main_key.depth == self.depth_public_master and \ @@ -1651,7 +1796,7 @@ def import_key(self, key, account_id=0, name='', network=None, purpose=44, 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) @@ -1662,7 +1807,7 @@ def import_key(self, key, account_id=0, name='', network=None, purpose=44, 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: @@ -1676,23 +1821,20 @@ def import_key(self, key, account_id=0, name='', network=None, purpose=44, 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): + def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, network, address_index, + witness_type): 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 self.witness_type == 'p2sh-segwit': - script_type = 'p2sh_p2wsh' - 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, + script_type = 'p2sh' if witness_type == 'legacy' else \ + ('p2sh_p2wsh' if witness_type == 'p2sh-segwit' else '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.address).first() if already_found_key: return self.key(already_found_key.id) @@ -1701,20 +1843,43 @@ def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, if not name: name = "Multisig Key " + '/'.join(public_key_ids) - multisig_key = DbKey( + new_key_id = (self.session.query(func.max(DbKey.id)).scalar() or 0) + 1 + multisig_key = DbKey(id=new_key_id, 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', network_name=network) - self._session.add(multisig_key) + key_type='multisig', witness_type=witness_type, 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, network=None): + 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): """ 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. @@ -1731,15 +1896,18 @@ def new_key(self, name='', account_id=None, change=0, cosigner_id=None, network= :type change: int :param cosigner_id: Cosigner ID for key path :type cosigner_id: int - :param network: Network name. Leave empty for default network + :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 :type network: str - :return WalletKey: + :return list of 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") @@ -1750,23 +1918,27 @@ def new_key(self, name='', account_id=None, change=0, cosigner_id=None, network= 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 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).\ + 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).\ order_by(DbKey.address_index.desc()).first() if prevkey: address_index = prevkey.address_index + 1 - req_path = [change, address_index] - return self.key_for_path(req_path, name=name, account_id=account_id, network=network, - cosigner_id=cosigner_id, address_index=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) - def new_key_change(self, name='', account_id=None, network=None): + def new_key_change(self, name='', account_id=None, witness_type=None, network=None): """ Create new key to receive change for a transaction. Calls :func:`new_key` method with change=1. @@ -1780,7 +1952,7 @@ def new_key_change(self, name='', account_id=None, network=None): :return WalletKey: """ - return self.new_key(name=name, account_id=account_id, network=network, change=1) + return self.new_key(name=name, account_id=account_id, witness_type=witness_type, network=network, change=1) def scan_key(self, key): """ @@ -1849,7 +2021,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: @@ -1867,7 +2039,11 @@ 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 = self.get_keys(account_id, network, number_of_keys=scan_gap_limit, change=chg) + 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) + n_highest_updated = 0 for key in keys_to_scan: if key.key_id in keys_ignore: @@ -1883,7 +2059,8 @@ 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, network=None, cosigner_id=None, number_of_keys=1, change=0, as_list=False): + def _get_key(self, account_id=None, witness_type=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 @@ -1891,33 +2068,37 @@ def _get_key(self, account_id=None, network=None, cosigner_id=None, number_of_ke raise WalletError("Cosigner ID (%d) can not be greater then number of cosigners for this wallet (%d)" % (cosigner_id, len(self.cosigner))) - last_used_qr = self._session.query(DbKey.id).\ + witness_type = witness_type if witness_type else self.witness_type + 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).\ + used=True, change=change, depth=self.key_depth, witness_type=witness_type).\ 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).\ + dbkey = (self.session.query(DbKey.id). 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).filter(DbKey.id > last_used_key_id).\ - order_by(DbKey.id.desc()).all() - key_list = [] + 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()) if self.scheme == 'single' and len(dbkey): number_of_keys = len(dbkey) if number_of_keys > len(dbkey) else number_of_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) + 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 + if as_list: return key_list else: return key_list[0] - def get_key(self, account_id=None, network=None, cosigner_id=None, change=0): + def get_key(self, account_id=None, witness_type=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. @@ -1932,6 +2113,8 @@ def get_key(self, account_id=None, network=None, cosigner_id=None, change=0): :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 @@ -1941,9 +2124,9 @@ def get_key(self, account_id=None, network=None, cosigner_id=None, change=0): :return WalletKey: """ - return self._get_key(account_id, network, cosigner_id, change=change, as_list=False) + return self._get_key(account_id, witness_type, network, cosigner_id, change=change, as_list=False) - def get_keys(self, account_id=None, network=None, cosigner_id=None, number_of_keys=1, change=0): + def get_keys(self, account_id=None, witness_type=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. @@ -1952,6 +2135,8 @@ def get_keys(self, account_id=None, network=None, cosigner_id=None, number_of_ke :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 @@ -1965,30 +2150,34 @@ def get_keys(self, account_id=None, network=None, cosigner_id=None, number_of_ke """ 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, network, cosigner_id, number_of_keys, change, as_list=True) + return self._get_key(account_id, witness_type, network, cosigner_id, number_of_keys, change, as_list=True) - def get_key_change(self, account_id=None, network=None): + def get_key_change(self, account_id=None, witness_type=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=account_id, network=network, change=1, as_list=False) + return self._get_key(account_id, witness_type, network, change=1, as_list=False) - def get_keys_change(self, account_id=None, network=None, number_of_keys=1): + def get_keys_change(self, account_id=None, witness_type=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 @@ -1997,10 +2186,9 @@ def get_keys_change(self, account_id=None, network=None, number_of_keys=1): :return list of WalletKey: """ - return self._get_key(account_id=account_id, network=network, change=1, number_of_keys=number_of_keys, - as_list=True) + return self._get_key(account_id, witness_type, network, change=1, number_of_keys=number_of_keys, as_list=True) - def new_account(self, name='', account_id=None, network=None): + def new_account(self, name='', account_id=None, witness_type=None, network=None): """ Create a new account with a child key for payments and 1 for change. @@ -2010,6 +2198,8 @@ def new_account(self, name='', account_id=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 @@ -2022,7 +2212,7 @@ def new_account(self, name='', account_id=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 not found in key path %s" % + raise WalletError("Accounts are not supported for this wallet. Account level not found in key path %s" % self.key_path) if network is None: network = self.network.name @@ -2035,21 +2225,25 @@ def new_account(self, name='', account_id=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, purpose=self.purpose, network_name=network). \ + qr = self.session.query(DbKey). \ + filter_by(wallet_id=self.wallet_id, witness_type=witness_type, 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, network=network): + if self.keys(account_id=account_id, depth=self.depth_public_master, witness_type=witness_type, + 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, 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) + 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) return acckey def path_expand(self, path, level_offset=None, account_id=None, cosigner_id=0, address_index=None, change=0, @@ -2088,7 +2282,39 @@ 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, network=None, recreate=False): + 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): """ Return key for specified path. Derive all wallet keys in path if they not already exists @@ -2124,45 +2350,67 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi :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 WalletKey: + :return list of 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=self.purpose, address_index=address_index, change=change, - witness_type=self.witness_type, network=network) + purpose=purpose, address_index=address_index, change=change, + witness_type=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.key_for_path(path, level_offset=level_offset, account_id=account_id, name=name, - cosigner_id=cosigner_id, network=network, recreate=recreate) + 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) public_keys.append(wk) - return self._new_key_multisig(public_keys, name, account_id, change, cosigner_id, network, address_index) - - # Check for closest ancestor in wallet\ + 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 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() @@ -2173,40 +2421,80 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi 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: - return topkey + if dbkey and dbkey.path == normalize_path('/'.join(fullpath)) and not recreate and number_of_keys == 1: + return [topkey] else: - # Create 1 or more keys add them to wallet - nk = None + 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 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 = None if "change" not in self.key_path or self.key_path.index("change") >= len(fullpath) \ - else int(fullpath[self.key_path.index("change")]) + 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("'"))) 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("_", " ") - 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 + 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 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, network=None, include_private=False, as_dict=False): + has_balance=None, is_active=None, witness_type=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. @@ -2235,6 +2523,8 @@ 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 @@ -2245,9 +2535,11 @@ 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: @@ -2287,7 +2579,8 @@ def keys(self, account_id=None, name=None, key_id=None, change=None, depth=None, keys2.append({k: v for (k, v) in key.items() if k[:1] != '_' and k != 'wallet' and k not in private_fields}) return keys2 - qr.session.close() + # qr.session.close() + qr.session.commit() return keys def keys_networks(self, used=None, as_dict=False): @@ -2462,9 +2755,7 @@ def key(self, term): """ dbkey = None - qr = self._session.query(DbKey).filter_by(wallet_id=self.wallet_id) - if self.purpose: - qr = qr.filter_by(purpose=self.purpose) + qr = self.session.query(DbKey).filter_by(wallet_id=self.wallet_id) if isinstance(term, numbers.Number): dbkey = qr.filter_by(id=term).scalar() if not dbkey: @@ -2477,7 +2768,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: @@ -2500,7 +2791,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: @@ -2529,6 +2820,26 @@ 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: + """ + + 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] if qr else [self.witness_type] + def networks(self, as_dict=False): """ Get list of networks used by this wallet @@ -2541,7 +2852,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] @@ -2592,7 +2903,7 @@ def balance_update_from_serviceprovider(self, account_id=None, network=None): """ network, account_id, acckey = self._get_account_defaults(network, account_id) - 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) balance = srv.getbalance(self.addresslist(account_id=account_id, network=network)) if srv.results: new_balance = { @@ -2654,7 +2965,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), @@ -2726,7 +3037,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 @@ -2778,7 +3089,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 @@ -2793,14 +3104,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() @@ -2816,8 +3127,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, 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: @@ -2839,7 +3149,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']) @@ -2849,14 +3159,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']) @@ -2880,7 +3190,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 @@ -2894,7 +3204,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() @@ -2931,7 +3241,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), @@ -2960,7 +3270,7 @@ def utxo_add(self, address, value, txid, output_n, confirmations=1, script=''): """ Add a single UTXO to the wallet database. To update all utxo's use :func:`utxos_update` method. - Use this method for testing, offline wallets or if you wish to override standard method of retreiving UTXO's + Use this method for testing, offline wallets or if you wish to override standard method of retrieving UTXO's This method does not check if UTXO exists or is still spendable. @@ -2988,7 +3298,7 @@ def utxo_add(self, address, value, txid, output_n, confirmations=1, script=''): 'txid': txid, 'value': value } - return self.utxos_update(utxos=[utxo]) + return self.utxos_update(utxos=[utxo], rescan_all=False) def utxo_last(self, address): """ @@ -3003,7 +3313,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, @@ -3018,13 +3328,11 @@ def transactions_update_confirmations(self): :return: """ network = self.network.name - 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) blockcount = srv.blockcount() - db_txs = self._session.query(DbTransaction). \ + self.session.query(DbTransaction).\ filter(DbTransaction.wallet_id == self.wallet_id, - 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). \ + DbTransaction.network_name == network, DbTransaction.block_height > 0).\ update({DbTransaction.status: 'confirmed', DbTransaction.confirmations: (blockcount - DbTransaction.block_height) + 1}) self._commit() @@ -3043,7 +3351,7 @@ def transactions_update_by_txids(self, txids): txids = list(dict.fromkeys(txids)) txs = [] - srv = Service(network=self.network.name, providers=self.providers, cache_uri=self.db_cache_uri) + srv = Service(network=self.network.name, wallet_name=self.name, providers=self.providers, cache_uri=self.db_cache_uri) for txid in txids: tx = srv.gettransaction(to_hexstring(txid)) if tx: @@ -3058,7 +3366,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: @@ -3096,19 +3404,9 @@ 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 - if not key_id: - self.transactions_update_confirmations() + self.transactions_update_confirmations() - srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) - blockcount = srv.blockcount() - db_txs = self._session.query(DbTransaction).\ - filter(DbTransaction.wallet_id == self.wallet_id, - 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() + srv = Service(network=network, wallet_name=self.name, providers=self.providers, cache_uri=self.db_cache_uri) # Get transactions for wallet's addresses txs = [] @@ -3121,7 +3419,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() @@ -3136,7 +3434,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: @@ -3157,7 +3455,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() @@ -3188,7 +3486,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, @@ -3201,7 +3499,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, @@ -3261,7 +3559,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: @@ -3341,7 +3639,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, @@ -3349,8 +3647,46 @@ def transaction_spent(self, txid, output_n): if qr: return qr.transaction.txid.hex() + + def update_transactions_from_block(block, network=None): + pass + + def transaction_delete(self, txid): + """ + Remove specified transaction from wallet and update related transactions. + + :param txid: Transaction ID of transaction to remove + :type txid: str + + :return: + """ + wt = self.transaction(txid) + if wt: + wt.delete() + else: + raise WalletError("Transaction %s not found in this wallet" % txid) + + def transactions_remove_unconfirmed(self, hours_old=0, account_id=None, network=None): + """ + Removes all unconfirmed transactions from this wallet and updates related transactions / utxos. + + :param hours_old: Only delete unconfirmed transaction which are x hours old. You can also use decimals, ie: 0.5 for half an hour + :type hours_old: int, float + :param account_id: Filter by Account ID. Leave empty for default account_id + :type account_id: int, None + :param network: Filter by network name. Leave empty for default network + :type network: str, None + :return: + """ + txs = self.transactions(account_id=account_id, network=network) + td = datetime.utcnow() - timedelta(hours=hours_old) + for tx in txs: + if not tx.confirmations and tx.date < td: + self.transaction_delete(tx.txid) + def _objects_by_key_id(self, key_id): - key = self._session.query(DbKey).filter_by(id=key_id).scalar() + self.session.expire_all() + 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': @@ -3402,7 +3738,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) @@ -3412,8 +3748,15 @@ 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) - utxos = utxo_query.order_by(DbTransaction.confirmations.desc()).all() + utxo_query = utxo_query.filter(DbTransactionOutput.value >= dust_amount) + utxo_query = utxo_query.order_by(DbTransaction.confirmations.desc()) + try: + utxos = utxo_query.all() + except Exception as e: + self.session.close() + logger.warning("Error when querying database, retry: %s" % str(e)) + utxos = utxo_query.all() + if not utxos: raise WalletError("Create transaction: No unspent transaction outputs found or no key available for UTXO's") @@ -3467,7 +3810,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): + random_output_order=True, replace_by_fee=False): """ Create new transaction with specified outputs. @@ -3504,6 +3847,8 @@ 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 """ @@ -3524,7 +3869,8 @@ 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) + transaction = WalletTransaction(hdwallet=self, account_id=account_id, network=network, locktime=locktime, + replace_by_fee=replace_by_fee) transaction.outgoing_tx = True for o in output_arr: if isinstance(o, Output): @@ -3536,9 +3882,16 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco addr = o[0] if isinstance(addr, WalletKey): addr = addr.key() - transaction.add_output(value, addr) + transaction.add_output(value, addr, change=False) + + 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) + blockcount = srv.blockcount() + if blockcount: + transaction.locktime = blockcount - srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) transaction.fee_per_kb = None if isinstance(fee, int): fee_estimate = fee @@ -3558,8 +3911,10 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco # Add inputs sequence = 0xffffffff - if 0 < transaction.locktime < 0xffffffff: - sequence = 0xfffffffe + if replace_by_fee: + sequence = SEQUENCE_REPLACE_BY_FEE + elif 0 < transaction.locktime < 0xffffffff: + sequence = SEQUENCE_ENABLE_LOCKTIME amount_total_input = 0 if input_arr is None: selected_utxos = self.select_inputs(amount_total_output + fee_estimate, transaction.network.dust_amount, @@ -3570,18 +3925,20 @@ 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 - unlock_script_type = get_unlocking_script_type(utxo.script_type, self.witness_type, multisig=multisig) + 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) 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=self.witness_type) + key_path=utxo.key.path, witness_type=witness_type) # FIXME: Missing locktime_cltv=locktime_cltv, locktime_csv=locktime_csv (?) else: for inp in input_arr: locktime_cltv = None locktime_csv = None - unlocking_script_unsigned = None + locking_script = None unlocking_script_type = '' if isinstance(inp, Input): prev_txid = inp.prev_txid @@ -3590,22 +3947,13 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco value = inp.value signatures = inp.signatures unlocking_script = inp.unlocking_script - unlocking_script_unsigned = inp.unlocking_script_unsigned + locking_script = inp.locking_script unlocking_script_type = inp.script_type address = inp.address sequence = inp.sequence locktime_cltv = inp.locktime_cltv locktime_csv = inp.locktime_csv - # elif isinstance(inp, DbTransactionOutput): - # prev_txid = inp.transaction.txid - # output_n = inp.output_n - # key_id = inp.key_id - # value = inp.value - # signatures = None - # # FIXME: This is probably not an unlocking_script - # unlocking_script = inp.script - # unlocking_script_type = get_unlocking_script_type(inp.script_type) - # address = inp.key.address + witness_type = inp.witness_type else: prev_txid = inp[0] output_n = inp[1] @@ -3614,11 +3962,12 @@ 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() @@ -3627,11 +3976,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.witness_type + witness_type = inp_utxo.key.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" % ( @@ -3646,9 +3995,9 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco sigs_required=self.multisig_n_required, sort=self.sort_keys, compressed=key.compressed, value=value, signatures=signatures, unlocking_script=unlocking_script, address=address, - unlocking_script_unsigned=unlocking_script_unsigned, + locking_script=locking_script, sequence=sequence, locktime_cltv=locktime_cltv, locktime_csv=locktime_csv, - witness_type=self.witness_type, key_path=key.path) + witness_type=witness_type, key_path=key.path) # Calculate fees transaction.fee = fee fee_per_output = None @@ -3705,9 +4054,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=account_id, network=network, change=1)] + change_keys = [self.get_key(account_id, self.witness_type, network, change=1)] else: - change_keys = self.get_keys(account_id=account_id, network=network, change=1, + change_keys = self.get_keys(account_id, self.witness_type, network, change=1, number_of_keys=number_of_change_outputs) if number_of_change_outputs > 1: @@ -3724,7 +4073,7 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco change_amounts = [transaction.change] for idx, ck in enumerate(change_keys): - on = transaction.add_output(change_amounts[idx], ck.address, encoding=self.encoding) + on = transaction.add_output(change_amounts[idx], ck.address, encoding=self.encoding, change=True) transaction.outputs[on].key_id = ck.key_id # Shuffle output order to increase privacy @@ -3850,7 +4199,8 @@ 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): + min_confirms=1, priv_keys=None, max_utxos=None, locktime=0, broadcast=False, number_of_change_outputs=1, + random_output_order=True, replace_by_fee=False): """ 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 @@ -3859,7 +4209,7 @@ def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, n Uses the :func:`transaction_create` method to create a new transaction, and uses a random service client to send the transaction. >>> w = Wallet('bitcoinlib_legacy_wallet_test') - >>> t = w.send([('1J9GDZMKEr3ZTj8q6pwtMy4Arvt92FDBTb', 200000)], offline=True) + >>> t = w.send([('1J9GDZMKEr3ZTj8q6pwtMy4Arvt92FDBTb', 200000)]) >>> t >>> t.outputs # doctest:+ELLIPSIS @@ -3885,10 +4235,14 @@ def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, n :type max_utxos: int :param locktime: Transaction level locktime. Locks the transaction until a specified block (value from 1 to 5 million) or until a certain time (Timestamp in seconds after 1-jan-1970). Default value is 0 for transactions without locktime :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 broadcast: Just return the transaction object and do not send it when broadcast = False. Default is False + :type broadcast: 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 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: """ @@ -3898,7 +4252,8 @@ 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) + min_confirms, max_utxos, locktime, number_of_change_outputs, + random_output_order, replace_by_fee) 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: @@ -3910,7 +4265,8 @@ 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) + number_of_change_outputs, random_output_order, + replace_by_fee) transaction.sign(priv_keys) transaction.rawtx = transaction.raw() @@ -3918,18 +4274,19 @@ def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, n transaction.calc_weight_units() transaction.fee_per_kb = int(float(transaction.fee) / float(transaction.vsize) * 1000) transaction.txid = transaction.signature_hash()[::-1].hex() - transaction.send(offline) + transaction.send(broadcast) 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): + priv_keys=None, locktime=0, broadcast=False, number_of_change_outputs=1, random_output_order=True, + replace_by_fee=False): """ Create transaction and send it with default Service objects :func:`services.sendrawtransaction` method. Wrapper for wallet :func:`send` method. >>> w = Wallet('bitcoinlib_legacy_wallet_test') - >>> t = w.send_to('1J9GDZMKEr3ZTj8q6pwtMy4Arvt92FDBTb', 200000, offline=True) + >>> t = w.send_to('1J9GDZMKEr3ZTj8q6pwtMy4Arvt92FDBTb', 200000) >>> t >>> t.outputs # doctest:+ELLIPSIS @@ -3953,21 +4310,26 @@ def send_to(self, to_address, amount, input_key_id=None, account_id=None, networ :type priv_keys: HDKey, list :param locktime: Transaction level locktime. Locks the transaction until a specified block (value from 1 to 5 million) or until a certain time (Timestamp in seconds after 1-jan-1970). Default value is 0 for transactions without locktime :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 broadcast: Just return the transaction object and do not send it when broadcast = False. Default is False + :type broadcast: 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 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: """ 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) + min_confirms=min_confirms, priv_keys=priv_keys, locktime=locktime, broadcast=broadcast, + number_of_change_outputs=number_of_change_outputs, random_output_order=random_output_order, + replace_by_fee=replace_by_fee) 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): + fee_per_kb=None, fee=None, locktime=0, broadcast=False, replace_by_fee=False): """ Sweep all unspent transaction outputs (UTXO's) and send them to one or more output addresses. @@ -4004,8 +4366,10 @@ def sweep(self, to_address, account_id=None, input_key_id=None, network=None, ma :type fee: int, str :param locktime: Transaction level locktime. Locks the transaction until a specified block (value from 1 to 5 million) or until a certain time (Timestamp in seconds after 1-jan-1970). Default value is 0 for transactions without locktime :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 broadcast: Just return the transaction object and do not send it when broadcast = False. Default is False + :type broadcast: 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: """ @@ -4016,6 +4380,7 @@ def sweep(self, to_address, account_id=None, input_key_id=None, network=None, ma utxos = utxos[0:max_utxos] input_arr = [] total_amount = 0 + if not utxos: raise WalletError("Cannot sweep wallet, no UTXO's found") for utxo in utxos: @@ -4024,19 +4389,19 @@ def sweep(self, to_address, account_id=None, input_key_id=None, network=None, ma continue input_arr.append((utxo['txid'], utxo['output_n'], utxo['key_id'], utxo['value'])) total_amount += utxo['value'] - 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) + fee_modifier = 1 if self.witness_type == 'legacy' else 0.6 if isinstance(fee, str): - n_outputs = 1 if not isinstance(to_address, list) else len(to_address) fee_per_kb = srv.estimatefee(priority=fee) - tr_size = 125 + (len(input_arr) * (77 + self.multisig_n_required * 72)) + n_outputs * 30 - fee = 100 + int((tr_size / 1000.0) * fee_per_kb) - + fee = None if not fee: if fee_per_kb is None: fee_per_kb = srv.estimatefee() - tr_size = 125 + (len(input_arr) * 125) - fee = int((tr_size / 1000.0) * fee_per_kb) + n_outputs = 1 if not isinstance(to_address, list) else len(to_address) + tr_size = 125 + (len(input_arr) * (77 + self.multisig_n_required * 72)) + n_outputs * 30 + fee = int(100 + ((tr_size / 1000.0) * fee_per_kb * fee_modifier)) + if total_amount - fee <= self.network.dust_amount: raise WalletError("Amount to send is smaller then dust amount: %s" % (total_amount - fee)) @@ -4057,7 +4422,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) + broadcast=broadcast, replace_by_fee=replace_by_fee) def wif(self, is_private=False, account_id=0): """ @@ -4085,7 +4450,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, network=None): + def public_master(self, account_id=None, name=None, as_private=False, witness_type=None, network=None): """ Return public master key(s) for this wallet. Use to import in other wallets to sign transactions or create keys. @@ -4112,9 +4477,10 @@ def public_master(self, account_id=None, name=None, as_private=False, network=No 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) + cosigner_id=self.cosigner_id, witness_type=witness_type) return key if as_private else key.public() else: pm_list = [] diff --git a/docker/README.rst b/docker/README.rst new file mode 100644 index 00000000..70813d75 --- /dev/null +++ b/docker/README.rst @@ -0,0 +1,13 @@ +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 it will +run all unittests. + +.. code-block:: bash + + $ cd + $ docker build -t bitcoinlib . + $ docker run -it bitcoinlib diff --git a/docker/alpine/Dockerfile b/docker/alpine/Dockerfile new file mode 100644 index 00000000..833478d4 --- /dev/null +++ b/docker/alpine/Dockerfile @@ -0,0 +1,14 @@ +FROM alpine:latest +MAINTAINER Cryp Toon + +WORKDIR /code + +RUN apk add --no-cache git python3-dev gmp-dev python3 py3-pip gcc musl-dev libpq-dev +RUN apk add --no-cache postgresql postgresql-contrib mariadb-dev mysql-client + +RUN git clone https://github.com/1200wd/bitcoinlib.git + +WORKDIR /code/bitcoinlib +RUN python3 -m pip install .[dev] --break-system-packages + +CMD python3 -m unittest diff --git a/docker/fedora/Dockerfile b/docker/fedora/Dockerfile new file mode 100644 index 00000000..379c19d1 --- /dev/null +++ b/docker/fedora/Dockerfile @@ -0,0 +1,17 @@ +FROM fedora:latest +MAINTAINER Cryp Toon + +WORKDIR /code + +RUN yum update -y; yum clean all + +RUN yum install -y python3-devel gmp-devel python3-pip git gcc + +RUN yum install -y postgresql postgresql-server mariadb-server libpq-devel mariadb-devel + +RUN git clone https://github.com/1200wd/bitcoinlib.git + +WORKDIR /code/bitcoinlib +RUN python3 -m pip install .[dev] + +CMD python3 -m unittest diff --git a/docker/kali/Dockerfile b/docker/kali/Dockerfile new file mode 100644 index 00000000..a08056d0 --- /dev/null +++ b/docker/kali/Dockerfile @@ -0,0 +1,21 @@ +FROM kalilinux/kali-last-release +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 + +#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 default-libmysqlclient-dev default-mysql-server pkg-config libpq-dev +RUN apt-get clean + +RUN git clone https://github.com/1200wd/bitcoinlib.git + +WORKDIR /code/bitcoinlib +RUN python3 -m pip install .[dev] + +CMD python3 -m unittest 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/docker/ubuntu/Dockerfile b/docker/ubuntu/Dockerfile new file mode 100644 index 00000000..96366893 --- /dev/null +++ b/docker/ubuntu/Dockerfile @@ -0,0 +1,21 @@ +FROM ubuntu:latest +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 + +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 libmysqlclient-dev pkg-config +RUN apt-get clean + +RUN git clone https://github.com/1200wd/bitcoinlib.git + +WORKDIR /code/bitcoinlib +RUN python3 -m pip install .[dev] + +CMD python3 -m unittest diff --git a/docs/_static/manuals.command-line-wallet.rst b/docs/_static/manuals.command-line-wallet.rst index 6f5d8643..228a062a 100644 --- a/docs/_static/manuals.command-line-wallet.rst +++ b/docs/_static/manuals.command-line-wallet.rst @@ -1,16 +1,7 @@ Command Line Wallet =================== -Manage wallets from commandline. Allows you to - -* Show wallets and wallet info -* Create single and multi signature wallets -* Delete wallets -* Generate receive addresses -* Create transactions -* Import and export transactions -* Sign transactions with available private keys -* Broadcast transaction to the network +Manage Bitcoin wallets from commandline The Command Line wallet Script can be found in the tools directory. If you call the script without arguments it will show all available wallets. @@ -26,19 +17,36 @@ To create a wallet just specify an unused wallet name: .. code-block:: none - $ clw mywallet - Command Line Wallet for BitcoinLib + $ clw new -w mywallet + CREATE wallet 'newwallet' (bitcoin network) + Passphrase: sibling undo gift cat garage survey taxi index admit odor surface waste + Please write down on paper and backup. With this key you can restore your wallet and all keys - Wallet mywallet does not exist, create new wallet [yN]? y + Type 'yes' if you understood and wrote down your key: yes + Wallet info for newwallet + === WALLET === + ID 21 + Name newwallet + Owner + Scheme bip32 + Multisig False + Witness type segwit + Main network bitcoin + Latest update None - CREATE wallet 'mywallet' (bitcoin network) + = Wallet Master Key = + ID 177 + Private True + Depth 0 - Your mnemonic private key sentence is: mutual run dynamic armed brown meadow height elbow citizen put industry work + - NETWORK: bitcoin - + - - Keys + 182 m/84'/0'/0'/0/0 bc1qza24j7snqlmx7603z8qplm4rzfkr0p0mneraqv address index 0 0.00000000 ₿ - Please write down on paper and backup. With this key you can restore your wallet and all keys + - - Transactions Account 0 (0) + + = Balance Totals (includes unconfirmed) = - Type 'yes' if you understood and wrote down your key: yes - Updating wallet Generate / show receive addresses @@ -49,10 +57,10 @@ codes on the commandline install the pyqrcode module. .. code-block:: none - $ clw mywallet -r + $ clw -w mywallet -r Command Line Wallet for BitcoinLib - Receive address is 1JMKBiiDMdjTx6rfqGumALvcRMX6DQNeG1 + Receive address is bc1qza24j7snqlmx7603z8qplm4rzfkr0p0mneraqv Send funds / create transaction @@ -69,20 +77,251 @@ network. .. code-block:: none - $ clw -d dbtest mywallet -t 1FpBBJ2E9w9nqxHUAtQME8X4wGeAKBsKwZ 10000 + $ clw -w mywallet -d dbtest -t bc1qza24j7snqlmx7603z8qplm4rzfkr0p0mneraqv 10000 Restore wallet with passphrase ------------------------------ To restore or create a wallet with a passphrase use new wallet name and the --passphrase option. -If it's an old wallet you can recreate and scan it with the -s option. This will create new -addresses and update unspend outputs. +If it's an old wallet you can recreate and scan it with the -u / --update-transactions option. This will create new +addresses and update unspent outputs. .. code-block:: none - $ clw mywallet --passphrase "mutual run dynamic armed brown meadow height elbow citizen put industry work" - $ clw mywallet -s + $ clw new -w mywallet --passphrase "mutual run dynamic armed brown meadow height elbow citizen put industry work" + $ clw mywallet -ui + +The -i / --wallet-info shows the contents of the updated wallet. + + +Encrypt private key fields +-------------------------- + +Bitcoinlib has build in functionality to encrypt private key fields in the database. If you provide a password in +the runtime environment the data is encrypted at low level in de database module. You can provide a 32 byte key +in the DB_FIELD_ENCRYPTION_KEY variable or a password in the DB_FIELD_ENCRYPTION_PASSWORD variable. + +.. code-block:: bash + + $ export DB_FIELD_ENCRYPTION_PASSWORD=iforgot + $ clw new -w cryptwallet + Command Line Wallet - BitcoinLib 0.6.14 + + CREATE wallet 'cryptwallet' (bitcoin network) + Passphrase: job giant vendor side oil embrace true cushion have matrix glimpse rack + Please write down on paper and backup. With this key you can restore your wallet and all keys + + Type 'yes' if you understood and wrote down your key: yes + ... wallet info ... + + $ clw -w cryptwallet -r + Command Line Wallet - BitcoinLib 0.6.14 + + Receive address: bc1q2cr0chgs6530mdpag2rfn7v9nt232nlpqcc4kc + Install qr code module to show QR codes: pip install pyqrcode + +If we now remove the password from the environment, we cannot open the wallet anymore: + +.. code-block:: bash + + $ export DB_FIELD_ENCRYPTION_PASSWORD= + $ clw -w cryptwallet -i + Command Line Wallet - BitcoinLib 0.6.14 + + ValueError: Data is encrypted please provide key in environment + + +Example: Multi-signature Bitcoinlib test wallet +----------------------------------------------- + +First we generate 2 private keys to create a 2-of-2 multisig wallet: + +.. code-block:: bash + + $ clw -g -n bitcoinlib_test -y + Command Line Wallet - BitcoinLib 0.6.14 + + Passphrase: marine kiwi great try know scan rigid indicate place gossip fault liquid + Please write down on paper and backup. With this key you can restore your wallet and all keys + + Type 'yes' if you understood and wrote down your key: yes + Private Master key, to create multisig wallet on this machine: + BC19UtECk2r9PVQYhY4yboRf92XKEnKZf9hQEd1qBqCgQ98HkBeysLPqYewcWDUuaBRSSVXCShDfmhpbtgZ33sWeGPqfwoLwamzPEcnfwLoeqfQM + Public Master key, to share with other cosigner multisig wallets: + BC18rEEZrakM87qWbSSUv19vnRkEFL7ZtNtGx3exB886VbeFZp6aq9JLZucYAj1EtsHKUB2mkjvafCCGaeYeUVtdFcz5xTxTTgEPCE8fDC8LcahM + Network: bitcoinlib_test + + $ clw -g -n bitcoinlib_test -y + Command Line Wallet - BitcoinLib 0.6.14 + + Passphrase: trumpet utility cotton couch hard shadow ivory alpha glance pear snow emerge + Please write down on paper and backup. With this key you can restore your wallet and all keys + Private Master key, to create multisig wallet on this machine: + BC19UtECk2r9PVQYhaAa8kEgBMPWHC4fJVJD48zBMMb9gSpY9LQVvQ1HhzB3Xmkm2BpiH5SyWoboiewpbeexPLsw8QBfAqMbDfet6kLhedtfQF8r + Public Master key, to share with other cosigner multisig wallets: + BC18rEEvE8begagfJs7kdxx1yW9tFsz7879c9vQQ2mnGbF6WSeKuBEGtmxJYLEy8rpVV9wXffbBtnL1LPKZqujPtEKzHqQeERiRybKB3DRBBoSFH + Network: bitcoinlib_test + +The -g / --generate-key is used to generate a private key passphrase. +With -n / --network we specify the bitcoinlib_test network. This isn't actually a network but allows us to create and +verify transactions. +The -y / --yes options, skips the required user input. +We now use 1 private and 1 public key to create a wallet. + +.. code-block:: bash + + $ clw new -w multisig-2-2 -n bitcoinlib_test -m 2 2 BC19UtECk2r9PVQYhY4yboRf92XKEnKZf9hQEd1qBqCgQ98HkBeysLPqYewcWDUuaBRSSVXCShDfmhpbtgZ33sWeGPqfwoLwamzPEcnfwLoeqfQM BC18rEEvE8begagfJs7kdxx1yW9tFsz7879c9vQQ2mnGbF6WSeKuBEGtmxJYLEy8rpVV9wXffbBtnL1LPKZqujPtEKzHqQeERiRybKB3DRBBoSFH + + Command Line Wallet - BitcoinLib 0.6.14 + + CREATE wallet 'ms22' (bitcoinlib_test network) + Wallet info for ms22 + === WALLET === + ID 22 + Name ms22 + Owner + Scheme bip32 + Multisig True + Multisig Wallet IDs 23, 24 + Cosigner ID 1 + Witness type segwit + Main network bitcoinlib_test + Latest update None + + = Multisig Public Master Keys = + 0 183 BC18rEEvE8begagfJs7kdxx1yW9tFsz7879c9vQQ2mnGbF6WSeKuBEGtmxJYLEy8rpVV9wXffbBtnL1LPKZqujPtEKzHqQeERiRybKB3DRBBoSFH bip32 cosigner + 1 186 BC18rEEZrakM87qWbSSUv19vnRkEFL7ZtNtGx3exB886VbeFZp6aq9JLZucYAj1EtsHKUB2mkjvafCCGaeYeUVtdFcz5xTxTTgEPCE8fDC8LcahM bip32 main * + For main keys a private master key is available in this wallet to sign transactions. * cosigner key for this wallet + + - NETWORK: bitcoinlib_test - + - - Keys + + - - Transactions Account 0 (0) + + = Balance Totals (includes unconfirmed) = + +The multisig wallet has been created, you can view the wallet info by using the -i / --wallet-info option. Now we +generate a new receiving address with the -r / --receive option and update the unspent outputs with the +-x / --update-utxos option. + +.. code-block:: bash + + $ clw -w ms22 -r + Command Line Wallet - BitcoinLib 0.6.14 + + Receive address: blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p + Install qr code module to show QR codes: pip install pyqrcode + + $ clw -w ms22 -x + Command Line Wallet - BitcoinLib 0.6.14 + + Updating wallet utxo's + $ clw -w ms22 -i + Command Line Wallet - BitcoinLib 0.6.14 + + Wallet info for ms22 + === WALLET === + ID 22 + Name ms22 + Owner + Scheme bip32 + Multisig True + Multisig Wallet IDs 23, 24 + Cosigner ID 1 + Witness type segwit + Main network bitcoinlib_test + Latest update None + + = Multisig Public Master Keys = + 0 183 BC18rEEvE8begagfJs7kdxx1yW9tFsz7879c9vQQ2mnGbF6WSeKuBEGtmxJYLEy8rpVV9wXffbBtnL1LPKZqujPtEKzHqQeERiRybKB3DRBBoSFH bip32 cosigner + 1 186 BC18rEEZrakM87qWbSSUv19vnRkEFL7ZtNtGx3exB886VbeFZp6aq9JLZucYAj1EtsHKUB2mkjvafCCGaeYeUVtdFcz5xTxTTgEPCE8fDC8LcahM bip32 main * + For main keys a private master key is available in this wallet to sign transactions. * cosigner key for this wallet + + - NETWORK: bitcoinlib_test - + - - Keys + 193 m/48'/9999999'/0'/2'/0/0 blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p Multisig Key 185/192 2.00000000 T + + - - Transactions Account 0 (2) + 7b020ae9c7f8ba84a5a5136ae32e6195af5a4f25316f790a1278e04f479ca77d blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p 10 1.00000000 T U + 5d0f176259ab4bc596363aa3653c44858ebeb2fd8361311966776192968e545d blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p 10 1.00000000 T U + + = Balance Totals (includes unconfirmed) = + bitcoinlib_test (Account 0) 2.00000000 T + +We now have some utxo's in our wallet so we can create a transaction + +.. code-block:: bash + + $ clw -w ms22 -s blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p 0.1 + Connected to pydev debugger (build 233.13135.95) + Command Line Wallet - BitcoinLib 0.6.14 + + Transaction created + Transaction 3b96f493d189667565271041abbc0efbd8631bb54d76decb90e144bb145fa613 + Date: None + Network: bitcoinlib_test + Version: 1 + Witness type: segwit + Status: new + Verified: False + Inputs + - blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p 1.00000000 TST 7b020ae9c7f8ba84a5a5136ae32e6195af5a4f25316f790a1278e04f479ca77d 0 + segwit p2sh_multisig; sigs: 1 (2-of-2) not validated + Outputs + - blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p 0.10000000 TST p2wsh U + - blt1qe4tr993nftagprtapclxrm7ahrcvl4w0dnxfnhz2cx6pjaeg989syy9zge 0.89993601 TST p2wsh U + Size: 192 + Vsize: 192 + Fee: 6399 + Confirmations: None + Block: None + Pushed to network: False + Wallet: ms22 + + Transaction created but not sent yet. Transaction dictionary for export: + {} + +Copy the contents of the dictionary and save it as 3b96f493d189667565271041abbc0efbd8631bb54d76decb90e144bb145fa613.tx + +The transaction has been created, but cannot be verified because the wallet contains only 1 private key. So we need to +create another wallet with the other private key, in real life situations this would be on another (offiline) machine. + +Below we create a new wallet, generate a receive address and update the utxo's. Finally we can import the transaction +dictionary which we be signed once imported. And as you can see the transaction has been verified now! + +.. code-block:: bash + + $ clw new -w multisig-2-2-signer2 -n bitcoinlib_test -m 2 2 BC18rEEZrakM87qWbSSUv19vnRkEFL7ZtNtGx3exB886VbeFZp6aq9JLZucYAj1EtsHKUB2mkjvafCCGaeYeUVtdFcz5xTxTTgEPCE8fDC8LcahM BC19UtECk2r9PVQYhaAa8kEgBMPWHC4fJVJD48zBMMb9gSpY9LQVvQ1HhzB3Xmkm2BpiH5SyWoboiewpbeexPLsw8QBfAqMbDfet6kLhedtfQF8r + $ clw -w multisig-2-2-signer2 -r + $ clw -w multisig-2-2-signer2 -x + $ clw -w multisig-2-2-signer2 -a tx.tx + Command Line Wallet - BitcoinLib 0.6.14 + + Transaction 3b96f493d189667565271041abbc0efbd8631bb54d76decb90e144bb145fa613 + Date: None + Network: bitcoinlib_test + Version: 1 + Witness type: segwit + Status: new + Verified: True + Inputs + - blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p 1.00000000 TST 7b020ae9c7f8ba84a5a5136ae32e6195af5a4f25316f790a1278e04f479ca77d 0 + segwit p2sh_multisig; sigs: 2 (2-of-2) valid + Outputs + - blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p 0.10000000 TST p2wsh U + - blt1qe4tr993nftagprtapclxrm7ahrcvl4w0dnxfnhz2cx6pjaeg989syy9zge 0.89993601 TST p2wsh U + Size: 192 + Vsize: 192 + Fee: 6399 + Confirmations: None + Block: None + Pushed to network: False + Wallet: multisig-2-2-signer2 + + + Signed transaction: + {} Options Overview @@ -90,92 +329,99 @@ Options Overview Command Line Wallet for BitcoinLib -.. code-block:: none +usage: clw.py [-h] [--list-wallets] [--generate-key] [--passphrase-strength PASSPHRASE_STRENGTH] [--database DATABASE] [--wallet_name [WALLET_NAME]] [--network NETWORK] [--witness-type WITNESS_TYPE] [--yes] + [--quiet] [--wallet-remove] [--wallet-info] [--update-utxos] [--update-transactions] [--wallet-empty] [--receive] [--cosigner-id COSIGNER_ID] [--export-private] + [--import-private IMPORT_PRIVATE] [--send ADDRESS AMOUNT] [--number-of-change-outputs NUMBER_OF_CHANGE_OUTPUTS] [--input-key-id INPUT_KEY_ID] [--sweep ADDRESS] [--fee FEE] + [--fee-per-kb FEE_PER_KB] [--push] [--import-tx TRANSACTION] [--import-tx-file FILENAME_TRANSACTION] + {new} ... + +BitcoinLib command line wallet + +positional arguments: + {new} + +options: + -h, --help show this help message and exit + --list-wallets, -l List all known wallets in database + --generate-key, -g Generate a new masterkey, and show passphrase, WIF and public account key. Can be used to create a new (multisig) wallet + --passphrase-strength PASSPHRASE_STRENGTH + Number of bits for passphrase key. Default is 128, lower is not advised but can be used for testing. Set to 256 bits for more future-proof passphrases + --database DATABASE, -d DATABASE + URI of the database to use + --wallet_name [WALLET_NAME], -w [WALLET_NAME] + Name of wallet to create or open. Provide wallet name or number when running wallet actions + --network NETWORK, -n NETWORK + Specify 'bitcoin', 'litecoin', 'testnet' or other supported network + --witness-type WITNESS_TYPE, -j WITNESS_TYPE + Witness type of wallet: legacy, p2sh-segwit or segwit (default) + --yes, -y Non-interactive mode, does not prompt for confirmation + --quiet, -q Quiet mode, no output writen to console + +Wallet Actions: + --wallet-remove Name or ID of wallet to remove, all keys and transactions will be deleted + --wallet-info, -i Show wallet information + --update-utxos, -x Update unspent transaction outputs (UTXO's) for this wallet + --update-transactions, -u + Update all transactions and UTXO's for this wallet + --wallet-empty, -z Delete all keys and transactions from wallet, except for the masterkey(s). Use when updating fails or other errors occur. Please backup your database and masterkeys first. Update + empty wallet again to restore your wallet. + --receive, -r Show unused address to receive funds. + --cosigner-id COSIGNER_ID, -o COSIGNER_ID + Set this if wallet contains only public keys, more then one private key or if you would like to create keys for other cosigners. + --export-private, -e Export private key for this wallet and exit + --import-private IMPORT_PRIVATE, -v IMPORT_PRIVATE + Import private key in this wallet + +Transactions: + --send ADDRESS AMOUNT, -s ADDRESS AMOUNT + Create transaction to send amount to specified address. To send to multiple addresses, argument can be used multiple times. + --number-of-change-outputs NUMBER_OF_CHANGE_OUTPUTS + Number of change outputs. Default is 1, increase for more privacy or to split funds + --input-key-id INPUT_KEY_ID, -k INPUT_KEY_ID + Use to create transaction with 1 specific key ID + --sweep ADDRESS Sweep wallet, transfer all funds to specified address + --fee FEE, -f FEE Transaction fee + --fee-per-kb FEE_PER_KB, -b FEE_PER_KB + Transaction fee in satoshi per kilobyte + --push, -p Push created transaction to the network + --import-tx TRANSACTION + Import raw transaction hash or transaction dictionary in wallet and sign it with available key(s) + --import-tx-file FILENAME_TRANSACTION, -a FILENAME_TRANSACTION + Import transaction dictionary or raw transaction string from specified filename and sign it with available key(s) + + +Options overview: New Wallet +---------------------------- + +usage: clw.py new [-h] --wallet_name [WALLET_NAME] [--password PASSWORD] [--network NETWORK] [--passphrase PASSPHRASE] [--create-from-key KEY] [--create-multisig [. ...]] [--witness-type WITNESS_TYPE] + [--cosigner-id COSIGNER_ID] [--database DATABASE] [--receive] [--yes] [--quiet] + +Create new wallet + +options: + -h, --help show this help message and exit + --wallet_name [WALLET_NAME], -w [WALLET_NAME] + Name of wallet to create or open. Provide wallet name or number when running wallet actions + --password PASSWORD Password for BIP38 encrypted key. Use to create a wallet with a protected key + --network NETWORK, -n NETWORK + Specify 'bitcoin', 'litecoin', 'testnet' or other supported network + --passphrase PASSPHRASE + Passphrase to recover or create a wallet. Usually 12 or 24 words + --create-from-key KEY, -c KEY + Create a new wallet from specified key + --create-multisig [. ...], -m [. ...] + [NUMBER_OF_SIGNATURES_REQUIRED, NUMBER_OF_SIGNATURES, KEY-1, KEY-2, ... KEY-N]Specify number of signatures followed by the number of signatures required and then a list of public or + private keys for this wallet. Private keys will be created if not provided in key list. Example, create a 2-of-2 multisig wallet and provide 1 key and create another key: -m 2 2 + tprv8ZgxMBicQKsPd1Q44tfDiZC98iYouKRC2CzjT3HGt1yYw2zuX2awTotzGAZQEAU9bi2M5MCj8iedP9MREPjUgpDEBwBgGi2C8eK5zNYeiX8 + tprv8ZgxMBicQKsPeUbMS6kswJc11zgVEXUnUZuGo3bF6bBrAg1ieFfUdPc9UHqbD5HcXizThrcKike1c4z6xHrz6MWGwy8L6YKVbgJMeQHdWDp + --witness-type WITNESS_TYPE, -j WITNESS_TYPE + Witness type of wallet: legacy, p2sh-segwit or segwit (default) + --cosigner-id COSIGNER_ID, -o COSIGNER_ID + Set this if wallet contains only public keys, more then one private key or if you would like to create keys for other cosigners. + --database DATABASE, -d DATABASE + URI of the database to use + --receive, -r Show unused address to receive funds. + --yes, -y Non-interactive mode, does not prompt for confirmation + --quiet, -q Quiet mode, no output writen to console + - usage: clw.py [-h] [--wallet-remove] [--list-wallets] [--wallet-info] - [--update-utxos] [--update-transactions] - [--wallet-recreate] [--receive [NUMBER_OF_ADDRESSES]] - [--generate-key] [--export-private] - [--passphrase [PASSPHRASE [PASSPHRASE ...]]] - [--passphrase-strength PASSPHRASE_STRENGTH] - [--network NETWORK] [--database DATABASE] - [--create-from-key KEY] - [--create-multisig [NUMBER_OF_SIGNATURES_REQUIRED [KEYS ...]]] - [--create-transaction [ADDRESS_1 [AMOUNT_1 ...]]] - [--sweep ADDRESS] [--fee FEE] [--fee-per-kb FEE_PER_KB] - [--push] [--import-tx TRANSACTION] - [--import-tx-file FILENAME_TRANSACTION] - [wallet_name] - - BitcoinLib CLI - - positional arguments: - wallet_name Name of wallet to create or open. Used to store your - all your wallet keys and will be printed on each paper - wallet - - optional arguments: - -h, --help show this help message and exit - - Wallet Actions: - --wallet-remove Name or ID of wallet to remove, all keys and - transactions will be deleted - --list-wallets, -l List all known wallets in BitcoinLib database - --wallet-info, -w Show wallet information - --update-utxos, -x Update unspent transaction outputs (UTXO's) for this - wallet - --update-transactions, -u - Update all transactions and UTXO's for this wallet - --wallet-recreate, -z - Delete all keys and transactions and recreate wallet, - except for the masterkey(s). Use when updating fails - or other errors occur. Please backup your database and - masterkeys first. - --receive [COSIGNER_ID], -r [COSIGNER_ID] - Show unused address to receive funds. Generate new - payment and change addresses if no unused addresses are - available. - --generate-key, -k Generate a new masterkey, and show passphrase, WIF and - public account key. Use to create multisig wallet - --export-private, -e Export private key for this wallet and exit - - Wallet Setup: - --passphrase [PASSPHRASE [PASSPHRASE ...]] - Passphrase to recover or create a wallet. Usually 12 - or 24 words - --passphrase-strength PASSPHRASE_STRENGTH - Number of bits for passphrase key. Default is 128, - lower is not adviced but can be used for testing. Set - to 256 bits for more future proof passphrases - --network NETWORK, -n NETWORK - Specify 'bitcoin', 'litecoin', 'testnet' or other - supported network - --database DATABASE, -d DATABASE - Name of specific database file to use - --create-from-key KEY, -c KEY - Create a new wallet from specified key - --create-multisig [NUMBER_OF_SIGNATURES_REQUIRED [KEYS ...]], -m [NUMBER_OF_SIGNATURES_REQUIRED [KEYS ...]] - Specificy number of signatures required followed by a - list of signatures. Example: -m 2 tprv8ZgxMBicQKsPd1Q4 - 4tfDiZC98iYouKRC2CzjT3HGt1yYw2zuX2awTotzGAZQEAU9bi2M5M - Cj8iedP9MREPjUgpDEBwBgGi2C8eK5zNYeiX8 tprv8ZgxMBicQKsP - eUbMS6kswJc11zgVEXUnUZuGo3bF6bBrAg1ieFfUdPc9UHqbD5HcXi - zThrcKike1c4z6xHrz6MWGwy8L6YKVbgJMeQHdWDp - - Transactions: - --create-transaction [ADDRESS_1 [AMOUNT_1 ...]], -t [ADDRESS_1 [AMOUNT_1 ...]] - Create transaction. Specify address followed by - amount. Repeat for multiple outputs - --sweep ADDRESS Sweep wallet, transfer all funds to specified address - --fee FEE, -f FEE Transaction fee - --fee-per-kb FEE_PER_KB - Transaction fee in sathosis (or smallest denominator) - per kilobyte - --push, -p Push created transaction to the network - --import-tx TRANSACTION, -i TRANSACTION - Import raw transaction hash or transaction dictionary - in wallet and sign it with available key(s) - --import-tx-file FILENAME_TRANSACTION, -a FILENAME_TRANSACTION - Import transaction dictionary or raw transaction - string from specified filename and sign it with - available key(s) diff --git a/docs/_static/manuals.databases.rst b/docs/_static/manuals.databases.rst index cdb94cb6..06df201b 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 MySQL database --------------------- +Using MariaDB / 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,6 +32,7 @@ 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 ------------------------- @@ -54,14 +55,23 @@ And assume you unwisely have chosen the password 'secret' you can use the databa .. code-block:: python - db_uri = 'postgresql://bitcoinlib:secret@localhost:5432/' + db_uri = 'postgresql+psycopg://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. -Encrypt database ----------------- +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 use an encrypted database. +.. 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 `Using Encrypted Databases `_ for more information. \ No newline at end of file +Please read `Encrypt Database or Private Keys `_ for more information. 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 c7c9a8c5..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. @@ -13,7 +13,7 @@ Ten tips for more privacy and security when using Bitcoin and Bitcoinlib: 4. Use a random number of change outputs and shuffle order of inputs and outputs. This way it is not visible which output is the change output. In the Wallet object you can set the number_of_change_outputs to zero to generate a random number of change outputs. -5. `Encrypt your database `_ with SQLCipher. +5. `Encrypt your database or private keys `_ with SQLCipher or AES. 6. Use password protected private keys. For instance use a password when `creating wallets `_. 7. Backup private keys and passwords! I have no proof but I assume more bitcoins are lost because of lost private keys then there are lost due to hacking... 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/_static/manuals.sqlcipher.rst b/docs/_static/manuals.sqlcipher.rst index b4ddaf2a..30341480 100644 --- a/docs/_static/manuals.sqlcipher.rst +++ b/docs/_static/manuals.sqlcipher.rst @@ -1,5 +1,13 @@ -Using SQLCipher encrypted database -================================== +Encrypt Database or Private Keys +================================ + +If you database contains private keys it is a good idea to encrypt your data. This will not be done automatically. At the moment you have 2 options: + +- Encrypt the database with SQLCipher. The database is fully encrypted and you need to provide the password in the Database URI when opening the database. +- Use a normal database but all private key data will be stored AES encrypted in the database. A key to encrypt and decrypt need to be provided in the Environment. + +Encrypt database with SQLCipher +------------------------------- To protect your data such as the private keys you can use SQLCipher to encrypt the full database. SQLCipher is a SQLite extension which uses 256-bit AES encryption and also works together with SQLAlchemy. @@ -14,8 +22,7 @@ your system might require other packages. Please read https://www.zetetic.net/sq # Previous, but now unmaintained: $ pip install pysqlcipher3 -Create an Encrypted Database for your Wallet --------------------------------------------- +**Create an Encrypted Database for your Wallet** Now you can simply create and use an encrypted database by supplying a password as argument to the Wallet object: @@ -26,8 +33,7 @@ Now you can simply create and use an encrypted database by supplying a password wlt = wallet_create_or_open('bcltestwlt4', network='bitcoinlib_test', db_uri=db_uri, db_password=password) -Encrypt using Database URI --------------------------- +**Encrypt using Database URI** You can also use a SQLCipher database URI to create and query a encrypted database: @@ -44,3 +50,53 @@ If you look at the contents of the SQLite database you can see it is encrypted. $ cat ~/.bitcoinlib/database/bcl_encrypted.db + + +Encrypt private key fields with AES +----------------------------------- + +It is also possible to just encrypt the private keys in the database with secure AES encryption. You need to provide a key or password as environment variable. + +* You can skip this step if you want, but this provides an extra warning / check when no encryption key is found: Enable database encryption in Bitcoinlib configuration settings at ~/.bitcoinlib/config.ini + +.. code-block:: text + + # Encrypt private key field in database using symmetrically EAS encryption. + # You need to set the password in the DB_FIELD_ENCRYPTION_KEY environment variable. + database_encryption_enabled=True + +You can provide an encryption key directly or use a password to create a key: + +1. Generate a secure 32 bytes encryption key yourself with Bitcoinlib: + +.. code-block:: python + + >>> from bitcoinlib.keys import Key + >>> Key().private_hex() + '2414966ea9f2de189a61953c333f61013505dfbf8e383b5ed6cb1981d5ec2620' + +This key needs to be stored in the environment when creating or accessing a wallet. No extra arguments have to be provided to the Wallet class, the data is encrypted an decrypted at database level. + +2. You can also just provide a password, and let Bitcoinlib create a key for you. You will need to pass the DB_FIELD_ENCRYPTION_PASSWORD environment variable. + +There are several ways to store the key in an Environment variable, on Linux you can do: + +.. code-block:: bash + + $ export DB_FIELD_ENCRYPTION_KEY='2414966ea9f2de189a61953c333f61013505dfbf8e383b5ed6cb1981d5ec2620' + +or + +.. code-block:: bash + + $ export DB_FIELD_ENCRYPTION_PASSWORD=ineedtorememberthispassword + +Or in Windows: + +.. code-block:: bash + + $ setx DB_FIELD_ENCRYPTION_KEY '2414966ea9f2de189a61953c333f61013505dfbf8e383b5ed6cb1981d5ec2620' + +Environment variables can also be stored in an .env key, in a virtual environment or in Python code itself. However anyone with access to the key can decrypt your private keys. + +Please make sure to remember and backup your encryption key or password, if you loose your key the private keys can not be recovered! \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 21182508..ab253b49 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -35,8 +35,8 @@ # General information about the project. project = 'Bitcoinlib' -copyright = '2023, Coineva (mccwdev)' -author = 'Lennart (mccwdev)' +copyright = '2017-2024, Coineva (mccwdev)' +author = 'Cryp Toon (mccwdev)' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -45,7 +45,7 @@ # The short X.Y version. version = '0.6' # The full version, including alpha/beta/rc tags. -release = '0.6.13' +release = '0.6.15' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -115,7 +115,7 @@ # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'Bitcoinlib.tex', 'Bitcoinlib Documentation', - 'Lennart (mccwdev)', 'manual'), + 'Cryp Toon (mccwdev)', 'manual'), ] diff --git a/docs/index.rst b/docs/index.rst index 6fc68ca1..7b644038 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -149,16 +149,39 @@ To create a new Bitcoin wallet .. code-block:: bash - $ clw newwallet - Command Line Wallet for BitcoinLib + $ python bitcoinlib/tools/clw.py new -w newwallet + Command Line Wallet - BitcoinLib 0.6.14 - Wallet newwallet does not exist, create new wallet [yN]? y + CREATE wallet 'newwallet' (bitcoin network) + Passphrase: sibling undo gift cat garage survey taxi index admit odor surface waste + Please write down on paper and backup. With this key you can restore your wallet and all keys - CREATE wallet 'newwallet' (bitcoin network) + Type 'yes' if you understood and wrote down your key: yes + Wallet info for newwallet + === WALLET === + ID 21 + Name newwallet + Owner + Scheme bip32 + Multisig False + Witness type segwit + Main network bitcoin + Latest update None + + = Wallet Master Key = + ID 177 + Private True + Depth 0 + + - NETWORK: bitcoin - + - - Keys + 182 m/84'/0'/0'/0/0 bc1qza24j7snqlmx7603z8qplm4rzfkr0p0mneraqv address index 0 0.00000000 ₿ + + - - Transactions Account 0 (0) + + = Balance Totals (includes unconfirmed) = - Your mnemonic private key sentence is: force humble chair kiss season ready elbow cool awake divorce famous tunnel - Please write down on paper and backup. With this key you can restore your wallet and all keys You can use the command line wallet 'clw' to create simple or multisig wallets for various networks, manage public and private keys and managing transactions. @@ -203,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 2932366a..1880f50c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ requests>=2.25.0 -SQLAlchemy>=1.4.28 -ecdsa>=0.17 -sphinx>=5.0.0 -sphinx_rtd_theme>=0.5.0 -numpy>=1.21.0 -pycryptodome>=3.14.1 +SQLAlchemy>=2.0.0 +fastecdsa>=2.2.1 +sphinx>=6.0.0 +sphinx_rtd_theme>=2.0.0 +numpy>=1.22.0 +pycryptodome>=3.16.0 diff --git a/examples/bip38_encrypted_wif_private_key.py b/examples/bip38_encrypted_wif_private_key.py new file mode 100644 index 00000000..37fe706e --- /dev/null +++ b/examples/bip38_encrypted_wif_private_key.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# +# BitcoinLib - Python Cryptocurrency Library +# +# EXAMPLES - Bip38 encrypted private keys +# +# © 2024 March - 1200 Web Development +# + +from bitcoinlib.keys import * + +# +# Example #1 - BIP38 key - no EC multiplication +# +private_wif = "L3QtG7mpcV1AiGCuRCi34HgwTtWDPWNe6m8Wi58S1LzavAsu3v1x" +password = "bitcoinlib" +expected_encrypted_wif = "6PYRg5u7XPXoL9v8nXbBJkzjcMtCqSDM1p9MJttpXb42W1DNt33iX8tosj" +k = HDKey(private_wif, witness_type='legacy') +encrypted_wif = k.encrypt(password=password) +assert(encrypted_wif == expected_encrypted_wif) +print("Encrypted WIF: %s" % encrypted_wif) + +k2 = HDKey(encrypted_wif, password=password, witness_type='legacy') +assert(k2.wif_key() == private_wif) +print("Decrypted WIF: %s" % k2.wif_key()) + + +# +# Example #2 - EC multiplied BIP38 key encryption - not lot and sequence +# from https://bip38.readthedocs.io/en/v0.3.0/index.html +# +passphrase = "meherett" +owner_salt = "75ed1cdeb254cb38" +seed = "99241d58245c883896f80843d2846672d7312e6195ca1a6c" +compressed = False +expected_intermediate_password = "passphraseondJwvQGEWFNrNJRPi4G5XAL5SU777GwTNtqmDXqA3CGP7HXfH6AdBxxc5WUKC" +expected_encrypted_wif = "6PfP7T3iQ5jLJLsH5DneySLLF5bhd879DHW87Pxzwtwvn2ggcncxsNKN5c" +expected_confirmation_code = "cfrm38V5NZfTZKRaRDTvFAMkNKqKAxTxdDjDdb5RpFfXrVRw7Nov5m2iP3K1Eg5QQRxs52kgapy" +expected_private_key = "5Jh21edvnWUXFjRz8mDVN3CSPp1CyTuUSFBKZeWYU726R6MW3ux" + +intermediate_password = bip38_intermediate_password(passphrase, owner_salt=owner_salt) +assert(intermediate_password == expected_intermediate_password) +print("\nIntermediate Password: %s" % intermediate_password) + +res = bip38_create_new_encrypted_wif(intermediate_password, compressed=compressed, seed=seed) +assert(res['encrypted_wif'] == expected_encrypted_wif) +print("Encrypted WIF: %s" % res['encrypted_wif']) +assert(res['confirmation_code'] == expected_confirmation_code) +print("Confirmation Code: %s" % res['confirmation_code']) + +k = HDKey(res['encrypted_wif'], password=passphrase, compressed=compressed, witness_type='legacy') +assert(k.wif_key() == expected_private_key) +print("Private WIF: %s" % k.wif_key()) + +# +# Example #2 - EC multiplied BIP38 key encryption - with lot and sequence +# from https://bip38.readthedocs.io/en/v0.3.0/index.html +# +passphrase = "meherett" +owner_salt = "75ed1cdeb254cb38" +seed = "99241d58245c883896f80843d2846672d7312e6195ca1a6c" +compressed = True +lot = 369861 +sequence = 1 +expected_intermediate_password = "passphraseb7ruSNDGP7cmnFHQpmos7TeAy26AFN4GyRTBqq6hiaFbQzQBvirD9oHsafQvzd" +expected_encrypted_wif = "6PoEPBnJjm8UAiSGWQEKKNq9V2GMHqKkTcUqUFzsaX7wgjpQWR2qWPdnpt" +expected_confirmation_code = "cfrm38VWx5xH1JFm5EVE3mzQvDPFkz7SqNiaFxhyUfp3Fjc2wdYmK7dGEWoW6irDPSrwoaxB5zS" +expected_private_key = "KzFbTBirbEEtEPgWL3xhohUcrg6yUmJupAGrid7vBP9F2Vh8GTUB" + +intermediate_password = bip38_intermediate_password(passphrase, lot=lot, sequence=sequence, owner_salt=owner_salt) +assert(intermediate_password == expected_intermediate_password) +print("\nIntermediate Password: %s" % intermediate_password) + +res = bip38_create_new_encrypted_wif(intermediate_password, compressed=compressed, seed=seed) +assert(res['encrypted_wif'] == expected_encrypted_wif) +print("Encrypted WIF: %s" % res['encrypted_wif']) +assert(res['confirmation_code'] == expected_confirmation_code) +print("Confirmation Code: %s" % res['confirmation_code']) + +k = HDKey(res['encrypted_wif'], password=passphrase, compressed=compressed, witness_type='legacy') +assert(k.wif_key() == expected_private_key) +print("Private WIF: %s" % k.wif_key()) diff --git a/examples/bitcoind_regtest.py b/examples/bitcoind_regtest.py index 439b7fb1..e2c0df3f 100644 --- a/examples/bitcoind_regtest.py +++ b/examples/bitcoind_regtest.py @@ -4,24 +4,54 @@ # # EXAMPLES - Bitcoind regtest network example # -# © 2022 Februari - 1200 Web Development +# © 2023 December - 1200 Web Development # from bitcoinlib.services.bitcoind import * +from bitcoinlib.wallets import wallet_create_or_open +from bitcoinlib.services.services import Service from pprint import pprint bdc = BitcoindClient(base_url='http://rpcuser:pwd@localhost:18444') +walletname = 'regtesttestwallet' +walletbcl = 'regtestwallet' + print("Current blockheight is %d" % bdc.proxy.getblockcount()) + +print("Open or create a new wallet") +wallets = bdc.proxy.listwallets() +if walletname not in wallets: + wallet = bdc.proxy.createwallet(walletname) + address = bdc.proxy.getnewaddress() print("Mine 50 blocks and generate regtest coins to address %s" % address) bdc.proxy.generatetoaddress(50, address) + print("Current blockheight is %d" % bdc.proxy.getblockcount()) +print("Current balance is %d" % bdc.proxy.getbalance()) -address2 = bdc.proxy.getnewaddress() -print("Send 10 rBTC to address %s" % address2) +w = wallet_create_or_open(walletname, network='regtest') +address2 = w.get_key().address +print("\nSend 10 rBTC to address %s" % address2) bdc.proxy.settxfee(0.00002500) txid = bdc.proxy.sendtoaddress(address2, 10) print("Resulting txid: %s" % txid) tx = bdc.proxy.gettransaction(txid) pprint(tx) + + +print("\n\nConnect to bitcoind regtest with Service class and retrieve new transaction and utxo info") +srv = Service(network='regtest', providers=['bitcoind'], cache_uri='') +print("Blockcount %d" % srv.blockcount()) +b = srv.getblock(500) +pprint(b.as_dict()) +b.transactions[0].info() + +t = srv.gettransaction(txid) +t.info() + +utxos = srv.getutxos(address) +print(srv.getbalance(address)) +for utxo in utxos: + print(utxo['txid'], utxo['value'], utxo['confirmations'], utxo['block_height']) \ No newline at end of file diff --git a/examples/wallet_bitcoind_connected_wallets.py b/examples/wallet_bitcoind_connected_wallets.py new file mode 100644 index 00000000..7acb2116 --- /dev/null +++ b/examples/wallet_bitcoind_connected_wallets.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# +# BitcoinLib - Python Cryptocurrency Library +# +# EXAMPLES - Using Bitcoin Core wallets with Bitcoinlib +# +# Method 1 - Create wallet in Bitcoin Core and use the same wallet in Bitcoinlib using the bitcoin node to +# receive and send bitcoin transactions. Only works for legacy wallets. +# +# © 2024 April - 1200 Web Development +# + +from bitcoinlib.wallets import * +from bitcoinlib.services.bitcoind import BitcoindClient + +# +# Settings and Initialization +# + +# Call bitcoin-cli dumpwallet and look for extended private masterkey at top off the export. Then copy the +# bitcoin core node masterseed here: +pkwif = 'tprv8ZgxMBicQKsPe2iVrERVdAgjcqHhxvZcWeS2Va6nvgddpDH1r33A4aTtdYYkoFDY6CCf5fogwLYmAdQQNxkk7W3ygwFd6hquJVLmmpbJRp2' +enable_verify_wallet = False + +# Put connection string with format http://bitcoinlib:password@localhost:18332) +# to Bitcoin Core node in the following file: +bitcoind_url = open(os.path.join(os.path.expanduser('~'), ".bitcoinlib/.bitcoind_connection_string")).read() +bcc = BitcoindClient(base_url=bitcoind_url) +lastblock = bcc.proxy.getblockcount() +print("Connected to bitcoind, last block: " + str(lastblock)) + +# +# Create a copy of the Bitcoin Core Wallet in Bitcoinlib +# +w = wallet_create_or_open('wallet_bitcoincore', pkwif, network='testnet', witness_type='segwit', + key_path=KEY_PATH_BITCOINCORE) +addr = bcc.proxy.getnewaddress() +addrinfo = bcc.proxy.getaddressinfo(addr) +bcl_addr = w.key_for_path(addrinfo['hdkeypath']).address + +# Verify if we are using the same wallet +if enable_verify_wallet and addr == bcl_addr: + print("Address %s with path %s, is identical in Bitcoin core and Bitcoinlib" % (addr, addrinfo['hdkeypath'])) +elif not addr == bcl_addr: + print ("Address %s with path %s, is NOT identical in Bitcoin core and Bitcoinlib" % (addr, addrinfo['hdkeypath'])) + raise ValueError("Wallets not identical in Bitcoin core and Bitcoinlib") + +# +# Using wallets +# + +# Now pick an address from your wallet and send some testnet coins to it, for example by using another wallet or a +# testnet faucet. +w.providers = ['bitcoind'] +w.scan() +# w.info() + +if not w.balance(): + print("No testnet coins available") +else: + print("Found testnet coins. Wallet balance: %d" % w.balance()) + # Send some coins to our own wallet + t = w.send_to(w.get_key().address, 1000, fee=200, broadcast=True) + t.info() + +# If you now run bitcoin-cli listunspent 0, you should see the 1 or 2 new utxo's for this transaction. diff --git a/examples/wallet_bitcoind_connected_wallets2.py b/examples/wallet_bitcoind_connected_wallets2.py new file mode 100644 index 00000000..8151df18 --- /dev/null +++ b/examples/wallet_bitcoind_connected_wallets2.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# +# BitcoinLib - Python Cryptocurrency Library +# +# EXAMPLES - Using Bitcoin Core wallets with Bitcoinlib +# +# Method 2 - Create wallet in Bitcoin Core, export public keys to bitcoinlib and easily manage wallet from bitcoinlib. +# +# © 2024 May - 1200 Web Development +# + +from bitcoinlib.wallets import * +from bitcoinlib.services.bitcoind import BitcoindClient + +# +# Settings and Initialization +# + +# Create wallet in Bitcoin Core and export descriptors +# $ bitcoin-cli createwallet wallet_bitcoincore2 +# $ bitcoin-cli -rpcwallet=wallet_bitcoincore2 listdescriptors + +# Now copy the descriptor of the public master key, which looks like: wpkh([.../84h/1h/0h] +pkwif = 'tpubDDuQM8y9z4VQW5FS13BXGMxUwkUKEXc8KG5xzzbe6UsssrJDKJEygqbgMATnn6ZDwLXQ5PQipH989qWRTzFhPPZMiHxYYrG14X34vc24pD6' + +# You can create the wallet and manage it from bitcoinlib +w = wallet_create_or_open("wallet_bitcoincore2", keys=pkwif, witness_type='segwit') +w.providers=['bitcoind'] +w.scan(scan_gap_limit=1) +w.info() diff --git a/examples/wallet_multisig_3of5.py b/examples/wallet_multisig_3of5.py index 9e1f62bc..51998414 100644 --- a/examples/wallet_multisig_3of5.py +++ b/examples/wallet_multisig_3of5.py @@ -96,7 +96,7 @@ print("\nNew unspent outputs found!") print("Now a new transaction will be created to sweep this wallet and send bitcoins to a testnet faucet") send_to_address = '2NGZrVvZG92qGYqzTLjCAewvPZ7JE8S8VxE' - t = wallet3o5.sweep(send_to_address, min_confirms=0, offline=True) + t = wallet3o5.sweep(send_to_address, min_confirms=0, broadcast=False) print("Now send the raw transaction hex to one of the other cosigners to sign using sign_raw.py") print("Raw transaction: %s" % t.raw_hex()) else: diff --git a/examples/wallets_segwit_testnet.py b/examples/wallets_segwit_testnet.py index 25338ef3..dc8a24af 100644 --- a/examples/wallets_segwit_testnet.py +++ b/examples/wallets_segwit_testnet.py @@ -70,7 +70,7 @@ print("Balance to low, please deposit at least %s to %s" % (((tx_fee+tx_amount)*4)-w1.balance(), w1_key.address)) print("Sending transaction from wallet #1 to wallet #2:") - t = w1.send_to(w2_key.address, 4 * tx_amount, fee=tx_fee, offline=False) + t = w1.send_to(w2_key.address, 4 * tx_amount, fee=tx_fee, broadcast=True) t.info() while True: @@ -79,7 +79,7 @@ sleep(1) if w2.utxos(): print("Sending transaction from wallet #2 to wallet #3:") - t2 = w2.send_to(w3_key.address, 3 * tx_amount, fee=tx_fee, offline=False) + t2 = w2.send_to(w3_key.address, 3 * tx_amount, fee=tx_fee, broadcast=True) t2.info() break @@ -89,7 +89,7 @@ sleep(1) if w3.utxos(): print("Sending transaction from wallet #3 to wallet #4:") - t3 = w3.send_to(w4_key.address, 2 * tx_amount, fee=tx_fee, offline=False) + t3 = w3.send_to(w4_key.address, 2 * tx_amount, fee=tx_fee, broadcast=True) t3.sign(wif2) t3.send() t3.info() @@ -101,7 +101,7 @@ sleep(1) if w4.utxos(): print("Sending transaction from wallet #4 to wallet #1:") - t4 = w4.send_to(w1_key.address, tx_amount, fee=tx_fee, offline=False) + t4 = w4.send_to(w1_key.address, tx_amount, fee=tx_fee, broadcast=True) t4.sign(wif2) t4.send() t4.info() diff --git a/index.html b/index.html new file mode 100644 index 00000000..c6c7706a --- /dev/null +++ b/index.html @@ -0,0 +1,5574 @@ + + + + Het Financieele Dagblad + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + +
+
+
+ + +
+ +
+
+
Close sub menu
+
+
+ + + + +
+ +
+
+
+
+ +
+ + + + + + + + + +
+
+ +
+ + +
+
+
+
+
+
+ +
+ + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+
+ + + + + + + + + + + +
+
+
+
+ + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +
+ +
+
+
+
+

Dagoverzicht

+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+ + + +
+

Politiek

+ +

Financiële Markten

+ +
+ +
+
+
+ +
+
+
+
+ + + + +

Schakel browser notificaties in

+ +
+

Schakel browser notificaties in om meldingen te ontvangen op dit apparaat.

+ +
+ + +
+
+
+ + +

Browser notificaties inschakelen

+ +
+

U heeft browser notificaties voor fd.nl eerder geweigerd. Ga naar browser instellingen en sta browser notificaties toe voor fd.nl. De precieze handelingen zijn afhankelijk van uw browser.

+
+
+ + +

U volgt een onderwerp

+ +
+

Wilt u ook meldingen ontvangen voor dit onderwerp?

+ +
+ + +
+
+
+ + +

Meldingen

+ +
+

Wilt u ook meldingen ontvangen voor dit onderwerp?

+ +
+ + +
+ +
+ + + +
+
+
+ + + diff --git a/requirements.txt b/requirements.txt index 151bfeed..347edd35 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,10 +6,9 @@ SQLAlchemy>=2.0.0 numpy>=1.22.0 sphinx>=6.0.0 coveralls>=3.0.1 -psycopg2>=2.9.2 +psycopg>=3.0.0 mysql-connector-python>=8.0.27 mysqlclient>=2.1.0 -parameterized>=0.8.1 sphinx_rtd_theme>=1.0.0 Cython>=3.0.0 win-unicode-console;platform_system=="Windows" diff --git a/setup.cfg b/setup.cfg index 4681f4b3..95aa31e1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = bitcoinlib -version = 0.6.13 +version = 0.6.15 url = http://github.com/1200wd/bitcoinlib author = 1200wd author_email = info@1200wd.com @@ -47,10 +47,9 @@ dev = scrypt >= 0.8.18;platform_system!="Windows" sphinx >= 6.0.0 coveralls >= 3.0.1 - psycopg2 >= 2.9.2 + psycopg >= 3.0.0 mysql-connector-python >= 8.0.27 mysqlclient >= 2.1.0 - parameterized >= 0.8.1 sphinx_rtd_theme >= 1.0.0 Cython>=3.0.0 win-unicode-console;platform_system=="Windows" diff --git a/tests/bip38_protected_key_tests.json b/tests/bip38_protected_key_tests.json index 5032a7df..7de05202 100644 --- a/tests/bip38_protected_key_tests.json +++ b/tests/bip38_protected_key_tests.json @@ -26,8 +26,35 @@ "wif": "KwYgW8gcxj1JWJXhPSu4Fqwzfhp5Yfi42mdYmMa4XqK7NJxXUSK7", "address": "1HmPbwsvG5qJ3KJfxzsZRZWhbm1xBMuS8B", "description": "no EC multiply / compression #2" + }, + { + "passphrase": "TestingOneTwoThree", + "passphrase_code": "passphrasepxFy57B9v8HtUsszJYKReoNDV6VHjUSGt8EVJmux9n1J3Ltf1gRxyDGXqnf9qm", + "bip38": "6PfQu77ygVyJLZjfvMLyhLMQbYnu5uguoJJ4kMCLqWwPEdfpwANVS76gTX", + "wif": "5K4caxezwjGCGfnoPTZ8tMcJBLB7Jvyjv4xxeacadhq8nLisLR2", + "address": "6PfQu77ygVyJLZjfvMLyhLMQbYnu5uguoJJ4kMCLqWwPEdfpwANVS76gTX", + "description": "EC multiply / no compression / no lot sequence numbers #1", + "test_encrypt": false + }, + { + "passphrase": "Satoshi", + "passphrase_code": "passphraseoRDGAXTWzbp72eVbtUDdn1rwpgPUGjNZEc6CGBo8i5EC1FPW8wcnLdq4ThKzAS", + "bip38": "6PfLGnQs6VZnrNpmVKfjotbnQuaJK4KZoPFrAjx1JMJUa1Ft8gnf5WxfKd", + "wif": "5KJ51SgxWaAYR13zd9ReMhJpwrcX47xTJh2D3fGPG9CM8vkv5sH", + "address": "1CqzrtZC6mXSAhoxtFwVjz8LtwLJjDYU3V", + "description": "EC multiply / no compression / no lot sequence numbers #2", + "test_encrypt": false + }, + { + "passphrase": "MOLON LABE", + "passphrase_code": "passphraseaB8feaLQDENqCgr4gKZpmf4VoaT6qdjJNJiv7fsKvjqavcJxvuR1hy25aTu5sX", + "bip38": "6PgNBNNzDkKdhkT6uJntUXwwzQV8Rr2tZcbkDcuC9DZRsS6AtHts4Ypo1j", + "wif": "5JLdxTtcTHcfYcmJsNVy1v2PMDx432JPoYcBTVVRHpPaxUrdtf8", + "address": "1Jscj8ALrYu2y9TD8NrpvDBugPedmbj4Yh", + "description": "EC multiply / no compression / lot sequence numbers #1", + "test_encrypt": false } - ], +], "invalid": { "decrypt": [], "encrypt": [], diff --git a/tests/bitcoinlib_encrypted.db b/tests/bitcoinlib_encrypted.db new file mode 100644 index 00000000..c8da5534 Binary files /dev/null and b/tests/bitcoinlib_encrypted.db differ diff --git a/bitcoinlib/data/config.ini.unittest b/tests/config.ini.unittest similarity index 100% rename from bitcoinlib/data/config.ini.unittest rename to tests/config.ini.unittest diff --git a/bitcoinlib/data/config_encryption.ini.unittest b/tests/config_encryption.ini.unittest similarity index 100% rename from bitcoinlib/data/config_encryption.ini.unittest rename to tests/config_encryption.ini.unittest diff --git a/tests/db_0_5.py b/tests/db_0_5.py deleted file mode 100644 index d3fba8d2..00000000 --- a/tests/db_0_5.py +++ /dev/null @@ -1,445 +0,0 @@ -# -*- coding: utf-8 -*- -# -# BitcoinLib - Python Cryptocurrency Library -# DataBase - SqlAlchemy database definitions -# © 2016 - 2020 February - 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 -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# - -from sqlalchemy import create_engine -from sqlalchemy import (Column, Integer, BigInteger, UniqueConstraint, CheckConstraint, String, Boolean, Sequence, - ForeignKey, DateTime, LargeBinary) -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.ext.compiler import compiles -from sqlalchemy.orm import sessionmaker, relationship, close_all_sessions -from urllib.parse import urlparse -from bitcoinlib.main import * - -_logger = logging.getLogger(__name__) -Base = declarative_base() - - -@compiles(LargeBinary, "mysql") -def compile_largebinary_mysql(type_, compiler, **kwargs): - length = type_.length - element = "BLOB" if not length else "VARBINARY(%d)" % length - return element - - -class Db: - """ - Bitcoinlib Database object used by Service() and HDWallet() class. Initialize database and open session when - creating database object. - - Create new database if is doesn't exist yet - - """ - def __init__(self, db_uri=None): - if db_uri is None: - db_uri = DEFAULT_DATABASE - self.o = urlparse(db_uri) - if not self.o.scheme or \ - len(self.o.scheme) < 2: # Dirty hack to avoid issues with urlparse on Windows confusing drive with scheme - db_uri = 'sqlite:///%s' % db_uri - if db_uri.startswith("sqlite://") and ALLOW_DATABASE_THREADS: - db_uri += "&" if "?" in db_uri else "?" - db_uri += "check_same_thread=False" - if self.o.scheme == 'mysql': - db_uri += "&" if "?" in db_uri else "?" - db_uri += 'binary_prefix=true' - self.engine = create_engine(db_uri, isolation_level='READ UNCOMMITTED') - - Session = sessionmaker(bind=self.engine) - Base.metadata.create_all(self.engine) - self._import_config_data(Session) - self.session = Session() - - _logger.info("Using Database %s" % db_uri) - self.db_uri = db_uri - - # VERIFY AND UPDATE DATABASE - # Just a very simple database update script, without any external libraries for now - # - version_db = self.session.query(DbConfig.value).filter_by(variable='version').scalar() - if version_db[:3] == '0.4' and BITCOINLIB_VERSION[:3] == '0.5': - raise ValueError("Old database version found (<0.4.19). Cannot to 0.5 version database automatically, " - "use db_update tool to update") - try: - if BITCOINLIB_VERSION != version_db: - _logger.warning("BitcoinLib database (%s) is from different version then library code (%s). " - "Let's try to update database." % (version_db, BITCOINLIB_VERSION)) - db_update(self, version_db, BITCOINLIB_VERSION) - - except Exception as e: - _logger.warning("Error when verifying version or updating database: %s" % e) - - def drop_db(self, yes_i_am_sure=False): - if yes_i_am_sure: - self.session.commit() - self.session.close_all() - close_all_sessions() - Base.metadata.drop_all(self.engine) - - @staticmethod - def _import_config_data(ses): - session = ses() - installation_date = session.query(DbConfig.value).filter_by(variable='installation_date').scalar() - if not installation_date: - session.merge(DbConfig(variable='version', value=BITCOINLIB_VERSION)) - session.merge(DbConfig(variable='installation_date', value=str(datetime.now()))) - url = '' - try: - url = str(session.bind.url) - except Exception: - pass - session.merge(DbConfig(variable='installation_url', value=url)) - session.commit() - session.close() - - -def add_column(engine, table_name, column): - """ - Used to add new column to database with migration and update scripts - - :param engine: - :param table_name: - :param column: - :return: - """ - column_name = column.compile(dialect=engine.dialect) - column_type = column.type.compile(engine.dialect) - engine.execute("ALTER TABLE %s ADD COLUMN %s %s" % (table_name, column_name, column_type)) - - -class DbConfig(Base): - """ - BitcoinLib configuration variables - - """ - __tablename__ = 'config' - variable = Column(String(30), primary_key=True) - value = Column(String(255)) - - -class DbWallet(Base): - """ - Database definitions for wallets in Sqlalchemy format - - Contains one or more keys. - - """ - __tablename__ = 'wallets' - id = Column(Integer, Sequence('wallet_id_seq'), primary_key=True, doc="Unique wallet ID") - name = Column(String(80), unique=True, doc="Unique wallet name") - owner = Column(String(50), doc="Wallet owner") - network_name = Column(String(20), ForeignKey('networks.name'), doc="Name of network, i.e.: bitcoin, litecoin") - network = relationship("DbNetwork", doc="Link to DbNetwork object") - purpose = Column(Integer, - doc="Wallet purpose ID. BIP-44 purpose field, indicating which key-scheme is used default is 44") - scheme = Column(String(25), doc="Key structure type, can be BIP-32 or single") - witness_type = Column(String(20), default='legacy', - doc="Wallet witness type. Can be 'legacy', 'segwit' or 'p2sh-segwit'. Default is legacy.") - encoding = Column(String(15), default='base58', - doc="Default encoding to use for address generation, i.e. base58 or bech32. Default is base58.") - main_key_id = Column(Integer, - doc="Masterkey ID for this wallet. All other keys are derived from the masterkey in a " - "HD wallet bip32 wallet") - keys = relationship("DbKey", back_populates="wallet", doc="Link to keys (DbKeys objects) in this wallet") - transactions = relationship("DbTransaction", back_populates="wallet", - doc="Link to transaction (DbTransactions) in this wallet") - multisig_n_required = Column(Integer, default=1, doc="Number of required signature for multisig, " - "only used for multisignature master key") - sort_keys = Column(Boolean, default=False, doc="Sort keys in multisig wallet") - parent_id = Column(Integer, ForeignKey('wallets.id'), doc="Wallet ID of parent wallet, used in multisig wallets") - children = relationship("DbWallet", lazy="joined", join_depth=2, - doc="Wallet IDs of children wallets, used in multisig wallets") - multisig = Column(Boolean, default=True, doc="Indicates if wallet is a multisig wallet. Default is True") - cosigner_id = Column(Integer, - doc="ID of cosigner of this wallet. Used in multisig wallets to differentiate between " - "different wallets") - key_path = Column(String(100), - doc="Key path structure used in this wallet. Key path for multisig wallet, use to create " - "your own non-standard key path. Key path must follow the following rules: " - "* Path start with masterkey (m) and end with change / address_index " - "* If accounts are used, the account level must be 3. I.e.: m/purpose/coin_type/account/ " - "* All keys must be hardened, except for change, address_index or cosigner_id " - " Max length of path is 8 levels") - default_account_id = Column(Integer, doc="ID of default account for this wallet if multiple accounts are used") - - __table_args__ = ( - CheckConstraint(scheme.in_(['single', 'bip32']), name='constraint_allowed_schemes'), - CheckConstraint(encoding.in_(['base58', 'bech32']), name='constraint_default_address_encodings_allowed'), - CheckConstraint(witness_type.in_(['legacy', 'segwit', 'p2sh-segwit']), name='wallet_constraint_allowed_types'), - ) - - def __repr__(self): - return "" % (self.name, self.network_name) - - -class DbKeyMultisigChildren(Base): - """ - Use many-to-many relationship for multisig keys. A multisig keys contains 2 or more child keys - and a child key can be used in more then one multisig key. - - """ - __tablename__ = 'key_multisig_children' - - parent_id = Column(Integer, ForeignKey('keys.id'), primary_key=True) - child_id = Column(Integer, ForeignKey('keys.id'), primary_key=True) - key_order = Column(Integer, Sequence('key_multisig_children_id_seq')) - - -class DbKey(Base): - """ - Database definitions for keys in Sqlalchemy format - - Part of a wallet, and used by transactions - - """ - __tablename__ = 'keys' - id = Column(Integer, Sequence('key_id_seq'), primary_key=True, doc="Unique Key ID") - parent_id = Column(Integer, Sequence('parent_id_seq'), doc="Parent Key ID. Used in HD wallets") - name = Column(String(80), index=True, doc="Key name string") - account_id = Column(Integer, index=True, doc="ID of account if key is part of a HD structure") - depth = Column(Integer, - doc="Depth of key if it is part of a HD structure. Depth=0 means masterkey, " - "depth=1 are the masterkeys children.") - change = Column(Integer, doc="Change or normal address: Normal=0, Change=1") - address_index = Column(BigInteger, doc="Index of address in HD key structure address level") - public = Column(LargeBinary(128), index=True, doc="Bytes representation of public key") - private = Column(LargeBinary(128), index=True, doc="Bytes representation of private key") - wif = Column(String(255), index=True, doc="Public or private WIF (Wallet Import Format) representation") - compressed = Column(Boolean, default=True, doc="Is key compressed or not. Default is True") - key_type = Column(String(10), default='bip32', doc="Type of key: single, bip32 or multisig. Default is bip32") - address = Column(String(255), index=True, - doc="Address representation of key. An cryptocurrency address is a hash of the public key") - cosigner_id = Column(Integer, doc="ID of cosigner, used if key is part of HD Wallet") - encoding = Column(String(15), default='base58', doc='Encoding used to represent address: base58 or bech32') - purpose = Column(Integer, default=44, doc="Purpose ID, default is 44") - is_private = Column(Boolean, doc="Is key private or not?") - path = Column(String(100), doc="String of BIP-32 key path") - wallet_id = Column(Integer, ForeignKey('wallets.id'), index=True, doc="Wallet ID which contains this key") - wallet = relationship("DbWallet", back_populates="keys", doc="Related HDWallet object") - transaction_inputs = relationship("DbTransactionInput", cascade="all,delete", back_populates="key", - doc="All DbTransactionInput objects this key is part of") - transaction_outputs = relationship("DbTransactionOutput", cascade="all,delete", back_populates="key", - doc="All DbTransactionOutput objects this key is part of") - balance = Column(BigInteger, default=0, doc="Total balance of UTXO's linked to this key") - used = Column(Boolean, default=False, doc="Has key already been used on the blockchain in as input or output? " - "Default is False") - network_name = Column(String(20), ForeignKey('networks.name'), - doc="Name of key network, i.e. bitcoin, litecoin, dash") - latest_txid = Column(LargeBinary(32), doc="TxId of latest transaction downloaded from the blockchain") - network = relationship("DbNetwork", doc="DbNetwork object for this key") - multisig_parents = relationship("DbKeyMultisigChildren", backref='child_key', - primaryjoin=id == DbKeyMultisigChildren.child_id, - doc="List of parent keys") - multisig_children = relationship("DbKeyMultisigChildren", backref='parent_key', - order_by="DbKeyMultisigChildren.key_order", - primaryjoin=id == DbKeyMultisigChildren.parent_id, - doc="List of children keys") - - __table_args__ = ( - CheckConstraint(key_type.in_(['single', 'bip32', 'multisig']), name='constraint_key_types_allowed'), - CheckConstraint(encoding.in_(['base58', 'bech32']), name='constraint_address_encodings_allowed'), - UniqueConstraint('wallet_id', 'public', name='constraint_wallet_pubkey_unique'), - UniqueConstraint('wallet_id', 'private', name='constraint_wallet_privkey_unique'), - UniqueConstraint('wallet_id', 'wif', name='constraint_wallet_wif_unique'), - UniqueConstraint('wallet_id', 'address', name='constraint_wallet_address_unique'), - ) - - def __repr__(self): - return "" % (self.id, self.name, self.wif) - - -class DbNetwork(Base): - """ - Database definitions for networks in Sqlalchemy format - - Most network settings and variables can be found outside the database in the libraries configurations settings. - Use the bitcoinlib/data/networks.json file to view and manage settings. - - """ - __tablename__ = 'networks' - name = Column(String(20), unique=True, primary_key=True, doc="Network name, i.e.: bitcoin, litecoin, dash") - description = Column(String(50)) - - def __repr__(self): - return "" % (self.name, self.description) - - -# class TransactionType(enum.Enum): -# """ -# Incoming or Outgoing transaction Enumeration -# """ -# incoming = 1 -# outgoing = 2 - - -class DbTransaction(Base): - """ - Database definitions for transactions in Sqlalchemy format - - Refers to 1 or more keys which can be part of a wallet - - """ - __tablename__ = 'transactions' - id = Column(Integer, Sequence('transaction_id_seq'), primary_key=True, - doc="Unique transaction index for internal usage") - txid = Column(LargeBinary(32), index=True, doc="Bytes representation of transaction ID") - wallet_id = Column(Integer, ForeignKey('wallets.id'), index=True, - doc="ID of wallet which contains this transaction") - account_id = Column(Integer, index=True, doc="ID of account") - wallet = relationship("DbWallet", back_populates="transactions", - doc="Link to HDWallet object which contains this transaction") - witness_type = Column(String(20), default='legacy', doc="Is this a legacy or segwit transaction?") - version = Column(BigInteger, default=1, - doc="Tranaction version. Default is 1 but some wallets use another version number") - locktime = Column(BigInteger, default=0, - doc="Transaction level locktime. Locks the transaction until a specified block " - "(value from 1 to 5 million) or until a certain time (Timestamp in seconds after 1-jan-1970)." - " Default value is 0 for transactions without locktime") - date = Column(DateTime, default=datetime.utcnow, - doc="Date when transaction was confirmed and included in a block. " - "Or when it was created when transaction is not send or confirmed") - coinbase = Column(Boolean, default=False, doc="Is True when this is a coinbase transaction, default is False") - confirmations = Column(Integer, default=0, - doc="Number of confirmation when this transaction is included in a block. " - "Default is 0: unconfirmed") - block_height = Column(Integer, index=True, doc="Number of block this transaction is included in") - size = Column(Integer, doc="Size of the raw transaction in bytes") - fee = Column(BigInteger, doc="Transaction fee") - inputs = relationship("DbTransactionInput", cascade="all,delete", - doc="List of all inputs as DbTransactionInput objects") - outputs = relationship("DbTransactionOutput", cascade="all,delete", - doc="List of all outputs as DbTransactionOutput objects") - status = Column(String(20), default='new', - doc="Current status of transaction, can be one of the following: new', " - "'unconfirmed', 'confirmed'. Default is 'new'") - is_complete = Column(Boolean, default=True, doc="Allow to store incomplete transactions, for instance if not all " - "inputs are known when retrieving UTXO's") - input_total = Column(BigInteger, default=0, - doc="Total value of the inputs of this transaction. Input total = Output total + fee. " - "Default is 0") - output_total = Column(BigInteger, default=0, - doc="Total value of the outputs of this transaction. Output total = Input total - fee") - network_name = Column(String(20), ForeignKey('networks.name'), doc="Blockchain network name of this transaction") - network = relationship("DbNetwork", doc="Link to DbNetwork object") - raw = Column(LargeBinary, - doc="Raw transaction hexadecimal string. Transaction is included in raw format on the blockchain") - verified = Column(Boolean, default=False, doc="Is transaction verified. Default is False") - - __table_args__ = ( - UniqueConstraint('wallet_id', 'txid', name='constraint_wallet_transaction_hash_unique'), - CheckConstraint(status.in_(['new', 'unconfirmed', 'confirmed']), - name='constraint_status_allowed'), - CheckConstraint(witness_type.in_(['legacy', 'segwit']), name='transaction_constraint_allowed_types'), - ) - - def __repr__(self): - return "" % (self.txid, self.confirmations) - - -class DbTransactionInput(Base): - """ - Transaction Input Table - - Relates to Transaction table and Key table - - """ - __tablename__ = 'transaction_inputs' - transaction_id = Column(Integer, ForeignKey('transactions.id'), primary_key=True, - doc="Input is part of transaction with this ID") - transaction = relationship("DbTransaction", back_populates='inputs', doc="Related DbTransaction object") - index_n = Column(Integer, primary_key=True, doc="Index number of transaction input") - key_id = Column(Integer, ForeignKey('keys.id'), index=True, doc="ID of key used in this input") - key = relationship("DbKey", back_populates="transaction_inputs", doc="Related DbKey object") - address = Column(String(255), - doc="Address string of input, used if no key is associated. " - "An cryptocurrency address is a hash of the public key or a redeemscript") - witness_type = Column(String(20), default='legacy', - doc="Type of transaction, can be legacy, segwit or p2sh-segwit. Default is legacy") - prev_txid = Column(LargeBinary(32), - doc="Transaction hash of previous transaction. Previous unspent outputs (UTXO) is spent " - "in this input") - output_n = Column(BigInteger, doc="Output_n of previous transaction output that is spent in this input") - script = Column(LargeBinary, doc="Unlocking script to unlock previous locked output") - script_type = Column(String(20), default='sig_pubkey', - doc="Unlocking script type. Can be 'coinbase', 'sig_pubkey', 'p2sh_multisig', 'signature', " - "'unknown', 'p2sh_p2wpkh' or 'p2sh_p2wsh'. Default is sig_pubkey") - sequence = Column(BigInteger, doc="Transaction sequence number. Used for timelock transaction inputs") - value = Column(BigInteger, default=0, doc="Value of transaction input") - double_spend = Column(Boolean, default=False, - doc="Indicates if a service provider tagged this transaction as double spend") - - __table_args__ = (CheckConstraint(script_type.in_(['', 'coinbase', 'sig_pubkey', 'p2sh_multisig', - 'signature', 'unknown', 'p2sh_p2wpkh', 'p2sh_p2wsh']), - name='transactioninput_constraint_script_types_allowed'), - CheckConstraint(witness_type.in_(['legacy', 'segwit', 'p2sh-segwit']), - name='transactioninput_constraint_allowed_types'), - UniqueConstraint('transaction_id', 'index_n', name='constraint_transaction_input_unique')) - - -class DbTransactionOutput(Base): - """ - Transaction Output Table - - Relates to Transaction and Key table - - When spent is False output is considered an UTXO - - """ - __tablename__ = 'transaction_outputs' - transaction_id = Column(Integer, ForeignKey('transactions.id'), primary_key=True, - doc="Transaction ID of parent transaction") - transaction = relationship("DbTransaction", back_populates='outputs', - doc="Link to transaction object") - output_n = Column(Integer, primary_key=True, doc="Sequence number of transaction output") - key_id = Column(Integer, ForeignKey('keys.id'), index=True, doc="ID of key used in this transaction output") - key = relationship("DbKey", back_populates="transaction_outputs", doc="List of DbKey object used in this output") - address = Column(String(255), - doc="Address string of output, used if no key is associated. " - "An cryptocurrency address is a hash of the public key or a redeemscript") - script = Column(LargeBinary, doc="Locking script which locks transaction output") - script_type = Column(String(20), default='p2pkh', - doc="Locking script type. Can be one of these values: 'p2pkh', 'multisig', 'p2sh', 'p2pk', " - "'nulldata', 'unknown', 'p2wpkh' or 'p2wsh'. Default is p2pkh") - value = Column(BigInteger, default=0, doc="Total transaction output value") - spent = Column(Boolean, default=False, doc="Indicated if output is already spent in another transaction") - spending_txid = Column(LargeBinary(32), doc="Transaction hash of input which spends this output") - spending_index_n = Column(Integer, doc="Index number of transaction input which spends this output") - - __table_args__ = (CheckConstraint(script_type.in_(['', 'p2pkh', 'multisig', 'p2sh', 'p2pk', 'nulldata', - 'unknown', 'p2wpkh', 'p2wsh']), - name='transactionoutput_constraint_script_types_allowed'), - UniqueConstraint('transaction_id', 'output_n', name='constraint_transaction_output_unique')) - - -def db_update_version_id(db, version): - _logger.info("Updated BitcoinLib database to version %s" % version) - db.session.query(DbConfig).filter(DbConfig.variable == 'version').update( - {DbConfig.value: version}) - db.session.commit() - return version - - -def db_update(db, version_db, code_version=BITCOINLIB_VERSION): - # Database changes from version 0.5+ - # - # Older databases cannnot be updated this way, use updatedb.py to copy keys and recreate database. - # - - version_db = db_update_version_id(db, code_version) - return version_db diff --git a/tests/import_test.tx b/tests/import_test.tx new file mode 100644 index 00000000..faad7bc2 --- /dev/null +++ b/tests/import_test.tx @@ -0,0 +1,70 @@ +{'block_hash': None, + 'block_height': None, + 'coinbase': False, + 'confirmations': None, + 'date': None, + 'fee': 12366, + 'fee_per_kb': 33333, + 'flag': None, + 'input_total': 100000000, + 'inputs': [{'address': '23K38iGiHEHFfHh7SSUd46RWGQNHwha9kAt', + 'compressed': True, + 'double_spend': False, + 'encoding': 'base58', + 'index_n': 0, + 'locktime_cltv': None, + 'locktime_csv': None, + 'output_n': 0, + 'prev_txid': '12821f8ac330e4eddb9f87ea29456b31ec300e232d2c63880f669a9b15e3741f', + 'public_hash': 'e2cf42c85bb53cff8d3b75bdabb31e2c8a00cb8a', + 'public_keys': ['0289d3f95b15f53c666a4b70391e9a7cf6c771f6177d745557750a4160929a932e', + '0331271d364803fd05e4a5b95acb2b0f200e9634dd75e95a577477762b8dacbcd3', + '03547034e1e807362c5edd66d6951381ac2bde926b5244d5ce9cb1a82a4240bc89'], + 'redeemscript': '52210289d3f95b15f53c666a4b70391e9a7cf6c771f6177d745557750a4160929a932e210331271d364803fd05e4a5b95acb2b0f200e9634dd75e95a577477762b8dacbcd32103547034e1e807362c5edd66d6951381ac2bde926b5244d5ce9cb1a82a4240bc8953ae', + 'script': '', + 'script_code': '', + 'script_type': 'p2sh_multisig', + 'sequence': 4294967295, + 'signatures': ['1862f7a0b7d161954431662fb63db86247cead6dfc6b6c8b9b1c2479297ad3b50a35dd0ec056a43da44d3d04e22181ea59ad9225d2fc4541424d464622a0a6f2'], + 'sigs_required': 2, + 'sort': True, + 'unlocking_script': '', + 'locking_script': '52210289d3f95b15f53c666a4b70391e9a7cf6c771f6177d745557750a4160929a932e210331271d364803fd05e4a5b95acb2b0f200e9634dd75e95a577477762b8dacbcd32103547034e1e807362c5edd66d6951381ac2bde926b5244d5ce9cb1a82a4240bc8953ae', + 'valid': None, + 'value': 100000000, + 'witness': '', + 'witness_type': 'legacy'}], + 'locktime': 0, + 'network': 'bitcoinlib_test', + 'output_total': 99987634, + 'outputs': [{'address': '23K38iGiHEHFfHh7SSUd46RWGQNHwha9kAt', + 'output_n': 0, + 'public_hash': 'e2cf42c85bb53cff8d3b75bdabb31e2c8a00cb8a', + 'public_key': '', + 'script': 'a914e2cf42c85bb53cff8d3b75bdabb31e2c8a00cb8a87', + 'script_type': 'p2sh', + 'spending_index_n': None, + 'spending_txid': None, + 'spent': False, + 'value': 50000000}, + {'address': '239M1DxQuxJcMHtYBdG6A81bfXQrrCNa2rr', + 'output_n': 1, + 'public_hash': '787f3d509665fd64ea9c7f2670ef9f60133290fe', + 'public_key': '', + 'script': 'a914787f3d509665fd64ea9c7f2670ef9f60133290fe87', + 'script_type': 'p2sh', + 'spending_index_n': None, + 'spending_txid': None, + 'spent': False, + 'value': 49987634}], + 'raw': '01000000011f74e3159b9a660f88632c2d230e30ec316b4529ea879fdbede430c38a1f82120000000000ffffffff0280f0fa020000000017a914e2cf42c85bb53cff8d3b75bdabb31e2c8a00cb8a8732c0fa020000000017a914787f3d509665fd64ea9c7f2670ef9f60133290fe8700000000', + 'size': 371, + 'status': 'new', + 'txhash': '', + 'txid': '2e07be62d933f5b257ac066b874df651cd6e6763795c24036904024a2b44180b', + 'verified': False, + 'version': 1, + 'vsize': 371, + 'witness_type': 'legacy' +} + diff --git a/tests/test_blocks.py b/tests/test_blocks.py index 92d5bad3..e4d0ecb3 100644 --- a/tests/test_blocks.py +++ b/tests/test_blocks.py @@ -150,6 +150,7 @@ def test_blocks_parse_block_and_transactions_2(self): self.assertEqual(b.tx_count, 81) self.assertEqual(b.transactions[0].txid, 'dfd63430f8d14f6545117d74b20da63efd4a75c7e28f723b3dead431b88469ee') self.assertEqual(b.transactions[4].txid, '717bc8b42f12baf771b6719c2e3b2742925fe3912917c716abef03e35fe49020') + self.assertEqual(b.transactions[4].index, 4) self.assertEqual(len(b.transactions), 5) b.parse_transactions(70) self.assertEqual(len(b.transactions), 75) @@ -207,5 +208,6 @@ def test_block_parse_transaction_dict(self): self.assertEqual(2668, len(b.transactions)) i = 0 for tx in tx_dict: + self.assertEqual(tx['index'], i) assert(tx['txid'].hex() == b.transactions[i].txid) i += 1 diff --git a/tests/test_db.py b/tests/test_db.py index c4b23d83..fa6f6bbf 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -19,58 +19,97 @@ # import unittest -from sqlalchemy.exc import OperationalError -from tests.db_0_5 import Db as DbInitOld from bitcoinlib.db import * from bitcoinlib.db_cache import * -from bitcoinlib.wallets import Wallet, WalletError +from bitcoinlib.wallets import Wallet, WalletError, WalletTransaction +from bitcoinlib.transactions import Input, Output from bitcoinlib.services.services import Service +try: + import mysql.connector + import psycopg + from psycopg import sql +except ImportError as e: + print("Could not import all modules. Error: %s" % e) -DATABASEFILE_UNITTESTS = os.path.join(str(BCL_DATABASE_DIR), 'bitcoinlib.unittest.sqlite') -DATABASEFILE_TMP = os.path.join(str(BCL_DATABASE_DIR), 'bitcoinlib.tmp.sqlite') -DATABASEFILE_CACHE_TMP = os.path.join(str(BCL_DATABASE_DIR), 'bitcoinlib_cache.tmp.sqlite') +DATABASE_NAME = 'bitcoinlib_tmp' +DATABASE_CACHE_NAME = 'bitcoinlib_cache_tmp' +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 class TestDb(unittest.TestCase): @classmethod def setUpClass(cls): - if os.path.isfile(DATABASEFILE_TMP): - os.remove(DATABASEFILE_TMP) - if os.path.isfile(DATABASEFILE_CACHE_TMP): - os.remove(DATABASEFILE_CACHE_TMP) - - def test_database_upgrade(self): - if os.path.isfile(DATABASEFILE_UNITTESTS): - os.remove(DATABASEFILE_UNITTESTS) - dbold = DbInitOld(DATABASEFILE_UNITTESTS) - - # self.assertFalse('latest_txid' in dbold.engine.execute("SELECT * FROM keys").keys()) - # self.assertFalse('address' in dbold.engine.execute("SELECT * FROM transaction_inputs").keys()) - # version_db = dbold.session.query(DbConfig.value).filter_by(variable='version').scalar() - # self.assertEqual(version_db, '0.4.10') + cls.database_uri = database_init(DATABASE_NAME) + cls.database_cache_uri = database_init(DATABASE_CACHE_NAME) def test_database_create_drop(self): - dbtmp = Db(DATABASEFILE_TMP) - Wallet.create("tmpwallet", db_uri=DATABASEFILE_TMP) + dbtmp = Db(self.database_uri) + Wallet.create("tmpwallet", db_uri=self.database_uri) self.assertRaisesRegex(WalletError, "Wallet with name 'tmpwallet' already exists", - Wallet.create, 'tmpwallet', db_uri=DATABASEFILE_TMP) + Wallet.create, 'tmpwallet', db_uri=self.database_uri) dbtmp.drop_db(yes_i_am_sure=True) - Wallet.create("tmpwallet", db_uri=DATABASEFILE_TMP) + Wallet.create("tmpwallet", db_uri=self.database_uri) def test_database_cache_create_drop(self): - dbtmp = DbCache(DATABASEFILE_CACHE_TMP) - srv = Service(cache_uri=DATABASEFILE_CACHE_TMP, exclude_providers=['bitaps', 'bitgo']) + if os.getenv('UNITTEST_DATABASE') == 'mysql': + self.skipTest('MySQL does not allow indexing on LargeBinary fields, so caching is not possible') + dbtmp = DbCache(self.database_cache_uri) + srv = Service(cache_uri=self.database_cache_uri, exclude_providers=['bitaps', 'bitgo']) t = srv.gettransaction('68104dbd6819375e7bdf96562f89290b41598df7b002089ecdd3c8d999025b13') if t: self.assertGreaterEqual(srv.results_cache_n, 0) srv.gettransaction('68104dbd6819375e7bdf96562f89290b41598df7b002089ecdd3c8d999025b13') self.assertGreaterEqual(srv.results_cache_n, 1) dbtmp.drop_db() - self.assertRaisesRegex(OperationalError, "", srv.gettransaction, + self.assertRaisesRegex(Exception, "", srv.gettransaction, '68104dbd6819375e7bdf96562f89290b41598df7b002089ecdd3c8d999025b13') + def test_database_transaction_integers(self): + db = Db(self.database_uri) + w = Wallet.create('StrangeTransactions', account_id=0x7fffffff, db_uri=db.db_uri) + inp = Input('68104dbd6819375e7bdf96562f89290b41598df7b002089ecdd3c8d999025b13', 0x7fffffff, + value=0xffffffff, index_n=0x7fffffff, sequence=0xffffffff) + outp = Output(0xffffffff, '37jKPSmbEGwgfacCr2nayn1wTaqMAbA94Z', output_n=0xffffffff) + wt = WalletTransaction(w, 0x7fffffff, locktime=0xffffffff, fee=0xffffffff, confirmations=0x7fffffff, + input_total= 2100000000001000, block_height=0x7fffffff, version=0x7fffffff, + output_total=2100000000000000, size=0x07fffffff, inputs=[inp], outputs=[outp]) + self.assertTrue(wt.store()) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_encoding.py b/tests/test_encoding.py index 6a062914..4b24f170 100644 --- a/tests/test_encoding.py +++ b/tests/test_encoding.py @@ -266,11 +266,6 @@ def test_der_encode_sig(self): ["BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", "0014751e76e8199196d454941c45d1b3a323f1433bd6"], ["tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"], - # Invalid checksum according to new bech32m definition - # ["bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", - # "5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6"], - # ["BC1SW50QA3JX3S", "6002751e"], - # ["bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj", "5210751e76e8199196d454941c45d1b3a323"], ["tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", "0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"], ] @@ -324,8 +319,8 @@ def test_der_encode_sig(self): ('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v8n0nx0muaewav253zgeav', 'Invalid decoded data length, must be between 2 and 40'), ('BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P', 'Invalid decoded data length, must be 20 or 32 bytes'), - ('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v07qwwzcrf', "cannot convert 'NoneType' object to bytes"), - ('tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vpggkg4j', "cannot convert 'NoneType' object to bytes"), + ('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v07qwwzcrf', "Invalid padding bits"), + ('tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vpggkg4j', "Invalid padding bits"), ('bc1gmk9yu', 'Invalid checksum (Bech32 instead of Bech32m)'), ] @@ -374,10 +369,7 @@ def test_invalid_checksum(self): def test_valid_address(self): """Test whether valid addresses decode to the correct output.""" for (address, hexscript) in VALID_ADDRESS: - try: - scriptpubkey = addr_bech32_to_pubkeyhash(address, include_witver=True) - except EncodingError: - scriptpubkey = addr_bech32_to_pubkeyhash(address, prefix='tb', include_witver=True) + scriptpubkey = addr_bech32_to_pubkeyhash(address, include_witver=True) self.assertEqual(scriptpubkey, bytes.fromhex(hexscript)) addr = pubkeyhash_to_addr_bech32(scriptpubkey, address[:2].lower()) self.assertEqual(address.lower(), addr) @@ -401,26 +393,26 @@ def test_quantity_class(self): # Source: https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki def test_bech32m_valid(self): for addr, pubkeyhash in BECH32M_VALID: - assert(pubkeyhash == addr_bech32_to_pubkeyhash(addr, include_witver=True).hex()) + assert pubkeyhash == addr_bech32_to_pubkeyhash(addr, include_witver=True).hex() prefix = addr.split('1')[0].lower() witver = change_base(addr.split('1')[1][0], 'bech32', 10) checksum_xor = addr_bech32_checksum(addr) addrc = pubkeyhash_to_addr_bech32(pubkeyhash, prefix, witver, checksum_xor=checksum_xor) - assert(addr.lower() == addrc) + assert addr.lower() == addrc def test_bech32_invalid(self): for addr, err in BECH32M_INVALID: try: addr_bech32_to_pubkeyhash(addr) except (EncodingError, TypeError) as e: - assert (str(e) == err) + assert str(e) == err def test_bech32_invalid_pubkeyhash(self): for pubkeyhash, err in BECH32M_INVALID_PUBKEYHASH: try: pubkeyhash_to_addr_bech32(pubkeyhash) except (EncodingError, TypeError) as e: - assert (str(e) == err) + assert str(e) == err class TestEncodingConfig(unittest.TestCase): diff --git a/tests/test_keys.py b/tests/test_keys.py index 602eb992..1fc1fff5 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -2,7 +2,7 @@ # # BitcoinLib - Python Cryptocurrency Library # Unit Tests for Key, Encoding and Mnemonic Class -# © 2017-2018 July - 1200 Web Development +# © 2017-2024 March - 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 @@ -26,7 +26,7 @@ # Number of bulktests for generation of private, public keys and HDKeys. Set to 0 to disable # WARNING: Can be slow for a larger number of tests -BULKTESTCOUNT = 100 +BULKTESTCOUNT = 250 class TestKeyClasses(unittest.TestCase): @@ -43,12 +43,72 @@ def test_keys_classes_dunder_methods(self): pubk2 = HDKey(k.wif_public()) self.assertEqual(str(pubk2), '03dc86716b2be27a0575558bac73279290ac22c3ea0240e42a2152d584f2b4006b') self.assertTrue(k.public() == pubk2) - self.assertEqual(k + k2, b'\x03\xdc\x86qk+\xe2z\x05uU\x8b\xacs\'\x92\x90\xac"\xc3\xea\x02@\xe4*!R\xd5' - b'\x84\xf2\xb4\x00k\x03\xdc\x86qk+\xe2z\x05uU\x8b\xacs\'\x92\x90\xac"' - b'\xc3\xea\x02@\xe4*!R\xd5\x84\xf2\xb4\x00k') - self.assertEqual(k + k2, k.public_byte + k2.public_byte) self.assertEqual(hash(k), hash(k)) + secret_a = 91016841482436413813855602003356453732719866824300837492458390942862039054048 + secret_b = 78671675202523181504169507283123166972338313435344626818080535590471773062636 + secret_a_add_b = 53896427447643399894454124277791712852220615980570559927933763391815650622347 + secret_a_min_b = 12345166279913232309686094720233286760381553388956210674377855352390265991412 + ka = HDKey(secret_a) + ka2 = HDKey(secret_a) + kb = HDKey(secret_b) + self.assertEqual(str(ka), '02dff8866c7dc58055d9823dbc0ef098be76d8a1c87e545a13559460669b56a6a6') + self.assertEqual(len(ka), 33) + self.assertTrue(ka == ka2) + pub_ka = HDKey(ka.wif_public()) + self.assertEqual(str(pub_ka), '02dff8866c7dc58055d9823dbc0ef098be76d8a1c87e545a13559460669b56a6a6') + self.assertTrue(ka.public() == pub_ka) + self.assertEqual((ka + kb).secret, secret_a_add_b) + self.assertEqual((kb + ka).secret, secret_a_add_b) + self.assertEqual((ka - kb).secret, secret_a_min_b) + + def test_keys_classes_dunder_methods_mul(self): + secret_a = 101842203467542661703461476767681059717614296435193763347876672834253776929083 + secret_b = 48056918761728599432510813046582785545807011954742048381717688544631745412510 + secret_a_mul_b = 88863767166841201737805106153187292662619702602208852020796235484522800819015 + ka = HDKey(secret_a) + kb = HDKey(secret_b) + self.assertEqual((ka * kb).secret, secret_a_mul_b) + self.assertEqual((kb * ka).secret, secret_a_mul_b) + + def test_keys_proof_distributivity_of_scalar_operations(self): + # Proof: (a - b) * c == a * c - b * c over SECP256k1 + ka = HDKey() + kb = HDKey() + kc = HDKey() + self.assertTrue(((ka - kb) * kc) == ((ka * kc) - (kb * kc))) + + def test_keys_inverse(self): + secret = 95695802915573022935630358993164660366922511389187789518108651759801046161623 + inv_x = 18153291153288219155018628681705413538294494009875615719062204619491226452658 + inv_y = 67935514921393906349711087930011707333238709725906400058836382320969451605430 + k = Key(secret) + k_inv = -k + self.assertEqual(k_inv.x, inv_x) + self.assertEqual(k_inv.y, inv_y) + + def test_keys_inverse2(self): + k = HDKey() + pub_k = k.public() + self.assertEqual(k.address(), pub_k.address()) + self.assertEqual((-k).address(), pub_k.inverse().address()) + self.assertEqual((-k).address(), k.inverse().address()) + + pkwif = 'Mtpv7L6Q8tPadPv8iUDKAXk1wyCmdJ6q2y2d3AixyoGVMH3WeoCDwkLbpUBXXB5HHbueeqTikkeBGTBV7tCcgJtEfm1wCt4ZcQixz7TtV5CAXfd' + k = HDKey(pkwif, network='litecoin', compressed=False, witness_type='p2sh-segwit') + pub_k = k.public() + self.assertEqual(pub_k, pub_k.inverse()) + + k = HDKey(pkwif, network='litecoin', witness_type='p2sh-segwit') + pub_k = k.public() + pub_k_inv = pub_k.inverse() + self.assertEqual(pub_k_inv.address(), "MQVYsZ5o5uhN2X6QMbu9RVu5YADiq859MY") + self.assertEqual(pub_k_inv.witness_type, 'p2sh-segwit') + self.assertEqual(pub_k_inv.network.name, 'litecoin') + self.assertEqual(k.address(), pub_k.address()) + self.assertEqual((-k).address(), pub_k_inv.address()) + self.assertEqual((-k).address(), k.inverse().address()) + def test_dict_and_json_outputs(self): k = HDKey() k.address(script_type='p2wsh', encoding='bech32') @@ -63,9 +123,9 @@ def test_dict_and_json_outputs(self): self.assertTrue(isinstance(k.as_dict(include_private=True), dict)) def test_path_expand(self): - self.assertListEqual(path_expand([0]), ['m', "44'", "0'", "0'", '0', '0']) - self.assertListEqual(path_expand([10, 20]), ['m', "44'", "0'", "0'", '10', '20']) - self.assertListEqual(path_expand([10, 20], witness_type='segwit'), ['m', "84'", "0'", "0'", '10', '20']) + self.assertListEqual(path_expand([0], witness_type='legacy'), ['m', "44'", "0'", "0'", '0', '0']) + self.assertListEqual(path_expand([10, 20], witness_type='legacy'), ['m', "44'", "0'", "0'", '10', '20']) + self.assertListEqual(path_expand([10, 20]), ['m', "84'", "0'", "0'", '10', '20']) self.assertListEqual(path_expand([], witness_type='p2sh-segwit'), ['m', "49'", "0'", "0'", '0', '0']) self.assertListEqual(path_expand([99], witness_type='p2sh-segwit', multisig=True), ['m', "48'", "0'", "0'", "1'", '0', '99']) @@ -74,6 +134,21 @@ def test_path_expand(self): self.assertRaisesRegex(BKeyError, "Please provide path as list with at least 1 item", path_expand, 5) + def test_keys_create_public_point(self): + k = HDKey() + p = (k.x, k.y) + k2 = HDKey(p) + self.assertEqual(k, k2) + self.assertEqual(k.public(), k2) + self.assertEqual(k.address(), k2.address()) + + k = HDKey(compressed=False, witness_type='legacy') + p = (k.x, k.y) + k2 = HDKey(p, compressed=False, witness_type='legacy') + self.assertEqual(k, k2) + self.assertEqual(k.public(), k2) + self.assertEqual(k.address(), k2.address()) + class TestGetKeyFormat(unittest.TestCase): @@ -272,11 +347,11 @@ def test_public_key_address_uncompressed(self): class TestHDKeysImport(unittest.TestCase): def setUp(self): - self.k = HDKey.from_seed('000102030405060708090a0b0c0d0e0f') + self.k = HDKey.from_seed('000102030405060708090a0b0c0d0e0f', witness_type='legacy') self.k2 = HDKey.from_seed('fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a878' - '4817e7b7875726f6c696663605d5a5754514e4b484542') + '4817e7b7875726f6c696663605d5a5754514e4b484542', witness_type='legacy') self.xpub = HDKey('xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqse' - 'fD265TMg7usUDFdp6W1EGMcet8') + 'fD265TMg7usUDFdp6W1EGMcet8', witness_type='legacy') def test_hdkey_import_seed_1(self): @@ -291,9 +366,14 @@ def test_hdkey_import_seed_2(self): self.assertEqual('xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJ' 'Y47LJhkJ8UB7WEGuduB', self.k2.wif_public()) + def test_hdkey_random_legacy(self): + self.k = HDKey(witness_type='legacy') + self.assertEqual('xprv', self.k.wif(is_private=True)[:4]) + self.assertEqual(111, len(self.k.wif(is_private=True))) + def test_hdkey_random(self): self.k = HDKey() - self.assertEqual('xprv', self.k.wif(is_private=True)[:4]) + self.assertEqual('zprv', self.k.wif(is_private=True)[:4]) self.assertEqual(111, len(self.k.wif(is_private=True))) def test_hdkey_import_extended_private_key(self): @@ -309,14 +389,14 @@ def test_hdkey_import_extended_public_key(self): self.assertEqual(extkey, self.k.wif()) def test_hdkey_import_simple_key(self): - self.k = HDKey('L45TpiVN3C8Q3MoosGDzug1acpnFjysseBLVboszztmEyotdSJ9g') + self.k = HDKey('L45TpiVN3C8Q3MoosGDzug1acpnFjysseBLVboszztmEyotdSJ9g', witness_type='legacy') self.assertEqual( 'xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAbeoRRpMHE67jGmBQKCr2YovK2G23x5uzaztRbEW9pc' 'j6SqMFd', self.k.wif(is_private=True)) def test_hdkey_import_bip38_key(self): if USING_MODULE_SCRYPT: - self.k = HDKey('6PYNKZ1EAgYgmQfmNVamxyXVWHzK5s6DGhwP4J5o44cvXdoY7sRzhtpUeo', + self.k = HDKey('6PYNKZ1EAgYgmQfmNVamxyXVWHzK5s6DGhwP4J5o44cvXdoY7sRzhtpUeo', witness_type='legacy', password='TestingOneTwoThree') self.assertEqual('L44B5gGEpqEDRS9vVPz7QT35jcBG2r3CZwSwQ4fCewXAhAhqGVpP', self.k.wif_key()) @@ -352,36 +432,40 @@ def test_hdkey_import_segwit_wifs(self): def test_hdkey_import_from_private_byte(self): keystr = b"fch\xe4w\xa8\xdd\xd4h\x08\xc5'\xcc %s" % (v[0], phrase)) self.assertEqual(v[1], phrase) self.assertEqual(v[2], seed) - k = HDKey.from_seed(seed) + k = HDKey.from_seed(seed, witness_type='legacy') self.assertEqual(k.wif(is_private=True), v[3]) # From Copyright (c) 2013 Pavol Rusnak diff --git a/tests/test_networks.py b/tests/test_networks.py index 31be4f1c..d2c385fb 100644 --- a/tests/test_networks.py +++ b/tests/test_networks.py @@ -25,22 +25,10 @@ def test_networks_prefix_hdkey_wif(self): network = Network('bitcoin') self.assertEqual(network.wif_prefix(is_private=True), b'\x04\x88\xad\xe4') self.assertEqual(network.wif_prefix(is_private=False), b'\x04\x88\xb2\x1e') - self.assertRaisesRegex(NetworkError, "WIF Prefix for script type p2wpkh not found", Network('dash').wif_prefix, - witness_type='segwit') - - def test_networks_print_value(self): - network = Network('dash') - self.assertEqual(network.print_value(10000), '0.00010000 DASH') - - self.assertEqual(print_value(123, rep='symbol', denominator=0.001), '0.00123 m₿') - self.assertEqual(print_value(123, denominator=1e-6), '1.23 µBTC') - self.assertEqual(print_value(1e+14, network='dogecoin', denominator=1e+6, decimals=0), '1 MDOGE') - self.assertEqual(print_value(1200, denominator=1e-8, rep='Satoshi'), '1200 Satoshi') - self.assertEqual(print_value(1200, denominator=1e-6, rep='none'), '12.00') def test_networks_network_value_for(self): prefixes = network_values_for('prefix_wif') - expected_prefixes = [b'\xb0', b'\xef', b'\x99', b'\x80', b'\xcc'] + expected_prefixes = [b'\xb0', b'\xef', b'\x99', b'\x80'] for expected in expected_prefixes: self.assertIn(expected, prefixes) self.assertEqual(network_values_for('denominator')[0], 1e-8) @@ -69,7 +57,7 @@ def test_network_dunders(self): self.assertFalse(n1 == n2) self.assertTrue(n1 == 'bitcoin') self.assertFalse(n2 == 'bitcoin') - self.assertTrue(n1 != 'dash') + self.assertTrue(n1 != 'dogecoin') self.assertEqual(str(n1), "") self.assertTrue(hash(n1)) diff --git a/tests/test_script.py b/tests/test_script.py index fe6ad70c..aac056ca 100644 --- a/tests/test_script.py +++ b/tests/test_script.py @@ -464,23 +464,22 @@ def test_op_nops(self): for n in [None, 1] + list(range(4, 11)): self.assertTrue(getattr(Stack(), 'op_nop%s' % (str(n) if n else ''))()) - # TODO: Add - # def test_op_checklocktimeverify(self): - # cur_timestamp = int(datetime.now().timestamp()) - # st = Stack([encode_num(500)]) - # self.assertTrue(st.op_checklocktimeverify(tx_locktime=1000, sequence=1)) - # self.assertFalse(st.op_checklocktimeverify(tx_locktime=1000, sequence=0xffffffff)) - # self.assertFalse(st.op_checklocktimeverify(tx_locktime=499, sequence=1)) - # self.assertTrue(st.op_checklocktimeverify(tx_locktime=500, sequence=1)) - # self.assertFalse(st.op_checklocktimeverify(tx_locktime=cur_timestamp, sequence=1)) - # - # st = Stack([encode_num(cur_timestamp-100)]) - # self.assertTrue(st.op_checklocktimeverify(sequence=0xfffffffe, tx_locktime=cur_timestamp)) - # self.assertFalse(st.op_checklocktimeverify(sequence=0xfffffffe, tx_locktime=660600)) - # - # cur_timestamp = int(datetime.now().timestamp()) - # st = Stack([encode_num(cur_timestamp+100)]) - # self.assertFalse(st.op_checklocktimeverify(sequence=0xfffffffe, tx_locktime=cur_timestamp)) + def test_op_checklocktimeverify(self): + cur_timestamp = int(datetime.now().timestamp()) + st = Stack([encode_num(500)]) + self.assertTrue(st.op_checklocktimeverify(tx_locktime=1000, sequence=1)) + self.assertFalse(st.op_checklocktimeverify(tx_locktime=1000, sequence=0xffffffff)) + self.assertFalse(st.op_checklocktimeverify(tx_locktime=499, sequence=1)) + self.assertTrue(st.op_checklocktimeverify(tx_locktime=500, sequence=1)) + self.assertFalse(st.op_checklocktimeverify(tx_locktime=cur_timestamp, sequence=1)) + + st = Stack([encode_num(cur_timestamp-100)]) + self.assertTrue(st.op_checklocktimeverify(sequence=0xfffffffe, tx_locktime=cur_timestamp)) + self.assertFalse(st.op_checklocktimeverify(sequence=0xfffffffe, tx_locktime=660600)) + + cur_timestamp = int(datetime.now().timestamp()) + st = Stack([encode_num(cur_timestamp+100)]) + self.assertFalse(st.op_checklocktimeverify(sequence=0xfffffffe, tx_locktime=cur_timestamp)) # TODO: Add # def test_op_checksequenceverify(self): @@ -560,7 +559,7 @@ def test_script_multisig_errors(self): def test_script_type_empty_unknown(self): s = Script.parse(b'') self.assertEqual(s.commands, []) - self.assertEqual(s.raw, b'') + self.assertEqual(s.as_bytes(), b'') def test_script_deserialize_sig_pk(self): scr = '493046022100cf4d7571dd47a4d47f5cb767d54d6702530a3555726b27b6ac56117f5e7808fe0221008cbb42233bb04d7f28a' \ @@ -625,10 +624,10 @@ def test_script_verify_transaction_input_p2sh_multisig(self): script = unlock_script + lock_script s = Script.parse_bytes(script) - self.assertEqual(s.blueprint, [0, 'signature', 'signature', 82, 'key', 'key', 'key', 83, 174, 169, + self.assertEqual(s.blueprint, [0, 'signature', 'signature', [82, 'key', 'key', 'key', 83, 174], 169, 'data-20', 135]) self.assertEqual(s.script_types, ['p2sh_multisig', 'p2sh']) - self.assertEqual(str(s), "OP_0 signature signature OP_2 key key key OP_3 OP_CHECKMULTISIG OP_HASH160 " + self.assertEqual(str(s), "OP_0 signature signature redeemscript OP_HASH160 " "data-20 OP_EQUAL") transaction_hash = bytes.fromhex('5a805853bf82bcdd865deb09c73ccdd61d2331ac19d8c2911f17c7d954aec059') self.assertTrue(s.evaluate(message=transaction_hash)) @@ -674,13 +673,12 @@ def test_script_verify_transaction_input_p2sh_multisig_huge(self): script = unlock_script + lock_script s = Script.parse_bytes(script) self.assertEqual(s.blueprint, [0, 'signature', 'signature', 'signature', 'signature', 'signature', - 'signature', 'signature', 'signature', 88, 'key', 'key', 'key', 'key', + 'signature', 'signature', 'signature', [88, 'key', 'key', 'key', 'key', 'key', 'key', 'key', 'key', 'key', 'key', 'key', 'key', 'key', 'key', 'key', - 95, 174, 169, 'data-20', 135]) + 95, 174], 169, 'data-20', 135]) self.assertEqual(s.script_types, ['p2sh_multisig', 'p2sh']) self.assertEqual(str(s), "OP_0 signature signature signature signature signature signature signature " - "signature OP_8 key key key key key key key key key key key key key key key OP_15 " - "OP_CHECKMULTISIG OP_HASH160 data-20 OP_EQUAL") + "signature redeemscript OP_HASH160 data-20 OP_EQUAL") transaction_hash = bytes.fromhex('8d190df3d02369999cad3eb222ac18b3315ff2bdc449b8fb30eb14db45730fe3') self.assertEqual(s.redeemscript, redeemscript) self.assertTrue(s.evaluate(message=transaction_hash)) @@ -724,11 +722,17 @@ def test_script_verify_transaction_input_p2wsh(self): self.assertEqual(str(s), "signature signature OP_2 key key key OP_3 OP_CHECKMULTISIG OP_SHA256 data-32 OP_EQUAL") transaction_hash = bytes.fromhex('43f0f6dfb58acc8ed05f5afc224c2f6c50523230bfcba5e5fd91d345e8a159ab') data = {'redeemscript': redeemscript} - self.assertTrue(s.evaluate(message=transaction_hash, tx_data=data)) + self.assertTrue(s.evaluate(message=transaction_hash, env_data=data)) def test_script_verify_transaction_input_p2pk(self): - pass - # TODO + p2pk_lockscript = '210312ed54eee6c84b440dd90623a714360196bebd842bfa64c7c7767b71b92a238dac' # key + checksig + p2pk_unlockscript = \ + ('463043021f52f02788988b941e3b810357762ccea5148e405edf124ea6b3b7eb9eba15430220609a9261612aaaa7544b7dae34' + '7b5dc3e53b0fc304957d6c4a46e1ae90a5d30001') # signature + script = p2pk_unlockscript + p2pk_lockscript + s = Script.parse_hex(script) + transaction_hash = bytes.fromhex("67b94bf5a5c17a5f6b2bedbefc51a17db669ce7ff3bbbc4943cfd876d68df986") + self.assertTrue(s.evaluate(message=transaction_hash)) def test_script_verify_transaction_output_return(self): script = bytes.fromhex('6a26062c74e4b802d60ffdd1daa37b848e39a2b0ecb2de72c6ca24d71b87813b5e056cb7f1e8c8b0') @@ -755,12 +759,18 @@ def test_script_add(self): def test_script_create_simple(self): script = Script([op.op_2, op.op_5, op.op_sub, op.op_1]) self.assertEqual(str(script), 'OP_2 OP_5 OP_SUB OP_1') - self.assertEqual(repr(script), '') + self.assertEqual(repr(script), '') self.assertEqual(script.serialize().hex(), '52559451') self.assertEqual(script.serialize_list(), [b'R', b'U', b'\x94', b'Q']) self.assertTrue(script.evaluate()) self.assertEqual(script.stack, [b'\3']) + def test_script_calc_evaluate(self): + s = Script.parse('0101016293016387') + self.assertListEqual(s.blueprint, ['data-1', 'data-1', 147, 'data-1', 135]) + self.assertTrue(s.view(), '01 62 OP_ADD 63 OP_EQUAL') + self.assertTrue(s.evaluate()) + def test_script_serialize(self): # Serialize p2sh_p2wsh tx 77ad5a0f9447dbfb9adcdb9b2437e91780519ec8ee24a8eda91b25a0666205cb from sigs and keys sig1 = b'0E\x02!\x00\xde\x8fDH\xe2\xd2\xe7F\x18>B\xe4\xfd\x87\xb8\x0b\x87\xfb\xb1\xd7ZYL\xa4\x08\x12\xe5\x07v' \ @@ -773,7 +783,7 @@ def test_script_serialize(self): key3 = bytes.fromhex('0221b302fb92b25f171f1cd57bd22e60a1d2956f5831df17d94b3e9c3490aad598') redeemscript = Script([op.op_2, key1, key2, key3, op.op_3, op.op_checkmultisig]) script_hash = bytes.fromhex('b0fcc0caed77aeba9786f39920151162dfaf90e679aafab7a71e9b978e7d3f39') - self.assertEqual(redeemscript.raw.hex(), + self.assertEqual(redeemscript.as_hex(), '522102cd9107f8f1505ffd779bb7d8596ee686afc116e340f01b435871a038922255eb210297faa15d33e14e80c' 'a8a8616030b677941245fea12c4ef2ca28b14bd35ed42e1210221b302fb92b25f171f1cd57bd22e60a1d2956f58' '31df17d94b3e9c3490aad59853ae') @@ -783,7 +793,7 @@ def test_script_serialize(self): Script([op.op_sha256, script_hash, op.op_equal]) self.assertEqual(str(script), 'OP_0 signature signature OP_2 key key key OP_3 OP_CHECKMULTISIG OP_SHA256 ' 'data-32 OP_EQUAL') - self.assertTrue(script.evaluate(message=transaction_hash, tx_data={'redeemscript': redeemscript.serialize()})) + self.assertTrue(script.evaluate(message=transaction_hash, env_data={'redeemscript': redeemscript.serialize()})) self.assertEqual(script.stack, []) def test_script_deserialize_sig_pk2(self): @@ -804,7 +814,7 @@ def test_deserialize_script_with_sizebyte(self): script = b'\x00\x14y\t\x19r\x18lD\x9e\xb1\xde\xd2+x\xe4\r\x00\x9b\xdf\x00\x89' s1 = Script.parse(script_size_byte) s2 = Script.parse(script) - s1._raw = s2.raw + s1._raw = s2.as_bytes() self.assertDictEqualExt(s1.__dict__, s2.__dict__) def test_script_parse_redeemscript(self): @@ -830,7 +840,7 @@ def test_script_create_redeemscript(self): '00bd217870a8b4f1f09f3a8e8353ae' self.assertEqual(expected_redeemscript, redeemscript.serialize().hex()) - redeemscript3 = b'\x52' + b''.join([varstr(k) for k in keylist]) + b'\x53\xae' + redeemscript3 = b'\x52' + b''.join([varstr(k.public_byte) for k in keylist]) + b'\x53\xae' self.assertEqual(redeemscript3, redeemscript.serialize()) def test_script_create_redeemscript_2(self): @@ -873,7 +883,7 @@ def test_script_large_redeemscript_packing(self): redeemscript_size = '4dff01' + redeemscript s = Script.parse_hex(redeemscript_size) - self.assertEqual((str(s)), redeemscript_str) + self.assertEqual((str(s)), "redeemscript OP_15 OP_CHECKMULTISIG") redeemscript_error = '4d0101' + redeemscript self.assertRaisesRegex(ScriptError, "Malformed script, not enough data found", Script.parse_hex, @@ -883,6 +893,38 @@ def test_script_large_redeemscript_packing(self): self.assertRaisesRegex(ScriptError, "Malformed script, not enough data found", Script.parse_hex, redeemscript_error) + def test_script_view(self): + script = bytes.fromhex( + '483045022100ba2ec7c40257b3d22864c9558738eea4d8771ab97888368124e176fdd6d7cd8602200f47c8d0c437df1ea8f98' + '19d344e05b9c93e38e88df1fc46abb6194506c50ce1012103e481f20561573cfd800e64efda61405917cb29e4bd20bed168c5' + '2b674937f53576a914f9cc73824051cc82d64a716c836c54467a21e22c88ac') + s = Script.parse(script) + expected_str = ('3045022100ba2ec7c40257b3d22864c9558738eea4d8771ab97888368124e176fdd6d7cd8602200f47c8d0c437' + 'df1ea8f9819d344e05b9c93e38e88df1fc46abb6194506c50ce101 03e481f20561573cfd800e64efda6140591' + '7cb29e4bd20bed168c52b674937f535 OP_DUP OP_HASH160 f9cc73824051cc82d64a716c836c54467a21e22c' + ' OP_EQUALVERIFY OP_CHECKSIG') + self.assertEqual(s.view(), expected_str) + self.assertEqual(s.blueprint, s.view(blueprint=True, as_list=True, op_code_numbers=True)) + self.assertEqual(str(s), s.view(blueprint=True)) + + def test_script_str(self): + script_str = "1 98 OP_ADD 99 OP_EQUAL" + s = Script.parse_str(script_str) + self.assertEqual(s.view(), script_str) + self.assertTrue(s.evaluate()) + self.assertEqual(s.as_hex(), '0101016293016387') + + script_str_2 = "OP_DUP OP_HASH160 af8e14a2cecd715c363b3a72b55b59a31e2acac9 OP_EQUALVERIFY OP_CHECKSIG" + s = Script.parse_str(script_str_2) + clist = [118, 169, b'\xaf\x8e\x14\xa2\xce\xcdq\\6;:r\xb5[Y\xa3\x1e*\xca\xc9', 136, 172] + self.assertListEqual(s.commands, clist) + self.assertEqual(s.view(), script_str_2) + + def test_script_locking_type(self): + script_str = (b'"\x00 \x04\x7f\x8d]S\x04\xb8\xa1x\xbf\xfb\xd7\xc1\xc0\xc7\xc2To\xc9O\xc3\xb2\x91\n\xdb\x9db' + b'\x19\x85{]\x9f') + self.assertEqual(Script.parse(script_str, is_locking=True).script_types, ['p2wsh']) + self.assertEqual(Script.parse(script_str, is_locking=False).script_types, ['p2sh_p2wsh']) class TestScriptMPInumbers(unittest.TestCase): diff --git a/tests/test_security.py b/tests/test_security.py index 6cb23f20..e4f49d90 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -23,20 +23,44 @@ from sqlalchemy.sql import text from bitcoinlib.db import BCL_DATABASE_DIR from bitcoinlib.wallets import Wallet -from bitcoinlib.config.config import DATABASE_ENCRYPTION_ENABLED +from bitcoinlib.keys import HDKey +from bitcoinlib.encoding import EncodingError -DATABASEFILE_UNITTESTS_ENCRYPTED = os.path.join(str(BCL_DATABASE_DIR), 'bitcoinlib.unittest_security.sqlite') -# DATABASEFILE_UNITTESTS_ENCRYPTED = 'postgresql://postgres:postgres@localhost:5432/bitcoinlib_security' +try: + import mysql.connector + import psycopg + from psycopg import sql +except ImportError: + pass # Only necessary when mysql or postgres is used -class TestSecurity(TestCase): - @classmethod - def setUpClass(cls): - if os.path.isfile(DATABASEFILE_UNITTESTS_ENCRYPTED): - os.remove(DATABASEFILE_UNITTESTS_ENCRYPTED) +if os.getenv('UNITTEST_DATABASE') == 'postgresql': + con = psycopg.connect(user='postgres', host='localhost', password='postgres', autocommit=True) + cur = con.cursor() + cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format(sql.Identifier('bitcoinlib_security'))) + cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier('bitcoinlib_security'))) + cur.close() + con.close() + DATABASEFILE_UNITTESTS_ENCRYPTED = 'postgresql+psycopg://postgres:postgres@localhost:5432/bitcoinlib_security' +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('bitcoinlib_security')) + cur.execute("CREATE DATABASE {}".format('bitcoinlib_security')) + con.commit() + cur.close() + con.close() + DATABASEFILE_UNITTESTS_ENCRYPTED = 'mysql://root:root@localhost:3306/bitcoinlib_security' +else: + DATABASEFILE_UNITTESTS_ENCRYPTED = os.path.join(str(BCL_DATABASE_DIR), 'bitcoinlib.unittest_security.sqlite') + if os.path.isfile(DATABASEFILE_UNITTESTS_ENCRYPTED): + os.remove(DATABASEFILE_UNITTESTS_ENCRYPTED) + + +class TestSecurity(TestCase): - def test_security_wallet_field_encryption(self): + def test_security_wallet_field_encryption_key(self): pk = 'xprv9s21ZrQH143K2HrtPWvqgD8mUhMrrfE1ZME43baM8ti3hWgJwWX1wjHc25y2x11seT5G3KeHFY28MyTRxceeW22kMDAWsMDn7' \ 'rcWnEMFP3t' pk_wif_enc_hex = \ @@ -45,8 +69,8 @@ def test_security_wallet_field_encryption(self): '17f1ffea8e20844309f6fb6b281349a2b3915af3d12dc4c90c3b68f6666eb665682d' pk_enc_hex = 'f8777f10a435d5e3fdbb64cfdcb929626ce38c7103e772921ad1fc21c5e69e474423a998523bf53565ab45711a14086c' - if not DATABASE_ENCRYPTION_ENABLED: - self.skipTest("Database encryption not enabled, skip this test") + if not os.environ.get('DB_FIELD_ENCRYPTION_KEY'): + self.skipTest("Database field encryption key not found in environment, skip this test") self.assertEqual(os.environ.get('DB_FIELD_ENCRYPTION_KEY'), '11223344556677889900aabbccddeeff11223344556677889900aabbccddeeff') @@ -54,13 +78,59 @@ def test_security_wallet_field_encryption(self): wallet.new_key() self.assertEqual(wallet.main_key.wif, pk) - db_query = text('SELECT wif, private FROM keys WHERE id=%d' % wallet._dbwallet.main_key_id) - encrypted_main_key_wif = wallet._session.execute(db_query).fetchone()[0] - encrypted_main_key_private = wallet._session.execute(db_query).fetchone()[1] + if os.getenv('UNITTEST_DATABASE') == 'mysql': + db_query = text("SELECT wif, private FROM `keys` WHERE id=%d" % wallet._dbwallet.main_key_id) + else: + db_query = text("SELECT wif, private FROM keys WHERE id=%d" % wallet._dbwallet.main_key_id) + encrypted_main_key_wif = wallet.session.execute(db_query).fetchone()[0] + encrypted_main_key_private = wallet.session.execute(db_query).fetchone()[1] self.assertIn(type(encrypted_main_key_wif), (bytes, memoryview), "Encryption of database private key failed!") self.assertEqual(encrypted_main_key_wif.hex(), pk_wif_enc_hex) self.assertEqual(encrypted_main_key_private.hex(), pk_enc_hex) + def test_security_wallet_field_encryption_password(self): + pk = ('zprvAWgYBBk7JR8GivM5h6vdbXRrYRC6CU9aFDsVp2gLZ82Tx74UGf7nnN4cToSvNsDnK19tkuyXjzBMDcYvuseYYE5Q4qQo9JaLuNGz' + 'hfcovSp') + pk_wif_enc_hex = \ + ('92410397d5a80ce75cdb5b0fe1204fd2e1411752c75f7b32a8c6a5574570d8be97155bcc4a86b6d34b0e6c22dfe32d340cc90dae1' + '5e54316a9db538ad8a274881c7a45a7be0a00d6e5deda2ea28d8fa0ffcf8783b1fb580df6f5c056e43b79a93859bd083fc1922c86' + 'c17a3f945bbad5fa699a9d1cb2fc9f240708a1eee90b') + pk_enc_hex = '1ff6958f0edc774f16d09d9fb36baa912fb9034f0e354c354a0a91c21e58fe05ad7c8089642565bf4fffd357db108117' + + if not os.environ.get('DB_FIELD_ENCRYPTION_PASSWORD'): + self.skipTest("Database field encryption password not found in environment, skip this test") + self.assertEqual(os.environ.get('DB_FIELD_ENCRYPTION_PASSWORD'), + 'verybadpassword') + + wallet = Wallet.create('wlt-private-key-encryption-test-pwd', keys=pk, + db_uri=DATABASEFILE_UNITTESTS_ENCRYPTED) + wallet.new_key() + self.assertEqual(wallet.main_key.wif, pk) + + if os.getenv('UNITTEST_DATABASE') == 'mysql': + db_query = text("SELECT wif, private FROM `keys` WHERE id=%d" % wallet._dbwallet.main_key_id) + else: + db_query = text("SELECT wif, private FROM keys WHERE id=%d" % wallet._dbwallet.main_key_id) + encrypted_main_key_wif = wallet.session.execute(db_query).fetchone()[0] + encrypted_main_key_private = wallet.session.execute(db_query).fetchone()[1] + self.assertIn(type(encrypted_main_key_wif), (bytes, memoryview), "Encryption of database private key failed!") + self.assertEqual(encrypted_main_key_wif.hex(), pk_wif_enc_hex) + self.assertEqual(encrypted_main_key_private.hex(), pk_enc_hex) + self.assertNotEqual(encrypted_main_key_private, HDKey(pk).private_byte) + + def test_security_encrypted_db_incorrect_password(self): + if not(os.environ.get('DB_FIELD_ENCRYPTION_PASSWORD') or os.environ.get('DB_FIELD_ENCRYPTION_KEY')): + self.skipTest("This test only runs when no encryption keys are provided") + db = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'bitcoinlib_encrypted.db') + self.assertRaisesRegex(EncodingError, "Could not decrypt value \(password incorrect\?\): MAC check failed", + Wallet, 'wlt-encryption-test', db_uri=db) + + def test_security_encrypted_db_no_password(self): + if os.environ.get('DB_FIELD_ENCRYPTION_PASSWORD') or os.environ.get('DB_FIELD_ENCRYPTION_KEY'): + self.skipTest("This test only runs when no encryption keys are provided") + db = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'bitcoinlib_encrypted.db') + self.assertRaisesRegex(ValueError, "Data is encrypted please provide key in environment", + Wallet, 'wlt-encryption-test', db_uri=db) if __name__ == '__main__': main() diff --git a/tests/test_services.py b/tests/test_services.py index edd09284..fbbbf6db 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -22,10 +22,9 @@ import logging try: import mysql.connector - from parameterized import parameterized_class - import psycopg2 - from psycopg2 import sql - from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT + import psycopg + from psycopg 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 @@ -39,19 +38,23 @@ MAXIMUM_ESTIMATED_FEE_DIFFERENCE = 3.00 # Maximum difference from average estimated fee before test_estimatefee fails. # Use value above >0, and 1 for 100% -DATABASEFILE_CACHE_UNITTESTS = os.path.join(str(BCL_DATABASE_DIR), 'bitcoinlibcache.unittest.sqlite') -DATABASEFILE_CACHE_UNITTESTS2 = os.path.join(str(BCL_DATABASE_DIR), 'bitcoinlibcache2.unittest.sqlite') -DATABASE_CACHE_POSTGRESQL = 'postgresql://postgres:postgres@localhost:5432/bitcoinlibcache.unittest' -# FIXME: MySQL databases are not supported. Not allowed to create indexes/primary keys on binary fields -DATABASE_CACHE_MYSQL = 'mysql://root:root@localhost:3306/bitcoinlibcache.unittest' +CACHE_DBNAME1 = 'bitcoinlib_cache_unittest1' +CACHE_DBNAME2 = 'bitcoinlib_cache_unittest2' +# FIXME: Mariadb for cache database does not work due to problem with BLOB indexing +# if os.getenv('UNITTEST_DATABASE') == 'mysql' or os.getenv('UNITTEST_DATABASE') == 'mariadb': +# DATABASE_CACHE_UNITTESTS = 'mariadb://root:root@localhost:3306/%s' % CACHE_DBNAME1 +# DATABASE_CACHE_UNITTESTS2 = 'mariadb://root:root@localhost:3306/%s' % CACHE_DBNAME2 +if os.getenv('UNITTEST_DATABASE') == 'postgresql': + DATABASE_CACHE_UNITTESTS = 'postgresql+psycopg://postgres:postgres@localhost:5432/%s' % CACHE_DBNAME1 + DATABASE_CACHE_UNITTESTS2 = 'postgresql+psycopg://postgres:postgres@localhost:5432/%s' % CACHE_DBNAME2 +else: + DATABASE_CACHE_UNITTESTS = os.path.join(str(BCL_DATABASE_DIR), CACHE_DBNAME1) + '.sqlite' + DATABASE_CACHE_UNITTESTS2 = os.path.join(str(BCL_DATABASE_DIR), CACHE_DBNAME2) + '.sqlite' + DATABASES_CACHE = [ - DATABASEFILE_CACHE_UNITTESTS2, + DATABASE_CACHE_UNITTESTS, + DATABASE_CACHE_UNITTESTS2, ] -if UNITTESTS_FULL_DATABASE_TEST: - DATABASES_CACHE += [ - DATABASE_CACHE_POSTGRESQL, - DATABASE_CACHE_MYSQL - ] TIMEOUT_TEST = 3 @@ -60,7 +63,7 @@ class ServiceTest(Service): def __init__(self, network=DEFAULT_NETWORK, min_providers=1, max_providers=1, providers=None, - timeout=TIMEOUT_TEST, cache_uri=DATABASEFILE_CACHE_UNITTESTS, ignore_priority=True, + timeout=TIMEOUT_TEST, cache_uri=DATABASE_CACHE_UNITTESTS, ignore_priority=True, exclude_providers=None, max_errors=SERVICE_MAX_ERRORS, strict=True): super(self.__class__, self).__init__(network, min_providers, max_providers, providers, timeout, cache_uri, ignore_priority, exclude_providers, max_errors, strict) @@ -94,15 +97,6 @@ def test_service_transaction_get_raw_litecoin(self): '1976a914c1b1668730f13dd1772977e8ce96e3f5f78d290388ac00000000' self.assertEqual(raw_tx, ServiceTest(network='litecoin').getrawtransaction(tx_id)) - # FIXME: Disabled for now, too many broken dash service providers - # def test_service_transaction_get_raw_dash(self): - # tx_id = '885042c885dc0d44167ce71ce82bb28b09bdd8445b7639ea96a5f5be8ceba4cf' - # raw_tx = '0100000001edfbcd24cd10350844061d62d03be6f3ed9c28b26b0b8082539c5d29454f7cb3010000006b483045022100e' \ - # '87b6a6dff07d1b91d12f530992cf8fa9f26a541af525337bbbc5c954cbf072b022062f1cc0f33d036c1c60a7d561de060' \ - # '67528fffca52292d803b75e53f7dfbf63d0121028bd465d7eb03bbee946c3a277ad1b331f78add78c6723eed00097520e' \ - # 'dc21ed2ffffffff0200f90295000000001976a914de4b569d39f05bfc43f56a1b22d7783a7d0661d488aca0fc7c040000' \ - # '00001976a9141495ac5ca428a17197c7cb5065614d8eabfcf8cb88ac00000000' - # self.assertEqual(raw_tx, ServiceTest(network='dash').getrawtransaction(tx_id)) def test_service_sendrawtransaction(self): raw_tx = \ @@ -117,7 +111,7 @@ def test_service_sendrawtransaction(self): except ServiceError: pass for provider in srv.errors: - print("Provider %s" % provider) + # print("Provider %s" % provider) prov_error = str(srv.errors[provider]) if isinstance(srv.errors[provider], Exception) or 'response [429]' in prov_error \ or 'response [503]' in prov_error: @@ -134,7 +128,7 @@ def test_service_get_balance(self): if len(srv.results) < 2: self.fail("Only 1 or less service providers found, nothing to compare. Errors %s" % srv.errors) for provider in srv.results: - print("Provider %s" % provider) + # print("Provider %s" % provider) balance = srv.results[provider] if prev is not None and balance != prev: self.fail("Different address balance from service providers: %d != %d" % (balance, prev)) @@ -148,7 +142,7 @@ def test_service_get_balance_litecoin(self): if len(srv.results) < 2: self.skipTest("Only 1 or less service providers found, nothing to compare. Errors %s" % srv.errors) for provider in srv.results: - print("Provider %s" % provider) + # print("Provider %s" % provider) balance = srv.results[provider] if prev is not None and balance != prev: self.fail("Different address balance from service providers: %d != %d" % (balance, prev)) @@ -172,7 +166,7 @@ def test_service_get_utxos(self): srv = ServiceTest(min_providers=3) srv.getutxos('1Mxww5Q2AK3GxG4R2KyCEao6NJXyoYgyAx') for provider in srv.results: - print("Provider %s" % provider) + # print("Provider %s" % provider) self.assertDictEqualExt(srv.results[provider][0], expected_dict, ['date', 'block_height']) def test_service_get_utxos_after_txid(self): @@ -181,7 +175,7 @@ def test_service_get_utxos_after_txid(self): srv.getutxos('1HLoD9E4SDFFPDiYfNYnkBLQ85Y51J3Zb1', after_txid='9293869acee7d90661ee224135576b45b4b0dbf2b61e4ce30669f1099fecac0c') for provider in srv.results: - print("Testing provider %s" % provider) + # print("Testing provider %s" % provider) self.assertEqual(srv.results[provider][0]['txid'], txid) def test_service_get_utxos_litecoin(self): @@ -189,7 +183,7 @@ def test_service_get_utxos_litecoin(self): srv.getutxos('Lct7CEpiN7e72rUXmYucuhqnCy5F5Vc6Vg') txid = '832518d58e9678bcdb9fe0e417a138daeb880c3a2ee1fb1659f1179efc383c25' for provider in srv.results: - print("Provider %s" % provider) + # print("Provider %s" % provider) self.assertEqual(srv.results[provider][0]['txid'], txid) def test_service_get_utxos_litecoin_after_txid(self): @@ -198,7 +192,7 @@ def test_service_get_utxos_litecoin_after_txid(self): srv.getutxos('Lfx4mFjhRvqyRKxXKqn6jyb17D6NDmosEV', after_txid='b328a91dd15b8b82fef5b01738aaf1f486223d34ee54357e1430c22e46ddd04e') for provider in srv.results: - print("Comparing provider %s" % provider) + # print("Comparing provider %s" % provider) self.assertEqual(srv.results[provider][0]['txid'], txid) def test_service_estimatefee(self): @@ -212,7 +206,7 @@ def test_service_estimatefee(self): # Normalize with dust amount, to avoid errors on small differences dust = Network().dust_amount for provider in srv.results: - print("Provider %s" % provider) + # print("Provider %s" % provider) if srv.results[provider] < average_fee and average_fee - srv.results[provider] > dust: srv.results[provider] += dust elif srv.results[provider] > average_fee and srv.results[provider] - average_fee > dust: @@ -270,7 +264,7 @@ def test_service_gettransactions(self): srv = ServiceTest(min_providers=3) srv.gettransactions(address) for provider in srv.results: - print("Testing: %s" % provider) + # print("Testing: %s" % provider) res = srv.results[provider] t = [r for r in res if r.txid == txid][0] @@ -309,15 +303,9 @@ def test_service_gettransactions_after_txid(self): def test_service_gettransactions_after_txid_segwit(self): res = ServiceTest(timeout=TIMEOUT_TEST, exclude_providers=['blockcypher']).\ - gettransactions('bc1q34aq5drpuwy3wgl9lhup9892qp6svr8ldzyy7c', - after_txid='f91d0a8a78462bc59398f2c5d7a84fcff491c26ba54c4833478b202796c8aafd') - tx_ids = [ - '9e914f4438cdfd2681bf5fb0b3dea8206fffcc48d1ca7e0f05f7b77c76115803', - 'a4bc261faf9ca47722760c9f9f075ab974c7351d8da7b0b5e5a316b3aa7aefa2', - '04be18177781f8060d63390a705cf89ffed2252a3506fab69be7079bc7ba9410'] - self.assertIn(res[0].txid, tx_ids) - self.assertIn(res[1].txid, tx_ids) - self.assertIn(res[2].txid, tx_ids) + gettransactions('bc1qj9hlju59t0m4389033r2x8mlxwc86qgqm9flm626sd22cdhfs9jsyrrp6q', + after_txid='bd430d52f35166a7dd6251c73a48559ad8b5f41b6c5bc4a6c4c1a3e3702f4287') + self.assertEqual(res[0].txid, 'cab75da6d7fe1531c881d4efdb4826410a2604aa9e6442ab12a08363f34fb408') def test_service_gettransactions_after_txid_litecoin(self): res = ServiceTest('litecoin').gettransactions( @@ -425,51 +413,11 @@ def test_service_gettransaction(self): srv.gettransaction('2ae77540ec3ef7b5001de90194ed0ade7522239fe0fc57c12c772d67274e2700') for provider in srv.results: - print("Comparing provider %s" % provider) + # print("Comparing provider %s" % provider) self.assertTrue(srv.results[provider].verify()) self.assertDictEqualExt(srv.results[provider].as_dict(), expected_dict, ['block_hash', 'block_height', 'spent', 'value']) - # FIXME: Disabled, not enough working providers - # def test_service_gettransaction_dash(self): - # expected_dict = {'block_hash': '000000000000002eddff510f4f6c61243e350102c58bdf8c986430b405ce7a22', - # 'network': 'dash', 'input_total': 2575500000, 'fee_per_kb': None, 'outputs': [ - # {'public_key_hash': 'de4b569d39f05bfc43f56a1b22d7783a7d0661d4', 'output_n': 0, 'spent': True, - # 'public_key': '', 'address': 'XvxE6SRkZMbhBW34QfrgxPqcNmgTsRvyeJ', 'script_type': 'p2pkh', - # 'script': '76a914de4b569d39f05bfc43f56a1b22d7783a7d0661d488ac', 'value': 2500000000}, - # {'public_key_hash': '1495ac5ca428a17197c7cb5065614d8eabfcf8cb', 'output_n': 1, 'spent': True, - # 'public_key': '', 'address': 'XcZgeaA4cwUqBqtKUPfZHUme8a5G3gA8LC', 'script_type': 'p2pkh', - # 'script': '76a9141495ac5ca428a17197c7cb5065614d8eabfcf8cb88ac', 'value': 75300000}], - # 'output_total': 2575300000, 'block_height': 900147, 'locktime': 0, 'flag': None, - # 'coinbase': False, - # 'status': 'confirmed', 'version': 1, - # 'hash': '885042c885dc0d44167ce71ce82bb28b09bdd8445b7639ea96a5f5be8ceba4cf', 'size': 226, - # 'fee': 200000, 'inputs': [ - # {'redeemscript': '', 'address': 'XczHdW9k4Kg9mu6AdJayJ1PJtfX3Z9wYxm', 'double_spend': False, - # 'sequence': 4294967295, - # 'prev_txid': 'b37c4f45295d9c5382800b6bb2289cedf3e63bd0621d0644083510cd24cdfbed', 'output_n': 1, - # 'signatures': [ - # 'e87b6a6dff07d1b91d12f530992cf8fa9f26a541af525337bbbc5c954cbf072b62f1cc0f33d036c1c60a7d561de0' - # '6067528fffca52292d803b75e53f7dfbf63d', - # 'e87b6a6dff07d1b91d12f530992cf8fa9f26a541af525337bbbc5c954cbf072b62f1cc0f33d036c1c60a7d561de0' - # '6067528fffca52292d803b75e53f7dfbf63d'], - # 'public_key': '028bd465d7eb03bbee946c3a277ad1b331f78add78c6723eed00097520edc21ed2', 'index_n': 0, - # 'script_type': 'sig_pubkey', - # 'script': '483045022100e87b6a6dff07d1b91d12f530992cf8fa9f26a541af525337bbbc5c954cbf072b022062f1cc' - # '0f33d036c1c60a7d561de06067528fffca52292d803b75e53f7dfbf63d0121028bd465d7eb03bbee946c3a' - # '277ad1b331f78add78c6723eed00097520edc21ed2', - # 'value': 2575500000}], 'date': datetime(2018, 7, 8, 21, 35, 58)} - # - # srv = ServiceTest(network='dash', min_providers=3) - # - # # Get transactions by hash - # srv.gettransaction('885042c885dc0d44167ce71ce82bb28b09bdd8445b7639ea96a5f5be8ceba4cf') - # for provider in srv.results: - # print("Comparing provider %s" % provider) - # self.assertTrue(srv.results[provider].verify()) - # self.assertDictEqualExt(srv.results[provider].as_dict(), expected_dict, - # ['block_hash', 'block_height', 'spent', 'value']) - def test_service_gettransactions_litecoin(self): txid = '832518d58e9678bcdb9fe0e417a138daeb880c3a2ee1fb1659f1179efc383c25' address = 'Lct7CEpiN7e72rUXmYucuhqnCy5F5Vc6Vg' @@ -490,7 +438,7 @@ def test_service_gettransactions_litecoin(self): srv = ServiceTest(min_providers=3, network='litecoin') srv.gettransactions(address) for provider in srv.results: - print("Provider %s" % provider) + # print("Provider %s" % provider) res = srv.results[provider] txs = [r for r in res if r.txid == txid] t = txs[0] @@ -597,7 +545,7 @@ def test_service_gettransaction_segwit_p2wpkh(self): 'script_code': '76a9140ca7deb0a467679f0011efb2906a6e528a8d22ef88ac', 'sequence': 4294967295, 'sigs_required': 1, - 'unlocking_script_unsigned': '76a9140ca7deb0a467679f0011efb2906a6e528a8d22ef88ac', + 'locking_script': '76a9140ca7deb0a467679f0011efb2906a6e528a8d22ef88ac', 'value': 506323064} ], 'locktime': 0, @@ -617,7 +565,7 @@ def test_service_gettransaction_segwit_p2wpkh(self): srv.gettransaction('299dab85f10c37c6296d4fb10eaa323fb456a5e7ada9adf41389c447daa9c0e4') for provider in srv.results: - print("\nComparing provider %s" % provider) + # print("\nComparing provider %s" % provider) self.assertDictEqualExt(srv.results[provider].as_dict(), expected_dict, ['block_hash', 'block_height', 'spent', 'value', 'flag']) @@ -655,32 +603,16 @@ def test_service_network_litecoin_legacy(self): self.assertIn(txid, [utxo['txid'] for utxo in utxos]) def test_service_blockcount(self): - srv = ServiceTest(min_providers=3) - n_blocks = None - for provider in srv.results: - if n_blocks is not None: - self.assertAlmostEqual(srv.results[provider], n_blocks, delta=5000, - msg="Provider %s value %d != %d" % (provider, srv.results[provider], n_blocks)) - n_blocks = srv.results[provider] - - # Test Litecoin network - srv = ServiceTest(min_providers=3, network='litecoin') - n_blocks = None - for provider in srv.results: - if n_blocks is not None: - self.assertAlmostEqual(srv.results[provider], n_blocks, delta=5000, - msg="Provider %s value %d != %d" % (provider, srv.results[provider], n_blocks)) - n_blocks = srv.results[provider] - - # FIXME: Disabled, not enough working providers - # # Test Dash network - # srv = ServiceTest(min_providers=3, network='dash') - # n_blocks = None - # for provider in srv.results: - # if n_blocks is not None: - # self.assertAlmostEqual(srv.results[provider], n_blocks, delta=5000, - # msg="Provider %s value %d != %d" % (provider, srv.results[provider], n_blocks)) - # n_blocks = srv.results[provider] + for nw in ['bitcoin', 'litecoin', 'testnet']: + srv = ServiceTest(min_providers=3, cache_uri='', network=nw, exclude_providers=['bitgo', 'bitaps']) + srv.blockcount() + n_blocks = None + for provider in srv.results: + if n_blocks is not None: + self.assertAlmostEqual(srv.results[provider], n_blocks, delta=200, + msg="Network %s, provider %s value %d != %d" % + (nw, provider, srv.results[provider], n_blocks)) + n_blocks = srv.results[provider] def test_service_max_providers(self): srv = ServiceTest(max_providers=1, cache_uri='') @@ -707,23 +639,6 @@ def test_service_mempool(self): # print("Mempool: Comparing ltc provider %s" % provider) self.assertListEqual(srv.results[provider], []) - # FIXME: Disabled, not enough working providers - # txid = '15641a37e21a0cf7611a1633954be645512f1ab725a0d5077a9ad0aa0ca20bed' - # srv = ServiceTest(min_providers=3, network='dash') - # srv.mempool(txid) - # for provider in srv.results: - # # print("Mempool: Comparing dash provider %s" % provider) - # self.assertListEqual(srv.results[provider], []) - - # FIXME: Disabled, not enough working providers - # def test_service_dash(self): - # srv = ServiceTest(network='dash') - # address = 'XoLTipv6ryWECYu94vbkmDjntAXqNgouTW' - # txid = 'f770f05d2b1c63b71b2650227252da06ef226661982c4ee9b136b64f77bbbd0c' - # self.assertGreaterEqual(srv.getbalance(address), 50000000000) - # self.assertEqual(srv.getutxos(address)[0]['txid'], txid) - # self.assertEqual(srv.gettransactions(address)[0].txid, txid) - def test_service_getblock_id(self): srv = ServiceTest(min_providers=3, timeout=TIMEOUT_TEST, cache_uri='') srv.getblock('0000000000000a3290f20e75860d505ce0e948a1d1d846bec7e39015d242884b', parse_transactions=False) @@ -748,7 +663,7 @@ def test_service_getblock_id(self): def test_service_getblock_height(self): srv = ServiceTest(timeout=TIMEOUT_TEST, cache_uri='') b = srv.getblock(599999, parse_transactions=True, limit=3) - print("Test getblock using provider %s" % list(srv.results.keys())[0]) + # print("Test getblock using provider %s" % list(srv.results.keys())[0]) self.assertEqual(b.height, 599999) self.assertEqual(to_hexstring(b.block_hash), '00000000000000000003ecd827f336c6971f6f77a0b9fba362398dd867975645') self.assertEqual(to_hexstring(b.merkle_root), 'ca13ce7f21619f73fb5a062696ec06a4427c6ad9e523e7bc1cf5287c137ddcea') @@ -774,7 +689,7 @@ def test_service_getblock_height(self): def test_service_getblock_parse_tx_paging(self): srv = ServiceTest(timeout=TIMEOUT_TEST, cache_uri='') b = srv.getblock(120000, parse_transactions=True, limit=25, page=2) - print("Test getblock using provider %s" % list(srv.results.keys())[0]) + # print("Test getblock using provider %s" % list(srv.results.keys())[0]) self.assertEqual(to_hexstring(b.block_hash), '0000000000000e07595fca57b37fea8522e95e0f6891779cfd34d7e537524471') self.assertEqual(b.height, 120000) @@ -793,7 +708,7 @@ def test_service_getblock_parse_tx_paging_last_page(self): def test_service_getblock_litecoin(self): srv = ServiceTest(timeout=TIMEOUT_TEST, network='litecoin', cache_uri='') b = srv.getblock(1000000, parse_transactions=True, limit=2) - print("Test getblock using provider %s" % list(srv.results.keys())[0]) + # print("Test getblock using provider %s" % list(srv.results.keys())[0]) self.assertEqual(b.height, 1000000) self.assertEqual(to_hexstring(b.block_hash), '8ceae698f0a2d338e39b213eb9c253a91a270ca6451a4d9bba7bf2c9e637dfda') self.assertEqual(to_hexstring(b.merkle_root), @@ -877,37 +792,56 @@ def test_service_transaction_unconfirmed(self): self.assertIsNone(t.date) self.assertIsNone(t.block_height) + def test_service_exlude_providers(self): + srv = ServiceTest(network='testnet', cache_uri='') + providers = [srv.providers[pi]['provider'] for pi in srv.providers] + try: + srv2 = ServiceTest(network='testnet', exclude_providers=providers[1:], cache_uri='') + except ServiceError: + self.skipTest("Blockcount for provider %s was not successful" % providers[0]) + self.assertEqual(len(srv2.providers), 1) + class TestServiceCache(unittest.TestCase): - # TODO: Add mysql support @classmethod def setUpClass(cls): - try: - if os.path.isfile(DATABASEFILE_CACHE_UNITTESTS2): - os.remove(DATABASEFILE_CACHE_UNITTESTS2) - except Exception: - pass - try: - DbCache(DATABASE_CACHE_POSTGRESQL).drop_db() - # DbCache(DATABASEFILE_CACHE_MYSQL).drop_db() - except Exception: - pass + if os.getenv('UNITTEST_DATABASE') == 'postgresql': + try: + 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(CACHE_DBNAME1))) + cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format(sql.Identifier(CACHE_DBNAME2))) + cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(CACHE_DBNAME1))) + cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(CACHE_DBNAME2))) + except: + pass + cur.close() + con.close() + except Exception: + pass + # 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(CACHE_DBNAME1)) + # cur.execute("DROP DATABASE IF EXISTS {}".format(CACHE_DBNAME2)) + # cur.execute("CREATE DATABASE {}".format(CACHE_DBNAME1)) + # cur.execute("CREATE DATABASE {}".format(CACHE_DBNAME2)) + # con.commit() + # cur.close() + # con.close() + else: + if os.path.isfile(DATABASE_CACHE_UNITTESTS): + try: + os.remove(DATABASE_CACHE_UNITTESTS) + os.remove(DATABASE_CACHE_UNITTESTS2) + except: + pass - try: - con = psycopg2.connect(user='postgres', host='localhost', password='postgres') - con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) - cur = con.cursor() - cur.execute(sql.SQL("CREATE DATABASE {}").format( - sql.Identifier('bitcoinlibcache.unittest')) - ) - cur.close() - con.close() - except Exception: - pass def test_service_cache_transactions(self): - srv = ServiceTest(cache_uri=DATABASEFILE_CACHE_UNITTESTS2) + srv = ServiceTest(cache_uri=DATABASE_CACHE_UNITTESTS2) address = '1JQ7ybfFBoWhPJpjoihezpeAjd2xv9nXaN' # Get 2 transactions, nothing in cache res = srv.gettransactions(address, limit=2) @@ -931,7 +865,7 @@ def test_service_cache_transactions(self): # FIXME: Disabled, lack of providers # def test_service_cache_gettransaction(self): - # srv = ServiceTest(network='litecoin_testnet', cache_uri=DATABASEFILE_CACHE_UNITTESTS2) + # srv = ServiceTest(network='litecoin_testnet', cache_uri=DATABASE_CACHE_UNITTESTS2) # txid = 'b6533d361daac291f64fff32a5c157a4785b423ce36e2eac27117879f93973da' # # t = srv.gettransaction(txid) @@ -956,7 +890,7 @@ def test_service_cache_transactions(self): def test_service_cache_transactions_after_txid(self): # Do not store anything in cache if after_txid is used - srv = ServiceTest(cache_uri=DATABASEFILE_CACHE_UNITTESTS2, exclude_providers=['mempool']) + srv = ServiceTest(cache_uri=DATABASE_CACHE_UNITTESTS2, exclude_providers=['mempool']) address = '12spqcvLTFhL38oNJDDLfW1GpFGxLdaLCL' res = srv.gettransactions(address, after_txid='5f31da8f47a5bd92a6929179082c559e8acc270a040b19838230aab26309cf2d') @@ -976,7 +910,7 @@ def test_service_cache_transactions_after_txid(self): self.assertGreaterEqual(srv.results_cache_n, 1) def test_service_cache_transaction_coinbase(self): - srv = ServiceTest(cache_uri=DATABASEFILE_CACHE_UNITTESTS2, exclude_providers=['bitaps', 'bitgo']) + srv = ServiceTest(cache_uri=DATABASE_CACHE_UNITTESTS2, exclude_providers=['bitaps', 'bitgo']) t = srv.gettransaction('68104dbd6819375e7bdf96562f89290b41598df7b002089ecdd3c8d999025b13') if t: self.assertGreaterEqual(srv.results_cache_n, 0) @@ -1000,7 +934,7 @@ def test_service_cache_transaction_segwit_database(self): self.assertEqual(t.raw_hex(), rawtx) def test_service_cache_with_latest_tx_query(self): - srv = ServiceTest(cache_uri=DATABASEFILE_CACHE_UNITTESTS2) + srv = ServiceTest(cache_uri=DATABASE_CACHE_UNITTESTS2) address = 'bc1qxfrgfhs49d7dtcfzlhp7f7cwsp8zpp60hywp0f' after_txid = '13401ad121c8ae91e18b4bb0db5d8f350a2b0b5ddd5ca26165137bf07fefad90' srv.gettransaction('4156e78f347e47d2ccdd4a19614d958c6e4502d09a68f63ed0c72691f63a5028') @@ -1010,7 +944,7 @@ def test_service_cache_with_latest_tx_query(self): self.assertGreaterEqual(len(txs), 5) def test_service_cache_correctly_update_spent_info(self): - srv = ServiceTest(cache_uri=DATABASEFILE_CACHE_UNITTESTS2) + srv = ServiceTest(cache_uri=DATABASE_CACHE_UNITTESTS2) srv.gettransactions('1KoAvaL3wfpcNvGCQYkqFJG9Ccqm52sZHa', limit=1) txs = srv.gettransactions('1KoAvaL3wfpcNvGCQYkqFJG9Ccqm52sZHa') self.assertTrue(txs[0].outputs[0].spent) @@ -1034,7 +968,7 @@ def check_block_128594(b): for cache_db in DATABASES_CACHE: srv = ServiceTest(cache_uri=cache_db, exclude_providers=['blockchair', 'bitcoind']) b = srv.getblock('0000000000001a7dcac3c01bf10c5d5fe53dc8cc4b9c94001662e9d7bd36f6cc', limit=1) - print("Test getblock with hash using provider %s" % list(srv.results.keys())[0]) + # print("Test getblock with hash using provider %s" % list(srv.results.keys())[0]) check_block_128594(b) self.assertEqual(srv.results_cache_n, 0) @@ -1052,10 +986,18 @@ def test_service_cache_disabled(self): def test_service_cache_transaction_p2sh_p2wpkh_input(self): txid = '6ab6432a6b7b04ecc335c6e8adccc45c25f46e33752478f0bcacaf3f1b61ad92' - srv = ServiceTest(cache_uri=DATABASEFILE_CACHE_UNITTESTS2) + srv = ServiceTest(cache_uri=DATABASE_CACHE_UNITTESTS2) t = srv.gettransaction(txid) self.assertEqual(t.size, 249) self.assertEqual(srv.results_cache_n, 0) t2 = srv.gettransaction(txid) self.assertEqual(t2.size, 249) self.assertEqual(srv.results_cache_n, 1) + + def test_service_cache_transaction_index(self): + srv = ServiceTest(cache_uri=DATABASE_CACHE_UNITTESTS2) + srv.getblock(104444, parse_transactions=True) + t = srv.gettransaction('d7795eb181ef87a35298e8689cabf852e831824ded4c23b1a7f711df119a6599') + if not srv.results_cache_n: + self.skipTest('Transaction not indexed for selected provider') + self.assertEqual(t.index, 5) \ No newline at end of file diff --git a/tests/test_tools.py b/tests/test_tools.py index 68de0e98..b37998a0 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -2,9 +2,10 @@ # # BitcoinLib - Python Cryptocurrency Library # Unit Tests for Bitcoinlib Tools -# © 2018 May - 1200 Web Development +# © 2018 - 2024 January - 1200 Web Development # +import ast import os import sys import unittest @@ -12,74 +13,62 @@ try: import mysql.connector - import psycopg2 - from parameterized import parameterized_class - from psycopg2 import sql - from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT + import psycopg + from psycopg import sql except ImportError: pass # Only necessary when mysql or postgres is used -from bitcoinlib.main import UNITTESTS_FULL_DATABASE_TEST -from bitcoinlib.db import BCL_DATABASE_DIR +from bitcoinlib.db import BCL_DATABASE_DIR, session from bitcoinlib.encoding import normalize_string -SQLITE_DATABASE_FILE = os.path.join(str(BCL_DATABASE_DIR), 'bitcoinlib.unittest.sqlite') DATABASE_NAME = 'bitcoinlib_unittest' -def init_sqlite(_): - if os.path.isfile(SQLITE_DATABASE_FILE): - os.remove(SQLITE_DATABASE_FILE) +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() + cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format( + sql.Identifier(dbname)) + ) + cur.execute(sql.SQL("CREATE DATABASE {}").format( + sql.Identifier(dbname)) + ) + 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): + os.remove(dburi) + return dburi -def init_postgresql(_): - con = psycopg2.connect(user='postgres', host='localhost', password='postgres') - con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) - cur = con.cursor() - cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format( - sql.Identifier(DATABASE_NAME)) - ) - cur.execute(sql.SQL("CREATE DATABASE {}").format( - sql.Identifier(DATABASE_NAME)) - ) - cur.close() - con.close() - - -def init_mysql(_): - con = mysql.connector.connect(user='root', host='localhost') - cur = con.cursor() - cur.execute("DROP DATABASE IF EXISTS {}".format(DATABASE_NAME)) - cur.execute("CREATE DATABASE {}".format(DATABASE_NAME)) - con.commit() - cur.close() - con.close() - - -db_uris = (('sqlite:///' + SQLITE_DATABASE_FILE, init_sqlite),) -if UNITTESTS_FULL_DATABASE_TEST: - db_uris += ( - # ('mysql://root@localhost:3306/' + DATABASE_NAME, init_mysql), - ('postgresql://postgres:postgres@localhost:5432/' + DATABASE_NAME, init_postgresql), - ) - - -@parameterized_class(('DATABASE_URI', 'init_fn'), db_uris) class TestToolsCommandLineWallet(unittest.TestCase): def setUp(self): - self.init_fn() self.python_executable = sys.executable self.clw_executable = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../bitcoinlib/tools/clw.py')) + self.database_uri = database_init() def test_tools_clw_create_wallet(self): - cmd_wlt_create = '%s %s test --passphrase "emotion camp sponsor curious bacon squeeze bean world ' \ + cmd_wlt_create = '%s %s new -w test --passphrase "emotion camp sponsor curious bacon squeeze bean world ' \ 'actual chicken obscure spray" -r -d %s' % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) - cmd_wlt_delete = "%s %s test --wallet-remove -d %s" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) - output_wlt_create = "14guS7uQpEbgf1e8TDo1zTEURJW3NGPc9E" + (self.python_executable, self.clw_executable, self.database_uri) + cmd_wlt_delete = "%s %s -w test --wallet-remove -d %s" % \ + (self.python_executable, self.clw_executable, self.database_uri) + output_wlt_create = "bc1qdv5tuzrluh4lzhnu59je9n83w4hkqjhgg44d5g" output_wlt_delete = "Wallet test has been removed" process = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True) @@ -96,10 +85,11 @@ def test_tools_clw_create_multisig_wallet(self): 'tprv8ZgxMBicQKsPeUbMS6kswJc11zgVEXUnUZuGo3bF6bBrAg1ieFfUdPc9UHqbD5HcXizThrcKike1c4z6xHrz6MWGwy8L6YKVbgJ' 'MeQHdWDp' ] - cmd_wlt_create = "%s %s testms -m 2 2 %s -r -n testnet -d %s" % \ - (self.python_executable, self.clw_executable, ' '.join(key_list), self.DATABASE_URI) - cmd_wlt_delete = "%s %s testms --wallet-remove -d %s" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + cmd_wlt_create = "%s %s new -w testms -m 2 2 %s -r -n testnet -d %s -o 0" % \ + (self.python_executable, self.clw_executable, ' '.join(key_list), self.database_uri) + print(cmd_wlt_create) + cmd_wlt_delete = "%s %s -w testms --wallet-remove -d %s" % \ + (self.python_executable, self.clw_executable, self.database_uri) output_wlt_create = "2NBrLTapyFqU4Wo29xG4QeEt8kn38KVWRR" output_wlt_delete = "Wallet testms has been removed" @@ -115,39 +105,39 @@ def test_tools_clw_create_multisig_wallet_one_key(self): 'tprv8ZgxMBicQKsPd1Q44tfDiZC98iYouKRC2CzjT3HGt1yYw2zuX2awTotzGAZQEAU9bi2M5MCj8iedP9MREPjUgpDEBwBgGi2C8eK' '5zNYeiX8' ] - cmd_wlt_create = "%s %s testms1 -m 2 2 %s -r -n testnet -d %s" % \ - (self.python_executable, self.clw_executable, ' '.join(key_list), self.DATABASE_URI) - cmd_wlt_delete = "%s %s testms1 --wallet-remove -d %s" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) - output_wlt_create = "if you understood and wrote down your key: Receive address:" + cmd_wlt_create = "%s %s new -w testms1 -m 2 2 %s -r -n testnet -d %s -o 0" % \ + (self.python_executable, self.clw_executable, ' '.join(key_list), self.database_uri) + cmd_wlt_delete = "%s %s -w testms1 --wallet-remove -d %s" % \ + (self.python_executable, self.clw_executable, self.database_uri) + output_wlt_create = "if you understood and wrote down your key" output_wlt_delete = "Wallet testms1 has been removed" process = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True) - poutput = process.communicate(input=b'y\nyes') + poutput = process.communicate(input=b'yes') self.assertIn(output_wlt_create, normalize_string(poutput[0])) process = Popen(cmd_wlt_delete, stdin=PIPE, stdout=PIPE, shell=True) poutput = process.communicate(input=b'testms1') self.assertIn(output_wlt_delete, normalize_string(poutput[0])) def test_tools_clw_create_multisig_wallet_error(self): - cmd_wlt_create = "%s %s testms2 -m 2 a -d %s" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) - output_wlt_create = "Number of signatures required (second argument) must be a numeric value" + cmd_wlt_create = "%s %s new -w testms2 -m 2 a -d %s" % \ + (self.python_executable, self.clw_executable, self.database_uri) + output_wlt_create = "Number of total signatures (second argument) must be a numeric value" process = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True) poutput = process.communicate(input=b'y') self.assertIn(output_wlt_create, normalize_string(poutput[0])) def test_tools_clw_transaction_with_script(self): - cmd_wlt_create = '%s %s test2 --passphrase "emotion camp sponsor curious bacon squeeze bean world ' \ + cmd_wlt_create = '%s %s new -w test2 --passphrase "emotion camp sponsor curious bacon squeeze bean world ' \ 'actual chicken obscure spray" -r -n bitcoinlib_test -d %s' % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) - cmd_wlt_update = "%s %s test2 -d %s" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) - cmd_wlt_transaction = "%s %s test2 -d %s -t 21HVXMEdxdgjNzgfERhPwX4okXZ8WijHkvu 50000000 -f 100000 -p" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) - cmd_wlt_delete = "%s %s test2 --wallet-remove -d %s" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) - output_wlt_create = "21GPfxeCbBunsVev4uS6exPhqE8brPs1ZDF" + (self.python_executable, self.clw_executable, self.database_uri) + cmd_wlt_update = "%s %s -w test2 -x -d %s" % \ + (self.python_executable, self.clw_executable, self.database_uri) + cmd_wlt_transaction = "%s %s -w test2 -d %s -s 21HVXMEdxdgjNzgfERhPwX4okXZ8WijHkvu 0.5 -f 100000 -p" % \ + (self.python_executable, self.clw_executable, self.database_uri) + cmd_wlt_delete = "%s %s -w test2 --wallet-remove -d %s" % \ + (self.python_executable, self.clw_executable, self.database_uri) + output_wlt_create = "blt1qj0mgwyhxuw9p0ngj5kqnxhlrx8ypecqekm2gr7" output_wlt_transaction = 'Transaction pushed to network' output_wlt_delete = "Wallet test2 has been removed" @@ -167,11 +157,11 @@ def test_tools_clw_transaction_with_script(self): self.assertIn(output_wlt_delete, normalize_string(poutput[0])) def test_tools_clw_create_litecoin_segwit_wallet(self): - cmd_wlt_create = '%s %s ltcsw --passphrase "lounge chief tip frog camera build trouble write end ' \ - 'sword order share" -r -d %s -y segwit -n litecoin' % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) - cmd_wlt_delete = "%s %s ltcsw --wallet-remove -d %s" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + cmd_wlt_create = '%s %s new -w ltcsw --passphrase "lounge chief tip frog camera build trouble write end ' \ + 'sword order share" -d %s -j segwit -n litecoin -r' % \ + (self.python_executable, self.clw_executable, self.database_uri) + cmd_wlt_delete = "%s %s -w ltcsw --wallet-remove -d %s" % \ + (self.python_executable, self.clw_executable, self.database_uri) output_wlt_create = "ltc1qgc7c2z56rr4lftg0fr8tgh2vknqc3yuydedu6m" output_wlt_delete = "Wallet ltcsw has been removed" @@ -191,10 +181,10 @@ def test_tools_clw_create_multisig_wallet_p2sh_segwit(self): 'YprvANkMzkodih9AKQ8evAkiDWCzpQsU6N1uasNtWznNj44Y2X6FJqkv9wcfavxVEkz9qru7VKRhzmQXqy562b9Tk4JGdsaVazByzmX' '7FW6wpKW' ] - cmd_wlt_create = "%s %s testms-p2sh-segwit -m 3 2 %s -r -y p2sh-segwit -d %s" % \ - (self.python_executable, self.clw_executable, ' '.join(key_list), self.DATABASE_URI) - cmd_wlt_delete = "%s %s testms-p2sh-segwit --wallet-remove -d %s" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + cmd_wlt_create = "%s %s new -w testms-p2sh-segwit -m 2 3 %s -r -j p2sh-segwit -d %s -o 0" % \ + (self.python_executable, self.clw_executable, ' '.join(key_list), self.database_uri) + cmd_wlt_delete = "%s %s -w testms-p2sh-segwit --wallet-remove -d %s" % \ + (self.python_executable, self.clw_executable, self.database_uri) output_wlt_create = "3MtNi5U2cjs3EcPizzjarSz87pU9DTANge" output_wlt_delete = "Wallet testms-p2sh-segwit has been removed" @@ -205,6 +195,166 @@ def test_tools_clw_create_multisig_wallet_p2sh_segwit(self): poutput = process.communicate(input=b'testms-p2sh-segwit') self.assertIn(output_wlt_delete, normalize_string(poutput[0])) + def test_tools_generate_key_quiet(self): + cmd_generate_passphrase = "%s %s -gq --passphrase-strength 256" % \ + (self.python_executable, self.clw_executable) + process = Popen(cmd_generate_passphrase, stdin=PIPE, stdout=PIPE, shell=True) + poutput = process.communicate()[0] + self.assertEqual(len(poutput.split(b' ')), 24) + + def test_tools_wallet_create_from_key(self): + phrase = ("hover rescue clock ocean strategy post melt banner anxiety phone pink paper enhance more " + "copy gate bag brass raise logic stone duck muffin conduct") + cmd_wlt_create = "%s %s new -w wlt_from_key -c \"%s\" -d %s -y" % \ + (self.python_executable, self.clw_executable, phrase, self.database_uri) + output_wlt_create = "bc1qpylcrcyqa5wkwe2stzc6h7q0mhs5skxuas44w2" + + poutput = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True).communicate() + self.assertIn(output_wlt_create, normalize_string(poutput[0])) + + def test_tools_wallet_send_to_multi(self): + send_str = ("-s blt1qzt90vqqjsqspuaegu9fh4e2htaxrgt0l76d9gz 0.1 " + "-s blt1qu825hm0a6ajg66j79x4tzkn56qmljjms97c5tp 1") + cmd_wlt_create = "%s %s new -w wallet_send_to_multi -d %s -n bitcoinlib_test -yq" % \ + (self.python_executable, self.clw_executable, self.database_uri) + cmd_wlt_update = "%s %s -w wallet_send_to_multi -d %s -x" % \ + (self.python_executable, self.clw_executable, self.database_uri) + cmd_wlt_send = "%s %s -w wallet_send_to_multi -d %s %s" % \ + (self.python_executable, self.clw_executable, self.database_uri, send_str) + + Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True).communicate() + Popen(cmd_wlt_update, stdin=PIPE, stdout=PIPE, shell=True).communicate() + process = Popen(cmd_wlt_send, stdin=PIPE, stdout=PIPE, shell=True) + self.assertIn(b"Transaction created", process.communicate()[0]) + + def test_tools_wallet_empty(self): + pk = ("zprvAWgYBBk7JR8GiejuVoZaVXtWf5zNawFbTH88uKao9qnZxBypJQNvh1tGHZghpfjUfSUiS7G7MmNw3cyakkNcNis3MjD4ic54n" + "FY5LQxMszQ") + cmd_wlt_create = "%s %s new -w wlt_create_and_empty -c %s -d %s -y" % \ + (self.python_executable, self.clw_executable, pk, self.database_uri) + output_wlt_create = "bc1qqnqkjpnmr5zsxar76wxqcntp28ltly0fz6crdg" + poutput = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True).communicate() + self.assertIn(output_wlt_create, normalize_string(poutput[0])) + + cmd_wlt_empty = "%s %s -w wlt_create_and_empty -d %s --wallet-empty" % \ + (self.python_executable, self.clw_executable, self.database_uri) + poutput = Popen(cmd_wlt_empty, stdin=PIPE, stdout=PIPE, shell=True).communicate() + self.assertIn("Removed transactions and emptied wallet", normalize_string(poutput[0])) + + cmd_wlt_info = "%s %s -w wlt_create_and_empty -d %s -i" % \ + (self.python_executable, self.clw_executable, self.database_uri) + poutput = Popen(cmd_wlt_info, stdin=PIPE, stdout=PIPE, shell=True).communicate() + self.assertIn("- - Transactions Account 0 (0)", normalize_string(poutput[0])) + self.assertNotIn(output_wlt_create, normalize_string(poutput[0])) + + def test_tools_wallet_sweep(self): + cmd_wlt_create = "%s %s new -w wlt_sweep -d %s -n bitcoinlib_test -yq" % \ + (self.python_executable, self.clw_executable, self.database_uri) + cmd_wlt_update = "%s %s -w wlt_sweep -d %s -x" % \ + (self.python_executable, self.clw_executable, self.database_uri) + cmd_wlt_send = "%s %s -w wlt_sweep -d %s --sweep blt1qzt90vqqjsqspuaegu9fh4e2htaxrgt0l76d9gz -p" % \ + (self.python_executable, self.clw_executable, self.database_uri) + cmd_wlt_info = "%s %s -w wlt_sweep -d %s -i" % \ + (self.python_executable, self.clw_executable, self.database_uri) + Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True).communicate() + Popen(cmd_wlt_update, stdin=PIPE, stdout=PIPE, shell=True).communicate() + process = Popen(cmd_wlt_send, stdin=PIPE, stdout=PIPE, shell=True) + self.assertIn(b"Transaction pushed to network", process.communicate()[0]) + process = Popen(cmd_wlt_info, stdin=PIPE, stdout=PIPE, shell=True) + self.assertIn("-1.00000000 T = Balance Totals (includes unconfirmed) =", + normalize_string(process.communicate()[0]).replace('\n', '').replace('\r', '')) + + def test_tools_wallet_multisig_cosigners(self): + pk1 = ('BC12Se7KL1uS2bA6QNjPAjFirwyoB8bDA3EPLMwDex7D3fZrWG4pP2zUcyEPKpgXfcoxxhZQqWX7b57MBWVxjjioNvsfvnpJVT9' + 'XWVvHtmdyowDz') + pk2 = ('BC12Se7KL1uS2bA6QQH1M6YkFGbNXoFSUavaE6EfMEmTrtSERw1JRCWf6Jj5tfoLhZopA4s2FSzqZqYTMpChvUvV9KdgtnJ1sFi' + 'B7SZVyHC31ybq') + pk3 = ('BC12Se7KL1uS2bA6QNjZ8T9CzaubwGjTH3WTaZdDB45GVwNMt26ixhgk4L8zus4NxhKWez5xj6xiT7DkpsSnD363h8WEoR7b5d2' + 'u64ec4KeCXQKg') + pub_key1 = ('BC11mYr7gRWJM1oBUFSkW8tPWVeb8bVv9kzjkjH7emfNnsSWVKLo24vopvN8vxud7VvFjYBvhCrEECC6mVTtE7imyytvkLT' + '9URKHJ3Crs1dSecKa') + pub_key2 = ('BC11mYrAhSZGc4JJYubuRSJDjbeoi2BueBjggutvkC8AMv8v2vdKT9T1Tq5VmXgnmzdb2maK5VF5fnbpZR1yt5bJRNBAgJb' + 'ZYXRnhWiS3jjHqgeZ') + pub_key3 = ('BC11mYrL5yBtMgaYxHEUg3anvLX3gcLi8hbtwbjymReCgGiP6hYifVMi96M3ejtvZpZbDvetBfbzgRxmu22ZkqP2i7yhFge' + 'mSkHp7BRhoDubrQvs') + cmd_wlt_create1 = ("%s %s new -w wlt_multisig_2_3_A -m 2 3 %s %s %s -d %s -n bitcoinlib_test -q " + "--disable-anti-fee-sniping") % \ + (self.python_executable, self.clw_executable, pk1, pub_key2, pub_key3, self.database_uri) + Popen(cmd_wlt_create1, stdin=PIPE, stdout=PIPE, shell=True).communicate() + cmd_wlt_create2 = ("%s %s new -w wlt_multisig_2_3_B -m 2 3 %s %s %s -d %s -n bitcoinlib_test -q " + "--disable-anti-fee-sniping") % \ + (self.python_executable, self.clw_executable, pub_key1, pub_key2, pk3, self.database_uri) + print(cmd_wlt_create2) + Popen(cmd_wlt_create2, stdin=PIPE, stdout=PIPE, shell=True).communicate() + + cmd_wlt_receive1 = "%s %s -w wlt_multisig_2_3_A -d %s -r -o 1 -q" % \ + (self.python_executable, self.clw_executable, self.database_uri) + output1 = Popen(cmd_wlt_receive1, stdin=PIPE, stdout=PIPE, shell=True).communicate() + cmd_wlt_receive2 = "%s %s -w wlt_multisig_2_3_B -d %s -r -o 1 -q" % \ + (self.python_executable, self.clw_executable, self.database_uri) + output2 = Popen(cmd_wlt_receive2, stdin=PIPE, stdout=PIPE, shell=True).communicate() + self.assertEqual(output1[0], output2[0]) + address = normalize_string(output1[0].strip(b'\n')) + + cmd_wlt_update1 = "%s %s -w wlt_multisig_2_3_A -d %s -x -o 1" % \ + (self.python_executable, self.clw_executable, self.database_uri) + Popen(cmd_wlt_update1, stdin=PIPE, stdout=PIPE, shell=True).communicate() + cmd_wlt_update2 = "%s %s -w wlt_multisig_2_3_B -d %s -x -o 1" % \ + (self.python_executable, self.clw_executable, self.database_uri) + Popen(cmd_wlt_update2, stdin=PIPE, stdout=PIPE, shell=True).communicate() + + create_tx = "%s %s -w wlt_multisig_2_3_A -d %s -s %s 0.5 -o 1" % \ + (self.python_executable, self.clw_executable, self.database_uri, address) + output = Popen(create_tx, stdin=PIPE, stdout=PIPE, shell=True).communicate() + tx_dict_str = '{' + normalize_string(output[0]).split('{', 1)[1] + sign_tx = "%s %s -w wlt_multisig_2_3_B -d %s -o 1 --import-tx \"%s\"" % \ + (self.python_executable, self.clw_executable, self.database_uri, + tx_dict_str.replace('\r', '').replace('\n', '')) + output = Popen(sign_tx, stdin=PIPE, stdout=PIPE, shell=True).communicate() + response = normalize_string(output[0]) + self.assertIn('12821f8ac330e4eddb9f87ea29456b31ec300e232d2c63880f669a9b15e3741f', response) + self.assertIn('Signed transaction', response) + self.assertIn("'verified': True,", response) + + filename = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'import_test.tx') + sign_import_tx_file = "%s %s -w wlt_multisig_2_3_B -d %s -o 1 --import-tx-file %s" % \ + (self.python_executable, self.clw_executable, self.database_uri, filename) + output = Popen(sign_import_tx_file, stdin=PIPE, stdout=PIPE, shell=True).communicate() + response2 = normalize_string(output[0]) + self.assertIn('2e07be62d933f5b257ac066b874df651cd6e6763795c24036904024a2b44180b', response2) + self.assertIn('239M1DxQuxJcMHtYBdG6A81bfXQrrCNa2rr', response2) + self.assertIn('Signed transaction', response2) + self.assertIn("'verified': True,", response2) + + def test_tools_transaction_options(self): + cmd_wlt_create = "%s %s new -w test_tools_transaction_options -d %s -n bitcoinlib_test -yq" % \ + (self.python_executable, self.clw_executable, self.database_uri) + cmd_wlt_update = "%s %s -w test_tools_transaction_options -d %s -x" % \ + (self.python_executable, self.clw_executable, self.database_uri) + cmd_wlt_send = ("%s %s -w test_tools_transaction_options -d %s -s blt1qg7du8cs0scxccmfly7x252qurv7kwsy6rm4xr7 0.001 " + "--number-of-change-outputs 5") % \ + (self.python_executable, self.clw_executable, self.database_uri) + Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True).communicate() + Popen(cmd_wlt_update, stdin=PIPE, stdout=PIPE, shell=True).communicate() + output = normalize_string(Popen(cmd_wlt_send, stdin=PIPE, stdout=PIPE, shell=True).communicate()[0]) + tx_dict_str = '{' + output.split('{', 1)[1] + tx_dict = ast.literal_eval(tx_dict_str.replace('\r', '').replace('\n', '')) + self.assertEqual(len(tx_dict['outputs']), 6) + self.assertTrue(tx_dict['verified']) + + cmd_wlt_update2 = "%s %s -w test_tools_transaction_options -d %s -ix" % \ + (self.python_executable, self.clw_executable, self.database_uri) + output = normalize_string(Popen(cmd_wlt_update2, stdin=PIPE, stdout=PIPE, shell=True).communicate()[0]) + output_list = [i for i in output.split('Keys')[1].split(' ') if i != ''] + first_key_id = int(output_list[1]) + address = output_list[3] + cmd_wlt_send2 = ("%s %s -w test_tools_transaction_options -d %s " + "-s blt1qdjre3yw9hnt53entkp6tflhg34y4sp999emjnk 0.5 -k %d") % \ + (self.python_executable, self.clw_executable, self.database_uri, first_key_id) + output = normalize_string(Popen(cmd_wlt_send2, stdin=PIPE, stdout=PIPE, shell=True).communicate()[0]) + self.assertIn(address, output) + self.assertIn("Transaction created", output) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_transactions.py b/tests/test_transactions.py index 7e99511d..3be30dc4 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -2,7 +2,7 @@ # # BitcoinLib - Python Cryptocurrency Library # Unit Tests for Transaction Class -# © 2017 - 2022 November - 1200 Web Development +# © 2017 - 2024 March - 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 @@ -45,7 +45,7 @@ def test_transaction_input_add_scriptsig(self): b"\x15,\x03\x02 \x16\x170?c\x8e\x08\x94\x7f\x18i~\xdc\xb3\xa7\xa5:\xe6m\xf9O&)\xdb\x98\xdc\x0c\xc5\x07k4" \ b"\xb7\x01!\x020\x9a\x19i\x19\xcf\xf1\xd1\x87T'\x1b\xe7\xeeT\xd1\xb3\x7fAL\xbb)+U\xd7\xed\x1f\r\xc8 \x9d" \ b"\x13" - ti = Input(prev_txid, output_index, unlocking_script=unlock_scr) + ti = Input(prev_txid, output_index, unlocking_script=unlock_scr, witness_type='legacy') expected_dict = { 'output_n': 0, 'script': '47304402206ca28f7bafdd65bdfc0fbd88f5a5b003699127caf0fff6e65535d7f131152c0302201617' @@ -71,18 +71,10 @@ def test_transaction_input_add_public_key(self): ti = Input(prev_txid=ph, output_n=1, keys=k.public(), compressed=k.compressed) self.assertEqual('16UwLL9Risc3QfPqBUvKofHmBQ7wMtjvM', ti.keys[0].address()) - def test_transaction_input_with_pkh(self): - ki = Key('cTuDU2P6AhB72ZrhHRnFTcZRoHdnoWkp7sSMPCBnrMG23nRNnjUX', network='dash_testnet', compressed=False) - prev_tx = "5b5903a9e5f5a1fee68fbd597085969a36789dc5b5e397dad76a57c3fb7c232a" - output_n = 0 - ki_public_hash = ki.hash160 - ti = Input(prev_txid=prev_tx, output_n=output_n, public_hash=ki_public_hash, network='dash_testnet', - compressed=False) - self.assertEqual(ti.address, 'yWut2kHY6nXbpgqatMCNkwsxoYHcpWeF6Q') - def test_transaction_input_locking_script(self): ph = "81b4c832d70cb56ff957589752eb4125a4cab78a25a8fc52d6a09e5bd4404d48" - ti = Input(ph, 0, unlocking_script_unsigned='76a91423e102597c4a99516f851406f935a6e634dbccec88ac') + ti = Input(ph, 0, locking_script='76a91423e102597c4a99516f851406f935a6e634dbccec88ac', + witness_type='legacy') self.assertEqual(ti.address, '14GiCdJHj3bznWpcocjcu9ByCmDPEhEoP8') def test_transaction_compressed_mixup_error(self): @@ -103,55 +95,50 @@ def test_transaction_hash_type(self): self.assertTrue(t.verify()) self.assertEqual(t.inputs[0].hash_type, 0x81) - # TODO: Move and rewrite - # def test_transaction_input_locktime(self): - # rawtx = '0200000002f42e4ee59d33dffc39978bd6f7a1fdef42214b7de7d6d2716b2a5ae0a92fbb09000000006a473044' \ - # '022003ea734e54ddc00d4d681e2cac9ecbedb45d24af307aefbc55ecb005c5d2dc13022054d5a0fdb7a0c3ae7b' \ - # '161ffb654be7e89c84de06013d416f708f85afe11845a601210213692eb7eb74a0f86284890885629f2d097733' \ - # '7376868b033029ba49cc64765dfdffffff27a321a0e098276e3dce7aedf33a633db31bf34262bde3fe30106a32' \ - # '7696a70a000000006a47304402207758c05e849310af174ad4d484cdd551d66244d4cf0b5bba84e94d59eb8d3c' \ - # '9b02203e005ef10ede62db1900ed0bc2c72c7edd83ef98a21a3c567b4c6defe8ffca06012103ab51db28d30d3a' \ - # 'c99965a5405c3d473e25dff6447db1368e9191229d6ec0b635fdffffff029b040000000000001976a91406d66a' \ - # 'dea8ca6fcbb4a7a5f18458195c869f4b5488ac307500000000000017a9140614a615ee10d84a1e6d85ec1ff7ff' \ - # 'f527757d5987ffffffff' - # t = Transaction.parse_hex(rawtx) - # t.inputs[0].set_locktime_relative_time(1000) - # self.assertEqual(t.inputs[0].sequence, 4194305) - # t.inputs[0].set_locktime_relative_time(0) - # self.assertEqual(t.inputs[0].sequence, 0xffffffff) - # t.inputs[0].set_locktime_relative_time(100) - # self.assertEqual(t.inputs[0].sequence, 4194305) - # t.inputs[0].set_locktime_relative_blocks(120) - # self.assertEqual(t.inputs[0].sequence, 120) - # t.inputs[0].set_locktime_relative_blocks(0) - # self.assertEqual(t.inputs[0].sequence, 0xffffffff) + def test_transaction_input_with_pkh(self): + ki = Key('cTuDU2P6AhB72ZrhHRnFTcZRoHdnoWkp7sSMPCBnrMG23nRNnjUX', network='bitcoin', + compressed=False) + prev_tx = "5b5903a9e5f5a1fee68fbd597085969a36789dc5b5e397dad76a57c3fb7c232a" + output_n = 0 + ki_public_hash = ki.hash160 + ti = Input(prev_txid=prev_tx, output_n=output_n, public_hash=ki_public_hash, network='bitcoin', + compressed = False, witness_type='legacy') + self.assertEqual(ti.address, '1BbSBYZChXewL1KTTcZksPmpgvDZH93wtt') class TestTransactionOutputs(unittest.TestCase): - def test_transaction_output_add_address(self): + def test_transaction_output_address(self): to = Output(1000, '1QhnmvncrbZFkjt5R8hs8yHDM7xXX3feg') self.assertEqual(b'v\xa9\x14\x04{\x9d\xc2=\xda\xa9\x17\x1e\xa5\x11\xe1\x93t\xabUo\xaa\xbbD\x88\xac', to.lock_script) self.assertEqual(repr(to), '') - def test_transaction_output_add_address_p2sh(self): + def test_transaction_output_address_p2sh(self): to = Output(1000, '2N5WPJ2qPzVpy5LeE576JCwZfWg1ikjUxdK', network='testnet') self.assertEqual(b'\xa9\x14\x86\x7f\x84`u\x87\xf7\xc2\x05G@\xc6\xca\xe0\x92\x98\xcc\xbc\xd5(\x87', to.lock_script) - def test_transaction_output_add_public_key(self): - to = Output(1000000000, public_key='0450863AD64A87AE8A2FE83C1AF1A8403CB53F53E486D8511DAD8A04887E5B23522CD470' - '243453A299FA9E77237716103ABC11A1DF38855ED6F2EE187E9C582BA6') + def test_transaction_output_public_key_legacy(self): + to = Output(1000000000, public_key='0450863AD64A87AE8A2FE83C1AF1A8403CB53F53E486D8511DAD8A04887E5B23522' + 'CD470243453A299FA9E77237716103ABC11A1DF38855ED6F2EE187E9C582BA6', + witness_type='legacy') self.assertEqual(b"v\xa9\x14\x01\tfw`\x06\x95=UgC\x9e^9\xf8j\r';\xee\x88\xac", to.lock_script) - def test_transaction_output_add_public_key_hash(self): - to = Output(1000, public_hash='010966776006953d5567439e5e39f86a0d273bee') + def test_transaction_output_public_key(self): + to = Output(1000000000, public_key='0450863AD64A87AE8A2FE83C1AF1A8403CB53F53E486D8511DAD8A04887E5B23522' + 'CD470243453A299FA9E77237716103ABC11A1DF38855ED6F2EE187E9C582BA6') + self.assertEqual(b"\x00\x14\x01\tfw`\x06\x95=UgC\x9e^9\xf8j\r';\xee", + to.lock_script) + self.assertEqual('segwit', to.witness_type) + + def test_transaction_output_public_key_hash(self): + to = Output(1000, public_hash='010966776006953d5567439e5e39f86a0d273bee', witness_type='legacy') self.assertEqual(b"v\xa9\x14\x01\tfw`\x06\x95=UgC\x9e^9\xf8j\r';\xee\x88\xac", to.lock_script) - def test_transaction_output_add_script(self): + def test_transaction_output_script(self): to = Output(1000, lock_script='76a91423e102597c4a99516f851406f935a6e634dbccec88ac') self.assertEqual('14GiCdJHj3bznWpcocjcu9ByCmDPEhEoP8', to.address) @@ -161,6 +148,22 @@ def test_transaction_output_value(self): self.assertRaisesRegex(ValueError, "Value uses different network \(bitcoin\) then supplied network: testnet", Output, '1 BTC', address=HDKey(network='testnet').address(), network='testnet') + def test_transaction_output_witness_types(self): + k = HDKey(witness_type='segwit') + o = Output(10000, k) + self.assertEqual(o.witness_type, 'segwit') + self.assertEqual(o.script_type, 'p2wpkh') + + k = HDKey(witness_type='legacy') + o = Output(10000, k) + self.assertEqual(o.witness_type, 'legacy') + self.assertEqual(o.script_type, 'p2pkh') + + k = HDKey() + o = Output(10000, k) + self.assertEqual(o.witness_type, 'segwit') + self.assertEqual(o.script_type, 'p2wpkh') + class TestTransactions(unittest.TestCase): def setUp(self): @@ -216,29 +219,17 @@ def test_transactions_deserialize_errors(self): self.assertRaisesRegex(TransactionError, 'Input transaction hash not found. Probably malformed raw transaction', Transaction.parse_hex, rawtx) - # FIXME: tx.parse_hex() should check remaining size - # rawtx = '01000000000101c114c54564ea09b33c73bfd0237a4d283fe9e73285ad6d34fd3fa42c99f194640300000000ffffffff0200' \ - # 'e1f5050000000017a914e10a445f3084bd131394c66bf0023653dcc247ab877cdb3b0300000000220020701a8d401c84fb13' \ - # 'e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d04004830450221009c5bd2fa1acb5884fca1612217bd65992c96' \ - # 'c839accea226a3c59d7cc28779c502202cff98a71d195ab61c08fc126577466bb05ae0bfce5554b59455bd758309d4950148' \ - # '3045022100f81ce75339657d31698793e78f475c04fe56bafdb3cfc6e1035846aeeeb98f7902203ad5b1bcb96494457197cb' \ - # '3c12b67ddd3cf8127fe054dec971c858252c004bf8016952210375e00eb72e29da82b89367947f29ef34afb75e8654f6ea36' \ - # '8e0acdfd92976b7c2103a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70eff01874496feff2103c96d495bfdd5' \ - # 'ba4145e3e046fee45e84a8a48ad05bd8dbb395c011a32cf9f88053ae000000' - # self.assertRaisesRegex(TransactionError, - # 'Error when deserializing raw transaction, bytes left for locktime must be 4 not 3', - # Transaction.parse, rawtx) - # rawtx = '01000000000101c114c54564ea09b33c73bfd0237a4d283fe9e73285ad6d34fd3fa42c99f194640300000000ffffffff0200' \ - # 'e1f5050000000017a914e10a445f3084bd131394c66bf0023653dcc247ab877cdb3b0300000000220020701a8d401c84fb13' \ - # 'e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d04004830450221009c5bd2fa1acb5884fca1612217bd65992c96' \ - # 'c839accea226a3c59d7cc28779c502202cff98a71d195ab61c08fc126577466bb05ae0bfce5554b59455bd758309d4950148' \ - # '3045022100f81ce75339657d31698793e78f475c04fe56bafdb3cfc6e1035846aeeeb98f7902203ad5b1bcb96494457197cb' \ - # '3c12b67ddd3cf8127fe054dec971c858252c004bf8016952210375e00eb72e29da82b89367947f29ef34afb75e8654f6ea36' \ - # '8e0acdfd92976b7c2103a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70f01874496feff2103c96d495bfdd5' \ - # 'ba4145e3e046fee45e84a8a48ad05bd8dbb395c011a32cf9f88053ae00000000' - # self.assertRaisesRegex(TransactionError, - # "Error when deserializing raw transaction, bytes left for locktime must be 4 not 3", - # Transaction.parse, rawtx) + rawtx = '01000000000101c114c54564ea09b33c73bfd0237a4d283fe9e73285ad6d34fd3fa42c99f194640300000000ffffffff0200' \ + 'e1f5050000000017a914e10a445f3084bd131394c66bf0023653dcc247ab877cdb3b0300000000220020701a8d401c84fb13' \ + 'e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d04004830450221009c5bd2fa1acb5884fca1612217bd65992c96' \ + 'c839accea226a3c59d7cc28779c502202cff98a71d195ab61c08fc126577466bb05ae0bfce5554b59455bd758309d4950148' \ + '3045022100f81ce75339657d31698793e78f475c04fe56bafdb3cfc6e1035846aeeeb98f7902203ad5b1bcb96494457197cb' \ + '3c12b67ddd3cf8127fe054dec971c858252c004bf8016952210375e00eb72e29da82b89367947f29ef34afb75e8654f6ea36' \ + '8e0acdfd92976b7c2103a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70eff01874496feff2103c96d495bfdd5' \ + 'ba4145e3e046fee45e84a8a48ad05bd8dbb395c011a32cf9f88053ae000000' + self.assertRaisesRegex(TransactionError, + 'Invalid transaction size, locktime bytes incomplete', + Transaction.parse, rawtx) def test_transactions_verify_signature(self): for r in self.rawtxs: @@ -256,10 +247,10 @@ def test_transactions_serialize_raw(self): def test_transactions_sign_1(self): pk = Key('cR6pgV8bCweLX1JVN3Q1iqxXvaw4ow9rrp8RenvJcckCMEbZKNtz', network='testnet') # Private key for import inp = Input(prev_txid='d3c7fbd3a4ca1cca789560348a86facb3bb21dcd75ed38e85235fb6a32802955', output_n=1, - keys=pk.public(), network='testnet') + keys=pk.public(), network='testnet', witness_type='legacy') # key for address mkzpsGwaUU7rYzrDZZVXFne7dXEeo6Zpw2 pubkey = Key('0391634874ffca219ff5633f814f7f013f7385c66c65c8c7d81e7076a5926f1a75', network='testnet') - out = Output(880000, public_hash=pubkey.hash160, network='testnet') + out = Output(880000, public_hash=pubkey.hash160, network='testnet', witness_type='legacy') t = Transaction([inp], [out], network='testnet') t.sign(pk) self.assertTrue(t.verify(), msg="Can not verify transaction '%s'") @@ -269,7 +260,7 @@ def test_transactions_sign_1(self): def test_transactions_sign_2(self): pk = Key('KwbbBb6iz1hGq6dNF9UsHc7cWaXJZfoQGFWeozexqnWA4M7aSwh4') # Private key for import inp = Input(prev_txid='fdaa42051b1fc9226797b2ef9700a7148ee8be9466fc8408379814cb0b1d88e3', - output_n=1, keys=pk.public()) + output_n=1, keys=pk.public(), witness_type='legacy') out = Output(95000, address='1K5j3KpsSt2FyumzLmoVjmFWVcpFhXHvNF') t = Transaction([inp], [out]) t.sign(pk) @@ -294,8 +285,8 @@ def test_transactions_sign_multiple_inputs(self): utxo_hash = '0177ac29fa8b2960051321c730c6f15017503aa5b9c1dd2d61e7286e366fbaba' pk1 = HDKey(wif1) pk2 = HDKey(wif2) - input1 = Input(prev_txid=utxo_hash, output_n=0, keys=pk1.public_byte, index_n=0) - input2 = Input(prev_txid=utxo_hash, output_n=1, keys=pk2.public_byte, index_n=1) + input1 = Input(prev_txid=utxo_hash, output_n=0, keys=pk1.public_byte, index_n=0, witness_type='legacy') + input2 = Input(prev_txid=utxo_hash, output_n=1, keys=pk2.public_byte, index_n=1, witness_type='legacy') # Create a transaction with 2 inputs, and add 2 outputs below osm_address = '1J3pt9koWJZTo2jarg98RL89iJqff9Kobp' @@ -336,17 +327,19 @@ def test_transaction_parse_short_signature(self): self.assertTrue(t.verify()) def test_transactions_estimate_size_p2pkh(self): - t = Transaction() + t = Transaction(witness_type='legacy') t.add_output(2710000, '1Khyc5eUddbhYZ8bEZi9wiN8TrmQ8uND4j') t.add_output(2720000, '1D1gLEHsvjunpJxqjkWcPZqU4QzzRrHDdL') - t.add_input('82b48b128232256d1d5ce0c6ae7f7897f2b464d44456c25d7cf2be51626530d9', 0) + t.add_input('82b48b128232256d1d5ce0c6ae7f7897f2b464d44456c25d7cf2be51626530d9', 0, + witness_type='legacy') self.assertEqual(t.estimate_size(), 227) def test_transactions_estimate_size_nulldata(self): - t = Transaction() + t = Transaction(witness_type='legacy') lock_script = b'j' + varstr(b'Please leave a message after the beep') t.add_output(0, lock_script=lock_script) - t.add_input('82b48b128232256d1d5ce0c6ae7f7897f2b464d44456c25d7cf2be51626530d9', 0) + t.add_input('82b48b128232256d1d5ce0c6ae7f7897f2b464d44456c25d7cf2be51626530d9', 0, + witness_type='legacy') self.assertEqual(t.estimate_size(number_of_change_outputs=1), 241) def test_transaction_very_large(self): @@ -1042,7 +1035,7 @@ def test_transaction_create_with_address_objects(self): transaction_output = Output(value=91234, address=addr) t = Transaction([transaction_input], [transaction_output]) self.assertEqual(t.inputs[0].address, "1MMMMSUb1piy2ufrSguNUdFmAcvqrQF8M5") - self.assertEqual(t.outputs[0].address, "1KKKK6N21XKo48zWKuQKXdvSsCf95ibHFa") + self.assertEqual(t.outputs[0].address, "bc1qer5sn9k8ccyqacrzs3sqc6zwmyzdznzupzevph") def test_transaction_info(self): t = Transaction() @@ -1058,42 +1051,32 @@ def test_transaction_errors(self): class TestTransactionsScripts(unittest.TestCase, CustomAssertions): - def test_transaction_redeemscript_errors(self): - exp_error = "Redeemscripts with more then 15 keys are non-standard and could result in locked up funds" - keys = [] - for n in range(20): - keys.append(HDKey().public_hex) - self.assertRaisesRegex(TransactionError, exp_error, serialize_multisig_redeemscript, keys) - - def test_transaction_script_type_string(self): - # Locking script - s = bytes.fromhex('5121032487c2a32f7c8d57d2a93906a6457afd00697925b0e6e145d89af6d3bca330162102308673d169' - '87eaa010e540901cc6fe3695e758c19f46ce604e174dac315e685a52ae') - os = "OP_1 032487c2a32f7c8d57d2a93906a6457afd00697925b0e6e145d89af6d3bca33016 " \ - "02308673d16987eaa010e540901cc6fe3695e758c19f46ce604e174dac315e685a OP_2 OP_CHECKMULTISIG" - self.assertEqual(os, str(script_to_string(s))) - # Signature unlocking script - sig = '304402203359857b3bc3409c161a3b9570306bde53f21a15fcf3d3946d8ddfc94dd6ff35022024dc076c7014ee199831079cc0f' \ - 'df5e55aeebee7e90f4d51a2d923cc57f9173a01' - self.assertEqual(script_to_string(sig), sig) - # Multisig redeemscript - script = '52210294d7bf6363ab715168e812dd5b64d1f503ba707746b55535b7ee8afadd979c0e21024b68079ccf41b9df944f4aa37' \ - '7a2431a8df6efd7d7939d1f4d4f17376dc3434d21028885aad1fe0ad25ba2d9a0917a415f035e83e2c1a149904006f2d1dd' \ - '63676d0e53ae' - script_string = 'OP_2 0294d7bf6363ab715168e812dd5b64d1f503ba707746b55535b7ee8afadd979c0e ' \ - '024b68079ccf41b9df944f4aa377a2431a8df6efd7d7939d1f4d4f17376dc3434d ' \ - '028885aad1fe0ad25ba2d9a0917a415f035e83e2c1a149904006f2d1dd63676d0e OP_3 OP_CHECKMULTISIG' - self.assertEqual(script_to_string(script), script_string) - - def test_transaction_sign_uncompressed(self): - ki = Key('cTuDU2P6AhB72ZrhHRnFTcZRoHdnoWkp7sSMPCBnrMG23nRNnjUX', network='dash_testnet', compressed=False) - prev_tx = "5b5903a9e5f5a1fee68fbd597085969a36789dc5b5e397dad76a57c3fb7c232a" - output_n = 0 - t = Transaction(network='dash_testnet') - t.add_input(prev_txid=prev_tx, output_n=output_n, compressed=False) - t.add_output(99900000, 'yUV8W2RmEbKZD8oD7YMeBNiydHWmormCDj') - t.sign(ki.private_byte) - self.assertTrue(t.verify()) + # def test_transaction_redeemscript_errors(self): + # exp_error = "Redeemscripts with more than 15 keys are non-standard and could result in locked up funds" + # keys = [] + # for n in range(20): + # keys.append(HDKey().public_hex) + # self.assertRaisesRegexp(TransactionError, exp_error, serialize_multisig_redeemscript, keys) + + # def test_transaction_script_type_string(self): + # # Locking script + # s = bytes.fromhex('5121032487c2a32f7c8d57d2a93906a6457afd00697925b0e6e145d89af6d3bca330162102308673d169' + # '87eaa010e540901cc6fe3695e758c19f46ce604e174dac315e685a52ae') + # os = "OP_1 032487c2a32f7c8d57d2a93906a6457afd00697925b0e6e145d89af6d3bca33016 " \ + # "02308673d16987eaa010e540901cc6fe3695e758c19f46ce604e174dac315e685a OP_2 OP_CHECKMULTISIG" + # self.assertEqual(os, str(script_to_string(s))) + # # Signature unlocking script + # sig = '304402203359857b3bc3409c161a3b9570306bde53f21a15fcf3d3946d8ddfc94dd6ff35022024dc076c7014ee199831079cc0f' \ + # 'df5e55aeebee7e90f4d51a2d923cc57f9173a01' + # self.assertEqual(script_to_string(sig), sig) + # # Multisig redeemscript + # script = '52210294d7bf6363ab715168e812dd5b64d1f503ba707746b55535b7ee8afadd979c0e21024b68079ccf41b9df944f4aa37' \ + # '7a2431a8df6efd7d7939d1f4d4f17376dc3434d21028885aad1fe0ad25ba2d9a0917a415f035e83e2c1a149904006f2d1dd' \ + # '63676d0e53ae' + # script_string = 'OP_2 0294d7bf6363ab715168e812dd5b64d1f503ba707746b55535b7ee8afadd979c0e ' \ + # '024b68079ccf41b9df944f4aa377a2431a8df6efd7d7939d1f4d4f17376dc3434d ' \ + # '028885aad1fe0ad25ba2d9a0917a415f035e83e2c1a149904006f2d1dd63676d0e OP_3 OP_CHECKMULTISIG' + # self.assertEqual(script_to_string(script), script_string) def test_transaction_p2pk_script(self): rawtx = '0100000001db1a1774240cb1bd39d6cd6df0c57d5624fd2bd25b8b1be471714ab00e1a8b5d00000000484730440220592ce8' \ @@ -1105,6 +1088,30 @@ def test_transaction_p2pk_script(self): self.assertEqual(t.inputs[0].script_type, 'signature') self.assertEqual(t.outputs[0].script_type, 'p2pk') + wif = 'tprv8ZgxMBicQKsPdx411rqb5SjGvY43Bjc2PyhU2UCVtbEwCDSyKzHhaM88XaKHe5LcyNVdwWgG9NBut4oytRLbhr7iHbJ7KxioG' \ + 'nQETYvZu3j' + k = HDKey(wif) + rawtx = ('0100000001cb7b368efcf5f17b09e9e43ec3907cbed622a5b4b33addb4c9c6f0b8ce855c9f0000000047463043021f52f0278' + '8988b941e3b810357762ccea5148e405edf124ea6b3b7eb9eba15430220609a9261612aaaa7544b7dae347b5dc3e53b0fc304' + '957d6c4a46e1ae90a5d30001ffffffff01581b00000000000023210312ed54eee6c84b440dd90623a714360196bebd842bfa6' + '4c7c7767b71b92a238dac00000000') + t = Transaction.parse_hex(rawtx, network='testnet') + t.inputs[0].keys = [k.public()] + t.update_inputs(0) + t.sign_and_update() + self.assertTrue(t.verify()) + + def test_transaction_sign_uncompressed(self): + ki = Key('cTuDU2P6AhB72ZrhHRnFTcZRoHdnoWkp7sSMPCBnrMG23nRNnjUX', + compressed=False) + prev_tx = "5b5903a9e5f5a1fee68fbd597085969a36789dc5b5e397dad76a57c3fb7c232a" + output_n = 0 + t = Transaction() + t.add_input(prev_txid=prev_tx, output_n=output_n, compressed=False, witness_type='legacy') + t.add_output(99900000, '1EHmhQH4HjJF7e4tyX61PVzzVevRJfsPMg') + t.sign(ki.private_byte) + self.assertTrue(t.verify()) + def test_transaction_sign_p2pk(self): wif = 'tprv8ZgxMBicQKsPdx411rqb5SjGvY43Bjc2PyhU2UCVtbEwCDSyKzHhaM88XaKHe5LcyNVdwWgG9NBut4oytRLbhr7iHbJ7KxioG' \ 'nQETYvZu3j' @@ -1113,13 +1120,18 @@ def test_transaction_sign_p2pk(self): output_n = 0 value = 9000 - inp = Input(prev_txid, output_n, k, value=value, network='testnet', script_type='signature') + inp = Input(prev_txid, output_n, k, value=value, network='testnet', script_type='signature', + witness_type='legacy') outp = Output(7000, k, network='testnet', script_type='p2pk') - t = Transaction([inp], [outp], network='testnet') + t = Transaction([inp], [outp], network='testnet', witness_type='legacy') t.sign(k.private_byte) self.assertTrue(t.verify()) self.assertEqual(t.signature_hash(sign_id=0).hex(), '67b94bf5a5c17a5f6b2bedbefc51a17db669ce7ff3bbbc4943cfd876d68df986') + t.sign_and_update() + if USE_FASTECDSA: + self.assertEqual(t.txid, "a3e18689f2b03659ae10735e332277e451b4270dbc46072b196baf63fb9a838b") + def test_transaction_sign_p2pk_value(self): wif = 'tprv8ZgxMBicQKsPdx411rqb5SjGvY43Bjc2PyhU2UCVtbEwCDSyKzHhaM88XaKHe5LcyNVdwWgG9NBut4oytRLbhr7iHbJ7KxioG' \ @@ -1130,51 +1142,52 @@ def test_transaction_sign_p2pk_value(self): value = Value.from_satoshi(9000, network='testnet') fee = 0.00002000 - inp = Input(prev_txid, output_n, k, value=value, network='testnet', script_type='signature') + inp = Input(prev_txid, output_n, k, value=value, network='testnet', script_type='signature', + witness_type='legacy') outp = Output(value - fee, k, network='testnet', script_type='p2pk') - t = Transaction([inp], [outp], network='testnet') + t = Transaction([inp], [outp], network='testnet', witness_type='legacy') t.sign(k.private_byte) self.assertTrue(t.verify()) self.assertEqual(t.signature_hash(sign_id=0).hex(), '67b94bf5a5c17a5f6b2bedbefc51a17db669ce7ff3bbbc4943cfd876d68df986') - def test_transaction_locktime(self): - # FIXME: Add more useful unittests for locktime - s = bytes.fromhex('76a914af8e14a2cecd715c363b3a72b55b59a31e2acac988ac') - s_cltv = script_add_locktime_cltv(10000, s) - s_csv = script_add_locktime_csv(600000, s) - self.assertIsNotNone(s_cltv) - self.assertIsNotNone(s_csv) - # Test deserialize locktime transactions - rawtx = '0200000002f42e4ee59d33dffc39978bd6f7a1fdef42214b7de7d6d2716b2a5ae0a92fbb09000000006a473044022003ea7' \ - '34e54ddc00d4d681e2cac9ecbedb45d24af307aefbc55ecb005c5d2dc13022054d5a0fdb7a0c3ae7b161ffb654be7e89c84' \ - 'de06013d416f708f85afe11845a601210213692eb7eb74a0f86284890885629f2d0977337376868b033029ba49cc64765df' \ - 'dffffff27a321a0e098276e3dce7aedf33a633db31bf34262bde3fe30106a327696a70a000000006a47304402207758c05e' \ - '849310af174ad4d484cdd551d66244d4cf0b5bba84e94d59eb8d3c9b02203e005ef10ede62db1900ed0bc2c72c7edd83ef9' \ - '8a21a3c567b4c6defe8ffca06012103ab51db28d30d3ac99965a5405c3d473e25dff6447db1368e9191229d6ec0b635fdff' \ - 'ffff029b040000000000001976a91406d66adea8ca6fcbb4a7a5f18458195c869f4b5488ac307500000000000017a914061' \ - '4a615ee10d84a1e6d85ec1ff7fff527757d5987b0cc0800' - t = Transaction.parse_hex(rawtx) - self.assertEqual(t.locktime, 576688) - rawtx = '010000000159dc9ad3dc18cd76827f107a50fd96981e323aec7be4cbf982df176b9ab64f4900000000fd170147304402207' \ - '97987a17ee28181a94437e20c60b9d8da8974e68f91f250c424b623f06aeea9022036faa2834da6f883078abc3dd2fb48c1' \ - '9fc17097aa5b87fa11d00385fd21740b0121025c8ee352e8b0d12aecd8b3d9ac3bd93cae1b2cc5de7ac56c2995ab506ac80' \ - '0bd206a9068119b30840206281418227f33f76c53c43fa59fad748d2954e6ecd595a94c8aa6140d424014e59608dae01e97' \ - '700da0b53b3095a1af882102ef7f775819d4518c67c904201e30d4181190552f0026db94f93bfde557e23d1187632102ef' \ - '7f775819d4518c67c904201e30d4181190552f0026db94f93bfde557e23d11ac670475f2df5cb17521025c8ee352e8b0d12' \ - 'aecd8b3d9ac3bd93cae1b2cc5de7ac56c2995ab506ac800bdac68feffffff011f000200000000001976a91436963a21b49f' \ - '701acf03dd1e778ab5774017b53c88ac75f2df5c' - t = Transaction.parse_hex(rawtx) - self.assertEqual(t.locktime, 1558180469) - # Input level locktimes - t = Transaction() - t.add_input('f601e39f6b99b64fc2e98beb706ec7f14d114db7e61722c0313b0048df49453e', 0, locktime_cltv=10000) - t.add_input('f601e39f6b99b64fc2e98beb706ec7f14d114db7e61722c0313b0048df494511', 0, locktime_csv=20000) - t.add_input('f601e39f6b99b64fc2e98beb706ec7f14d114db7e61722c0313b0048df494522', 0, - locktime_csv=SEQUENCE_LOCKTIME_TYPE_FLAG + 30000) - t.add_input('f601e39f6b99b64fc2e98beb706ec7f14d114db7e61722c0313b0048df494533', 0, - locktime_csv=SEQUENCE_LOCKTIME_TYPE_FLAG + 40000) - self.assertIsNone(t.info()) + # def test_transaction_locktime(self): + # # FIXME: Add more useful unittests for locktime + # s = bytes.fromhex('76a914af8e14a2cecd715c363b3a72b55b59a31e2acac988ac') + # s_cltv = script_add_locktime_cltv(10000, s) + # s_csv = script_add_locktime_csv(600000, s) + # self.assertIsNotNone(s_cltv) + # self.assertIsNotNone(s_csv) + # # Test deserialize locktime transactions + # rawtx = '0200000002f42e4ee59d33dffc39978bd6f7a1fdef42214b7de7d6d2716b2a5ae0a92fbb09000000006a473044022003ea7' \ + # '34e54ddc00d4d681e2cac9ecbedb45d24af307aefbc55ecb005c5d2dc13022054d5a0fdb7a0c3ae7b161ffb654be7e89c84' \ + # 'de06013d416f708f85afe11845a601210213692eb7eb74a0f86284890885629f2d0977337376868b033029ba49cc64765df' \ + # 'dffffff27a321a0e098276e3dce7aedf33a633db31bf34262bde3fe30106a327696a70a000000006a47304402207758c05e' \ + # '849310af174ad4d484cdd551d66244d4cf0b5bba84e94d59eb8d3c9b02203e005ef10ede62db1900ed0bc2c72c7edd83ef9' \ + # '8a21a3c567b4c6defe8ffca06012103ab51db28d30d3ac99965a5405c3d473e25dff6447db1368e9191229d6ec0b635fdff' \ + # 'ffff029b040000000000001976a91406d66adea8ca6fcbb4a7a5f18458195c869f4b5488ac307500000000000017a914061' \ + # '4a615ee10d84a1e6d85ec1ff7fff527757d5987b0cc0800' + # t = Transaction.parse_hex(rawtx) + # self.assertEqual(t.locktime, 576688) + # rawtx = '010000000159dc9ad3dc18cd76827f107a50fd96981e323aec7be4cbf982df176b9ab64f4900000000fd170147304402207' \ + # '97987a17ee28181a94437e20c60b9d8da8974e68f91f250c424b623f06aeea9022036faa2834da6f883078abc3dd2fb48c1' \ + # '9fc17097aa5b87fa11d00385fd21740b0121025c8ee352e8b0d12aecd8b3d9ac3bd93cae1b2cc5de7ac56c2995ab506ac80' \ + # '0bd206a9068119b30840206281418227f33f76c53c43fa59fad748d2954e6ecd595a94c8aa6140d424014e59608dae01e97' \ + # '700da0b53b3095a1af882102ef7f775819d4518c67c904201e30d4181190552f0026db94f93bfde557e23d1187632102ef' \ + # '7f775819d4518c67c904201e30d4181190552f0026db94f93bfde557e23d11ac670475f2df5cb17521025c8ee352e8b0d12' \ + # 'aecd8b3d9ac3bd93cae1b2cc5de7ac56c2995ab506ac800bdac68feffffff011f000200000000001976a91436963a21b49f' \ + # '701acf03dd1e778ab5774017b53c88ac75f2df5c' + # t = Transaction.parse_hex(rawtx) + # self.assertEqual(t.locktime, 1558180469) + # # Input level locktimes + # t = Transaction() + # t.add_input('f601e39f6b99b64fc2e98beb706ec7f14d114db7e61722c0313b0048df49453e', 0, locktime_cltv=10000) + # t.add_input('f601e39f6b99b64fc2e98beb706ec7f14d114db7e61722c0313b0048df494511', 0, locktime_csv=20000) + # t.add_input('f601e39f6b99b64fc2e98beb706ec7f14d114db7e61722c0313b0048df494522', 0, + # locktime_csv=SEQUENCE_LOCKTIME_TYPE_FLAG + 30000) + # t.add_input('f601e39f6b99b64fc2e98beb706ec7f14d114db7e61722c0313b0048df494533', 0, + # locktime_csv=SEQUENCE_LOCKTIME_TYPE_FLAG + 40000) + # self.assertIsNone(t.info()) def test_transaction_get_unlocking_script_type(self): self.assertEqual(get_unlocking_script_type('p2pk'), 'signature') @@ -1191,7 +1204,7 @@ def test_transaction_equal(self): t2 = Transaction([Input('a8d4eb4ba80e5cc87fc38c0a7df44461e995fb021ed33bfd5ecf0d12137fb85e', 0, '033c152137b251654f971bc4b2335646186e1298bfcbb0b7608ec33609ac08cc6f', '1e989d20c6f25bd36df33ef0206398b0708069ac7d1d7cdb0cd756dcb05f4dcc3c6ad2ca53cd3b5b82fc6969' - 'e43f4825bded505e351e7b4a3492b5c6f0054c94')], + 'e43f4825bded505e351e7b4a3492b5c6f0054c94', witness_type='legacy')], [Output(3077, '1Bj8rNK1tRsic6TRgJgVc6vF5FMAZVicaN', '75a94834a58225d5aa1d0403f3c72f7d8b01b0dd', script_type='p2pkh')], ) @@ -1277,6 +1290,148 @@ def test_transaction_p2tr_input_litecoin(self): self.assertEqual(t.inputs[0].witnesses[1].hex(), witness_1) self.assertEqual(t.inputs[0].witnesses[2].hex(), witness_2) + def test_transaction_non_standard_input_script_0001(self): + txid = '38cf5779d1c5ca32b79cd5052b54e824102e878f041607d3b962038f5a8cf1ed' + traw = \ + '0100000001bf9fe5c8a75a849345150a323ce50466827e4df1f2626eac7e30122dd6d1a812000000000100ffffffff0180380100000000001976a9148f0da0329aa2638c17fda841347f2ed737b6e40088ac00000000' + t = Transaction.parse_hex(traw) + self.assertEqual(t.inputs[0].script_type, 'nonstandard_0001') + self.assertEqual(t.txid, txid) + self.assertEqual(traw, t.raw_hex()) + + def test_transaction_bumpfee(self): + prev_txid = '67f621f333f59492ac4652900bef1b803eb5d04b71dc363a815bbde0ffe374ab' + output_n = 0 + value = 100000 + + # Test 1 - bumpfee, extra_fee and remove output + inp = Input(prev_txid, output_n, address='zyJgheqe9HEXmmn7VP45dawA5P6BAYY4NG', value=value) + outputs = [ + Output(90000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), + Output(5000, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', change=True)] + t = Transaction([inp], outputs, network='bitcoinlib_test') + self.assertEqual(t.fee, 5000) + self.assertEqual(len(t.outputs), 2) + txid_before = t.txid + t.bumpfee(extra_fee=5000) + self.assertEqual(t.fee, 10000) + self.assertEqual(len(t.outputs), 1) + self.assertNotEqual(t.txid, txid_before) + self.assertEqual(sum([i.value for i in t.inputs]) - sum([o.value for o in t.outputs]), t.fee) + + # Test 2 - bumpfee, extra_fee, round dust and remove output + inp = Input(prev_txid, output_n, address='zyJgheqe9HEXmmn7VP45dawA5P6BAYY4NG', value=value) + outputs = [ + Output(90000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), + Output(5000, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', change=True)] + t = Transaction([inp], outputs, network='bitcoinlib_test') + self.assertEqual(t.fee, 5000) + self.assertEqual(len(t.outputs), 2) + txid_before = t.txid + t.bumpfee(extra_fee=4000) + self.assertEqual(t.fee, 10000) + self.assertEqual(len(t.outputs), 1) + self.assertNotEqual(t.txid, txid_before) + self.assertEqual(sum([i.value for i in t.inputs]) - sum([o.value for o in t.outputs]), t.fee) + + # Test 3 - bumpfee, fee and remove output + inp = Input(prev_txid, output_n, address='zyJgheqe9HEXmmn7VP45dawA5P6BAYY4NG', value=value) + outputs = [ + Output(90000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), + Output(5000, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', change=True)] + t = Transaction([inp], outputs, network='bitcoinlib_test') + self.assertEqual(t.fee, 5000) + self.assertEqual(len(t.outputs), 2) + txid_before = t.txid + t.bumpfee(10000) + self.assertEqual(t.fee, 10000) + self.assertEqual(len(t.outputs), 1) + self.assertNotEqual(t.txid, txid_before) + self.assertEqual(sum([i.value for i in t.inputs]) - sum([o.value for o in t.outputs]), t.fee) + + # Test 4 - bumpfee, fee, round dust and remove output + inp = Input(prev_txid, output_n, address='zyJgheqe9HEXmmn7VP45dawA5P6BAYY4NG', value=value) + outputs = [ + Output(90000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), + Output(5000, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', change=True)] + t = Transaction([inp], outputs, network='bitcoinlib_test') + self.assertEqual(t.fee, 5000) + self.assertEqual(len(t.outputs), 2) + txid_before = t.txid + t.bumpfee(10000) + self.assertEqual(t.fee, 10000) + self.assertEqual(len(t.outputs), 1) + self.assertNotEqual(t.txid, txid_before) + self.assertEqual(sum([i.value for i in t.inputs]) - sum([o.value for o in t.outputs]), t.fee) + + # Test 5 - bumpfee, 2 change outputs, no parameters + inp = Input(prev_txid, output_n, address='zyJgheqe9HEXmmn7VP45dawA5P6BAYY4NG', value=200000) + outputs = [ + Output(180000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), + Output(10000, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', + change=True), + Output(6667, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', + change=True)] + t = Transaction([inp], outputs, network='bitcoinlib_test') + t.sign_and_update() + self.assertEqual(t.fee, 3333) + txid_before = t.txid + t.bumpfee() + self.assertEqual(t.fee, 4598) + self.assertNotEqual(t.txid, txid_before) + self.assertEqual(sum([i.value for i in t.inputs]) - sum([o.value for o in t.outputs]), t.fee) + + # Test 6 - bumpfee, fee, 2 change outputs + inp = Input(prev_txid, output_n, address='zyJgheqe9HEXmmn7VP45dawA5P6BAYY4NG', value=200000) + outputs = [ + Output(180000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), + Output(10000, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', + change=True), + Output(6667, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', change=True)] + t = Transaction([inp], outputs, network='bitcoinlib_test') + t.sign_and_update() + self.assertEqual(t.fee, 3333) + self.assertEqual(len(t.outputs), 3) + t.bumpfee(fee=18000) + self.assertEqual(t.fee, 20000) + self.assertEqual(len(t.outputs), 1) + self.assertEqual(sum([i.value for i in t.inputs]) - sum([o.value for o in t.outputs]), t.fee) + + def test_transaction_bumpfee_errors(self): + prev_txid = '67f621f333f59492ac4652900bef1b803eb5d04b71dc363a815bbde0ffe374ab' + output_n = 0 + value = 100000 + + inp = Input(prev_txid, output_n, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', value=value) + outputs = [ + Output(90000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), + Output(100000, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', + change=True)] + t = Transaction([inp], outputs, network='bitcoinlib_test', fee=0) + self.assertEqual(t.fee, 0) + self.assertRaisesRegex(TransactionError, "Current transaction fee is zero, cannot increase fee", t.bumpfee) + + inp = Input(prev_txid, output_n, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', value=200000) + outputs = [ + Output(190000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), + Output(100, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', change=True)] + t = Transaction([inp], outputs, network='bitcoinlib_test', fee=9900) + self.assertRaisesRegex(TransactionError, "Not enough unspent outputs to bump transaction fee", t.bumpfee) + + inp = Input(prev_txid, output_n, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', value=200000) + outputs = [ + Output(190000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), + Output(500, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', change=True)] + t = Transaction([inp], outputs, network='bitcoinlib_test', fee=9500) + self.assertRaisesRegex(TransactionError, "Fee cannot be less than minimal required fee", t.bumpfee, fee=100) + + inp = Input(prev_txid, output_n, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', value=200000) + outputs = [ + Output(190000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), + Output(500, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', change=True)] + t = Transaction([inp], outputs, network='bitcoinlib_test', fee=9500) + self.assertRaisesRegex(TransactionError, "Extra fee cannot be less than minimal required fee", t.bumpfee, + extra_fee=100) class TestTransactionsMultisigSoroush(unittest.TestCase): # Source: Example from @@ -1293,7 +1448,7 @@ def test_transaction_multisig_p2sh_sign(self): t.add_output(55600, '18tiB1yNTzJMCg6bQS1Eh29dvJngq8QTfx') t.add_input('02b082113e35d5386285094c2829e7e2963fa0b5369fb7f4b79c4c90877dcd3d', 0, keys=[self.keylist[0], self.keylist[1], self.keylist[2]], script_type='p2sh_multisig', - sigs_required=2, compressed=False, sort=False) + sigs_required=2, compressed=False, sort=False, witness_type='legacy') pk1 = Key(self.keylist[0]).private_byte pk2 = Key(self.keylist[2]).private_byte t.sign([pk1, pk2]) @@ -1310,7 +1465,7 @@ def test_transaction_multisig_p2sh_sign_separate(self): pubk2 = Key(self.keylist[2]).public() t.add_input('02b082113e35d5386285094c2829e7e2963fa0b5369fb7f4b79c4c90877dcd3d', 0, keys=[pubk0, self.keylist[0], pubk2], script_type='p2sh_multisig', - sigs_required=2, compressed=False, sort=False) + sigs_required=2, compressed=False, sort=False, witness_type='legacy') pk1 = Key(self.keylist[0]).private_byte pk2 = Key(self.keylist[2]).private_byte t.sign([pk1]) @@ -1347,7 +1502,8 @@ def test_transaction_multisig_signature_redeemscript_mixup(self): # Create 2-of-2 multisig transaction with 1 input and 1 output t = Transaction(network='testnet') t.add_input('a2c226037d73022ea35af9609c717d98785906ff8b71818cd4095a12872795e7', 1, - [pk1.public_byte, pk2.public_byte], script_type='p2sh_multisig', sigs_required=2) + [pk1.public_byte, pk2.public_byte], script_type='p2sh_multisig', sigs_required=2, + witness_type='legacy') t.add_output(900000, '2NEgmZU64NjiZsxPULekrFcqdS7YwvYh24r') # Sign with private key and verify t.sign(pk1) @@ -1362,7 +1518,7 @@ def test_transaction_multisig_sign_3_of_5(self): t = Transaction(network='testnet') t.add_input(self.utxo_prev_tx, self.utxo_output_n, [self.pk1.public_byte, self.pk2.public_byte, self.pk3.public_byte, self.pk4.public_byte, - self.pk5.public_byte], script_type='p2sh_multisig', sigs_required=3) + self.pk5.public_byte], script_type='p2sh_multisig', sigs_required=3, witness_type='legacy') t.add_output(100000, 'mi1Lxs5boL6nDM3teraP3moVfLXJXWrWSK') t.add_output(self.utxo_tbtcleft - 110000, '2Mt1veesS36nYspXhkMXYKGHRAbtEYF6b8W') @@ -1374,10 +1530,10 @@ def test_transaction_multisig_sign_3_of_5(self): self.assertTrue(t.verify()) def test_transaction_multisig_sign_2_of_5_not_enough(self): - t = Transaction(network='testnet') + t = Transaction(network='testnet', witness_type='legacy') t.add_input(self.utxo_prev_tx, self.utxo_output_n, [self.pk1.public_byte, self.pk2.public_byte, self.pk3.public_byte, self.pk4.public_byte, - self.pk5.public_byte], script_type='p2sh_multisig', sigs_required=3) + self.pk5.public_byte], script_type='p2sh_multisig', sigs_required=3, witness_type='legacy') t.add_output(100000, 'mi1Lxs5boL6nDM3teraP3moVfLXJXWrWSK') t.add_output(self.utxo_tbtcleft - 110000, '2Mt1veesS36nYspXhkMXYKGHRAbtEYF6b8W') @@ -1391,7 +1547,7 @@ def test_transaction_multisig_sign_duplicate(self): t = Transaction(network='testnet') t.add_input(self.utxo_prev_tx, self.utxo_output_n, [self.pk1.public_byte, self.pk2.public_byte, self.pk3.public_byte, self.pk4.public_byte, - self.pk5.public_byte], script_type='p2sh_multisig', sigs_required=3) + self.pk5.public_byte], script_type='p2sh_multisig', sigs_required=3, witness_type='legacy') t.add_output(100000, 'mi1Lxs5boL6nDM3teraP3moVfLXJXWrWSK') t.add_output(self.utxo_tbtcleft - 110000, '2Mt1veesS36nYspXhkMXYKGHRAbtEYF6b8W') @@ -1405,7 +1561,7 @@ def test_transaction_multisig_sign_extra_sig(self): t = Transaction(network='testnet') t.add_input(self.utxo_prev_tx, self.utxo_output_n, [self.pk1.public_byte, self.pk2.public_byte, self.pk3.public_byte, self.pk4.public_byte, - self.pk5.public_byte], script_type='p2sh_multisig', sigs_required=3) + self.pk5.public_byte], script_type='p2sh_multisig', sigs_required=3, witness_type='legacy') t.add_output(100000, 'mi1Lxs5boL6nDM3teraP3moVfLXJXWrWSK') t.add_output(self.utxo_tbtcleft - 110000, '2Mt1veesS36nYspXhkMXYKGHRAbtEYF6b8W') @@ -1427,9 +1583,9 @@ def test_transaction_multisig_estimate_size(self): pk2 = HDKey.from_passphrase(phrase2, network=network) pk3 = HDKey.from_passphrase(phrase3, network=network) - t = Transaction(network=network) + t = Transaction(network=network, witness_type='legacy') t.add_input(prev_txid, 0, [pk1.private_byte, pk2.public_byte, pk3.public_byte], script_type='p2sh_multisig', - sigs_required=2) + sigs_required=2, witness_type='legacy') t.add_output(10000, '22zkxRGNsjHJpqU8tSS7cahSZVXrz9pJKSs') self.assertEqual(t.estimate_size(), 339) @@ -1441,28 +1597,13 @@ def test_transaction_multisig_litecoin(self): t = Transaction(network=network) t.add_input(self.utxo_prev_tx, self.utxo_output_n, [pk1.public_byte, pk2.public_byte, pk3.public_byte], - script_type='p2sh_multisig', sigs_required=2) + script_type='p2sh_multisig', sigs_required=2, witness_type='legacy') t.add_output(100000, 'LTK1nK5TyGALmSup5SzhgkX1cnVQrC4cLd') t.sign(pk1) self.assertFalse(t.verify()) t.sign(pk3) self.assertTrue(t.verify()) - def test_transaction_multisig_dash(self): - network = 'dash' - pk1 = HDKey(network=network) - pk2 = HDKey(network=network) - pk3 = HDKey(network=network) - t = Transaction(network=network) - t.add_input(self.utxo_prev_tx, self.utxo_output_n, - [pk1.public_byte, pk2.public_byte, pk3.public_byte], - script_type='p2sh_multisig', sigs_required=2) - t.add_output(100000, 'XwZcTpBnRRURenL7Jh9Z52XGTx1jhvecUt') - t.sign(pk1) - self.assertFalse(t.verify()) - t.sign(pk3) - self.assertTrue(t.verify()) - def test_transaction_multisig_same_sigs_for_keys(self): traw = '0100000001b4397ffe208657210147a452ca85f9a2c934f6be09a81fb19b6eb9b10310053501000000fdfe0000483045022' \ '100acff5e244831a294909567601e8851533c17cc8692201f4ee056920a522dbc050220730cdc85757564e0bacbe9ecf3f2' \ @@ -1473,7 +1614,7 @@ def test_transaction_multisig_same_sigs_for_keys(self): 'aeffffffff02a0acb903000000001976a9146170e2cc18a4415f807cc4b29c50e52bd1157c4b88ac787bb6030000000017a' \ '914bb87f55537ee62a232f042f39fbc0d86b77d07fb8700000000' t = Transaction.parse(bytes.fromhex(traw)) - t.inputs[0].value = 972612109 + t.inputs[0].value = 124798308 self.assertTrue(t.verify()) def test_transaction_multisig_1_key_15_signatures(self): @@ -1615,7 +1756,8 @@ def test_transaction_save_load_sign(self): t = Transaction(network='testnet') t.add_input('a2c226037d73022ea35af9609c717d98785906ff8b71818cd4095a12872795e7', 1, - [pk1.public_byte, pk2.public_byte], script_type='p2sh_multisig', sigs_required=2) + [pk1.public_byte, pk2.public_byte], script_type='p2sh_multisig', sigs_required=2, + witness_type='legacy') t.add_output(900000, '2NEgmZU64NjiZsxPULekrFcqdS7YwvYh24r') self.assertFalse(t.verify()) t.save() @@ -1625,6 +1767,28 @@ def test_transaction_save_load_sign(self): t2.sign(pk2) self.assertTrue(t2.verify()) + def test_transaction_set_locktimes(self): + rawtx = '0200000002f42e4ee59d33dffc39978bd6f7a1fdef42214b7de7d6d2716b2a5ae0a92fbb09000000006a473044' \ + '022003ea734e54ddc00d4d681e2cac9ecbedb45d24af307aefbc55ecb005c5d2dc13022054d5a0fdb7a0c3ae7b' \ + '161ffb654be7e89c84de06013d416f708f85afe11845a601210213692eb7eb74a0f86284890885629f2d097733' \ + '7376868b033029ba49cc64765dfdffffff27a321a0e098276e3dce7aedf33a633db31bf34262bde3fe30106a32' \ + '7696a70a000000006a47304402207758c05e849310af174ad4d484cdd551d66244d4cf0b5bba84e94d59eb8d3c' \ + '9b02203e005ef10ede62db1900ed0bc2c72c7edd83ef98a21a3c567b4c6defe8ffca06012103ab51db28d30d3a' \ + 'c99965a5405c3d473e25dff6447db1368e9191229d6ec0b635fdffffff029b040000000000001976a91406d66a' \ + 'dea8ca6fcbb4a7a5f18458195c869f4b5488ac307500000000000017a9140614a615ee10d84a1e6d85ec1ff7ff' \ + 'f527757d5987ffffffff' + t = Transaction.parse_hex(rawtx) + t.set_locktime_relative_time(1000) + self.assertEqual(t.inputs[0].sequence, 4194305) + t.set_locktime_relative_time(0) + self.assertEqual(t.inputs[0].sequence, 0xffffffff) + t.set_locktime_relative_time(100) + self.assertEqual(t.inputs[0].sequence, 4194305) + t.set_locktime_relative_blocks(120) + self.assertEqual(t.inputs[0].sequence, 120) + t.set_locktime_relative_blocks(0) + self.assertEqual(t.inputs[0].sequence, 0xffffffff) + def test_transaction_locktime_cltv(self): # timelock = 533600 # inputs = [ @@ -1635,7 +1799,7 @@ def test_transaction_locktime_cltv(self): pass # rawtx = '' # print(t.raw_hex()) - # print(t.inputs[0].unlocking_script_unsigned) + # print(t.inputs[0].locking_script) def test_transaction_cltv_error(self): # TODO @@ -1736,7 +1900,6 @@ def test_transactions_segwit_p2sh_p2wpkh(self): '64f3b0f4dd2bb3aa1ce8566d220cc74dda9df97d8490cc81d89d735c92e59fb6') t.sign([pk1], 0) self.assertTrue(t.verify()) - print(t.raw_hex()) t2 = Transaction.parse(t.raw()) t2.inputs[0].value = int(10 * 100000000) self.assertEqual(t2.signature_hash(0).hex(), @@ -1781,7 +1944,6 @@ def test_transaction_segwit_p2sh_p2wsh(self): t = Transaction(inputs, outputs, witness_type='segwit') t.sign(key2) self.assertTrue(t.verify()) - print(t.raw_hex()) self.assertEqual(t.signature_hash(0).hex(), '1926c08d8c0f54498382e97704e7b2d8b4181ffa524b4c0d8a43aba61e3fc656') self.assertEqual(t.txid, txid) @@ -1826,7 +1988,7 @@ def test_transaction_segwit_redeemscript_bug(self): keys=['0236ab0a160e381d2eb6f01119937d29e697b78ca8be115617db972e0d95cef6b7', '038b46795c92b8c9a6b40644a082be367e740f09b581a8b8c26b71d4b39f5cd963', '03f93f3a5633478ed3ab3ff9d2fe5ddeecb8e755fb267c9a3bc9d7242f58e4f258'], - unlocking_script_unsigned='52210236ab0a160e381d2eb6f01119937d29e697b78ca8be115617db972e0d95ce' + locking_script='52210236ab0a160e381d2eb6f01119937d29e697b78ca8be115617db972e0d95ce' 'f6b721038b46795c92b8c9a6b40644a082be367e740f09b581a8b8c26b71d4b39f' '5cd9632103f93f3a5633478ed3ab3ff9d2fe5ddeecb8e755fb267c9a3bc9d7242f' '58e4f25853ae', @@ -1882,3 +2044,4 @@ def test_transaction_segwit_vsize(self): t = Transaction.parse_hex(rawtx) self.assertEqual(t.vsize, 612) self.assertEqual(t.weight_units, 2445) + self.assertEqual(t.raw_hex(), rawtx) diff --git a/tests/test_values.py b/tests/test_values.py index b7b4a7fa..b0d50430 100644 --- a/tests/test_values.py +++ b/tests/test_values.py @@ -32,8 +32,6 @@ def test_value_class(self): self.assertEqual(str(Value('10')), '10.00000000 BTC') self.assertEqual(str(Value('10 ltc')), '10.00000000 LTC') self.assertEqual(str(Value('10', network='litecoin')), '10.00000000 LTC') - self.assertEqual(str(Value('10', network='dash_testnet')), '10.00000000 tDASH') - self.assertEqual(str(Value('10 tDASH')), '10.00000000 tDASH') self.assertEqual(float(Value('0.001 BTC')), 0.001) self.assertEqual(float(Value('1 msat')), 0.00000000001) self.assertEqual(int(Value('1 BTC')), 1) @@ -102,7 +100,7 @@ def test_value_class_str(self): self.assertEqual(Value('0.00021 YBTC').str(1), '210000000000000000000.00000000 BTC') self.assertEqual(Value('127127504620 Doge').str('TDoge'), '0.12712750 TDOGE') self.assertRaisesRegex(ValueError, "Denominator not found in NETWORK_DENOMINATORS definition", - Value('123 Dash').str, 'DD') + Value('123 Doge').str, 'DD') def test_value_class_str_auto(self): self.assertEqual(Value('1000000 sat').str('auto'), '0.01000000 BTC') @@ -139,7 +137,7 @@ def test_value_operators_comparison(self): self.assertTrue(v3 == '1000.00000 mBTC') self.assertTrue(v3 == '1 BTC') self.assertTrue(v3 == '100000000 sat') - self.assertFalse(v3 == '1 dash') + self.assertFalse(v3 == '1 doge') def test_value_operators_arithmetic(self): value1 = Value('3 BTC') diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 407581db..f7166465 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 - 2023 May - 1200 Web Development +# © 2016 - 2024 February - 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 @@ -19,14 +19,13 @@ # import unittest +import time from random import shuffle try: import mysql.connector - from parameterized import parameterized_class - import psycopg2 - from psycopg2 import sql - from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT + import psycopg + from psycopg import sql except ImportError as e: print("Could not import all modules. Error: %s" % e) # from psycopg2cffi import compat # Use for PyPy support @@ -44,106 +43,55 @@ DATABASE_NAME = 'bitcoinlib_test' DATABASE_NAME_2 = 'bitcoinlib2_test' -db_uris = ( - ('sqlite', 'sqlite:///' + DATABASEFILE_UNITTESTS, 'sqlite:///' + DATABASEFILE_UNITTESTS_2),) - -print("UNITTESTS_FULL_DATABASE_TEST: %s" % UNITTESTS_FULL_DATABASE_TEST) - -if UNITTESTS_FULL_DATABASE_TEST: - db_uris += ( - ('mysql', 'mysql://root:root@localhost:3306/' + DATABASE_NAME, - 'mysql://root:root@localhost:3306/' + DATABASE_NAME_2), - ('postgresql', 'postgresql://postgres:postgres@localhost:5432/' + DATABASE_NAME, - 'postgresql://postgres:postgres@localhost:5432/' + DATABASE_NAME_2), - ) - - -params = (('SCHEMA', 'DATABASE_URI', 'DATABASE_URI_2'), ( - db_uris -)) - - -class TestWalletMixin: - SCHEMA = None - - @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() +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: - 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() + os.remove(dburi) + except PermissionError: + db_obj = Db(dburi) + db_obj.drop_db(True) + db_obj.session.close() + db_obj.engine.dispose() + return dburi - @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): + +class TestWalletCreate(unittest.TestCase): wallet = None + database_uri = None @classmethod def setUpClass(cls): - cls.db_remove() + cls.database_uri = database_init() cls.wallet = Wallet.create( - name='test_wallet_create', - db_uri=cls.DATABASE_URI) + name='test_wallet_create', witness_type='legacy', + db_uri=cls.database_uri) def test_wallet_create(self): self.assertTrue(isinstance(self.wallet, Wallet)) @@ -155,8 +103,8 @@ def test_wallet_info(self): self.assertIn("