From ea866c4808e260e7137ed09f3c8b98ae1d6aa93e Mon Sep 17 00:00:00 2001 From: Sachin Meier <moi@macmeier.lan> Date: Sun, 16 Aug 2020 02:51:49 -0400 Subject: [PATCH 1/4] Add rpc calls v0.19.0.1 --- .gitignore | 2 +- bitcoin/rpc.py | 1380 ++++++++++++++++++++++++++++++------- bitcoin/tests/test_rpc.py | 180 ++++- 3 files changed, 1314 insertions(+), 248 deletions(-) diff --git a/.gitignore b/.gitignore index d8994c6e..41b88eee 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ local*.cfg build/ htmlcov/ python_bitcoinlib.egg-info/ -dist/ +dist/ \ No newline at end of file diff --git a/bitcoin/rpc.py b/bitcoin/rpc.py index 51f24ac9..be7e691f 100644 --- a/bitcoin/rpc.py +++ b/bitcoin/rpc.py @@ -37,15 +37,21 @@ import os import platform import sys +import warnings try: import urllib.parse as urlparse except ImportError: import urlparse +if sys.version > '3': + from io import BytesIO as _BytesIO +else: + from cStringIO import StringIO as _BytesIO import bitcoin -from bitcoin.core import COIN, x, lx, b2lx, CBlock, CBlockHeader, CTransaction, COutPoint, CTxOut +from bitcoin.core import COIN, x, lx, b2lx, CBlock, CBlockHeader, CTransaction, COutPoint, CTxOut, CTxIn from bitcoin.core.script import CScript -from bitcoin.wallet import CBitcoinAddress, CBitcoinSecret +from bitcoin.wallet import CBitcoinAddress, CBitcoinSecret, CBitcoinAddressError +from bitcoin.core.key import CPubKey DEFAULT_USER_AGENT = "AuthServiceProxy/0.1" @@ -115,6 +121,8 @@ class InWarmupError(JSONRPCError): RPC_ERROR_CODE = -28 + + class BaseProxy(object): """Base JSON-RPC proxy class. Contains only private methods; do not use directly.""" @@ -360,84 +368,10 @@ def call(self, service_name, *args): """Call an RPC method by name and raw (JSON encodable) arguments""" return self._call(service_name, *args) - def dumpprivkey(self, addr): - """Return the private key matching an address - """ - r = self._call('dumpprivkey', str(addr)) - - return CBitcoinSecret(r) - - def fundrawtransaction(self, tx, include_watching=False): - """Add inputs to a transaction until it has enough in value to meet its out value. - - include_watching - Also select inputs which are watch only - - Returns dict: - - {'tx': Resulting tx, - 'fee': Fee the resulting transaction pays, - 'changepos': Position of added change output, or -1, - } - """ - hextx = hexlify(tx.serialize()) - r = self._call('fundrawtransaction', hextx, include_watching) - - r['tx'] = CTransaction.deserialize(unhexlify(r['hex'])) - del r['hex'] - - r['fee'] = int(r['fee'] * COIN) - - return r - - def generate(self, numblocks): - """ - DEPRECATED (will be removed in bitcoin-core v0.19) - - Mine blocks immediately (before the RPC call returns) - - numblocks - How many blocks are generated immediately. - - Returns iterable of block hashes generated. - """ - r = self._call('generate', numblocks) - return (lx(blk_hash) for blk_hash in r) - - def generatetoaddress(self, numblocks, addr): - """Mine blocks immediately (before the RPC call returns) and - allocate block reward to passed address. Replaces deprecated - "generate(self,numblocks)" method. - - numblocks - How many blocks are generated immediately. - addr - Address to receive block reward (CBitcoinAddress instance) - - Returns iterable of block hashes generated. - """ - r = self._call('generatetoaddress', numblocks, str(addr)) - return (lx(blk_hash) for blk_hash in r) - - def getaccountaddress(self, account=None): - """Return the current Bitcoin address for receiving payments to this - account.""" - r = self._call('getaccountaddress', account) - return CBitcoinAddress(r) - - def getbalance(self, account='*', minconf=1, include_watchonly=False): - """Get the balance - - account - The selected account. Defaults to "*" for entire wallet. It - may be the default account using "". - - minconf - Only include transactions confirmed at least this many times. - (default=1) - - include_watchonly - Also include balance in watch-only addresses (see 'importaddress') - (default=False) - """ - r = self._call('getbalance', account, minconf, include_watchonly) - return int(r*COIN) + # == Blockchain == def getbestblockhash(self): - """Return hash of best (tip) block in longest block chain.""" + """Return hash of best block in longest block chain.""" return lx(self._call('getbestblockhash')) def getblockheader(self, block_hash, verbose=False): @@ -449,11 +383,12 @@ def getblockheader(self, block_hash, verbose=False): Raises IndexError if block_hash is not valid. """ - try: - block_hash = b2lx(block_hash) - except TypeError: - raise TypeError('%s.getblockheader(): block_hash must be bytes; got %r instance' % - (self.__class__.__name__, block_hash.__class__)) + if type(block_hash) != str: + try: + block_hash = b2lx(block_hash) + except TypeError: + raise TypeError('%s.getblockheader(): block_hash must be bytes; got %r instance' % + (self.__class__.__name__, block_hash.__class__)) try: r = self._call('getblockheader', block_hash, verbose) except InvalidAddressOrKeyError as ex: @@ -472,17 +407,46 @@ def getblockheader(self, block_hash, verbose=False): else: return CBlockHeader.deserialize(unhexlify(r)) + def getblockchaininfo(self): + """Return a JSON object containing blockchaininfo""" + return self._call('getblockchaininfo') + + #Untested. Coudln't find valid filter_type? + def getblockfilter(self, block_hash, filter_type="basic"): + """ + Return a JSON object containing filter data and header data + Default filter_type must be changed + #UNTESTED + """ + # ALLOWED FOR str blockhash as well + if type(block_hash) != str: + try: + block_hash = b2lx(block_hash) + except TypeError: + raise TypeError('%s.getblock(): block_hash must be bytes; got %r instance' % + (self.__class__.__name__, block_hash.__class__)) + + try: + r = self._call('getblockfilter', block_hash, filter_type) + except InvalidAddressOrKeyError as ex: + raise IndexError('%s.getblockfilter(): %s (%d)' % + (self.__class__.__name__, ex.error['message'], ex.error['code'])) + except JSONRPCError as ex: + raise IndexError('%s.getblockfilter(): %s (%d)' % + (self.__class__.__name__, ex.error['message'], ex.error['code'])) + return r def getblock(self, block_hash): """Get block <block_hash> Raises IndexError if block_hash is not valid. """ - try: - block_hash = b2lx(block_hash) - except TypeError: - raise TypeError('%s.getblock(): block_hash must be bytes; got %r instance' % - (self.__class__.__name__, block_hash.__class__)) + if type(block_hash) != str: + try: + block_hash = b2lx(block_hash) + except TypeError: + raise TypeError('%s.getblock(): block_hash must be bytes or str; got %r instance' % + (self.__class__.__name__, block_hash.__class__)) try: # With this change ( https://github.com/bitcoin/bitcoin/commit/96c850c20913b191cff9f66fedbb68812b1a41ea#diff-a0c8f511d90e83aa9b5857e819ced344 ), # bitcoin core's rpc takes 0/1/2 instead of true/false as the 2nd argument which specifies verbosity, since v0.15.0. @@ -508,41 +472,83 @@ def getblockhash(self, height): raise IndexError('%s.getblockhash(): %s (%d)' % (self.__class__.__name__, ex.error['message'], ex.error['code'])) - def getinfo(self): - """Return a JSON object containing various state info""" - r = self._call('getinfo') - if 'balance' in r: - r['balance'] = int(r['balance'] * COIN) - if 'paytxfee' in r: - r['paytxfee'] = int(r['paytxfee'] * COIN) + def getblockstats(self, hash_or_height, *args): + # On clients before PR #17831, passing hash as bytes will result in Block not found + """Return a JSON object containing block stats""" + + try: + hval = b2lx(hash_or_height) + except TypeError: + hval = hash_or_height + try: + r = self._call('getblockstats', hval, args) + except (InvalidAddressOrKeyError, InvalidParameterError) as ex: + raise IndexError('%s.getblockstats(): %s (%d)' % + (self.__class__.__name__, ex.error['message'], ex.error['code'])) return r - def getmininginfo(self): - """Return a JSON object containing mining-related information""" - return self._call('getmininginfo') + def getchaintips(self): + """Returns JSON object with info on all current tips:""" + return self._call('getchaintips') - def getnewaddress(self, account=None): - """Return a new Bitcoin address for receiving payments. + def getchaintxstats(self, nblocks=None, block_hash=None): + """Compute stats about transactions in chain""" + if block_hash is not None: + if type(block_hash) != str: + block_hash = b2lx(block_hash) + return self._call('getchaintxstats', nblocks, block_hash) - If account is not None, it is added to the address book so payments - received with the address will be credited to account. - """ - r = None - if account is not None: - r = self._call('getnewaddress', account) - else: - r = self._call('getnewaddress') + def getdifficulty(self): + return self._call('getdifficulty') - return CBitcoinAddress(r) + def getmempoolancestors(self, txid, verbose=False): + """Returns a list of txids for ancestor transactions""" + if type(txid) != str: + try: + txid = b2lx(txid) + except TypeError: + raise TypeError("%s.getmempoolancestors(): txid must be bytes or str") + try: + r = self._call('getmempoolancestors', txid, verbose) + except InvalidAddressOrKeyError as ex: + raise IndexError('%s.getmempoolancestors(): %s (%d)' % + (self.__class__.__name__, ex.error['message'], ex.error['code'])) + return r - def getrawchangeaddress(self): - """Returns a new Bitcoin address, for receiving change. + def getmempooldescendants(self, txid, verbose=False): + """Returns a list of txids for descendant transactions""" + # Added str capacity + if type(txid) != str: + try: + txid = b2lx(txid) + except TypeError: + raise TypeError("%s.getmempooldescendants(): txid must be bytes or str") + try: + r = self._call('getmempooldescendants', txid, verbose) + except InvalidAddressOrKeyError as ex: + raise IndexError('%s.getmempooldescendants(): %s (%d)' % + (self.__class__.__name__, ex.error['message'], ex.error['code'])) + return r - This is for use with raw transactions, NOT normal use. - """ - r = self._call('getrawchangeaddress') - return CBitcoinAddress(r) + def getmempoolentry(self, txid): + """Returns a JSON object for mempool transaction""" + if type(txid) != str: + try: + txid = b2lx(txid) + except TypeError: + raise TypeError("%s.getmempoolentry(): txid must be bytes or str") + try: + r = self._call('getmempoolentry', txid) + except InvalidAddressOrKeyError as ex: + raise IndexError('%s.getmempoolentry(): %s (%d)' % + (self.__class__.__name__, ex.error['message'], ex.error['code'])) + return r + + def getmempoolinfo(self): + """Returns a JSON object of mempool info""" + return self._call('getmempoolinfo') + #Untested def getrawmempool(self, verbose=False): """Return the mempool""" if verbose: @@ -553,108 +559,935 @@ def getrawmempool(self, verbose=False): r = [lx(txid) for txid in r] return r - def getrawtransaction(self, txid, verbose=False): - """Return transaction with hash txid - - Raises IndexError if transaction not found. + def gettxout(self, outpoint, includemempool=True): + """Return details about an unspent transaction output. - verbose - If true a dict is returned instead with additional - information on the transaction. + Raises IndexError if outpoint is not found or was spent. - Note that if all txouts are spent and the transaction index is not - enabled the transaction may not be available. + includemempool - Include mempool txouts """ - try: - r = self._call('getrawtransaction', b2lx(txid), 1 if verbose else 0) - except InvalidAddressOrKeyError as ex: - raise IndexError('%s.getrawtransaction(): %s (%d)' % - (self.__class__.__name__, ex.error['message'], ex.error['code'])) - if verbose: - r['tx'] = CTransaction.deserialize(unhexlify(r['hex'])) - del r['hex'] - del r['txid'] - del r['version'] - del r['locktime'] - del r['vin'] - del r['vout'] - r['blockhash'] = lx(r['blockhash']) if 'blockhash' in r else None + # CHANGED TO ALLOW TUPLE (str(<txid>), n) + if isinstance(outpoint, COutPoint): + r = self._call('gettxout', b2lx(outpoint.hash), outpoint.n, includemempool) else: - r = CTransaction.deserialize(unhexlify(r)) + r = self._call('gettxout', outpoint[0], outpoint[1], includemempool) + if r is None: + raise IndexError('%s.gettxout(): unspent txout %r not found' % (self.__class__.__name__, outpoint)) + r['txout'] = CTxOut(int(r['value'] * COIN), + CScript(unhexlify(r['scriptPubKey']['hex']))) + del r['value'] + del r['scriptPubKey'] + r['bestblock'] = lx(r['bestblock']) return r - def getreceivedbyaddress(self, addr, minconf=1): - """Return total amount received by given a (wallet) address + def gettxoutproof(self, txids, block_hash=None): + """Returns a hex string object of proof of inclusion in block""" + if type(txids[0]) != str: + txids = [b2lx(txid) for txid in txids] + if type(block_hash) != str: + block_hash = b2lx(block_hash) + return self._call('gettxoutproof', txids, block_hash) - Get the amount received by <address> in transactions with at least - [minconf] confirmations. + def gettxoutsetinfo(self): + """Returns JSON object about utxo set""" + # This call will probably time out on a mediocre machine + return self._call('gettxoutsetinfo') - Works only for addresses in the local wallet; other addresses will - always show zero. + #Untested + def preciousblock(self, block_hash): + """Marks a block as precious. No return""" + if type(block_hash) != str: + block_hash = b2lx(block_hash) + self._call('preciousblock', block_hash) + + def pruneblockchain(self, height): + """Prune blockchain to height. No return""" + self._call('pruneblockchain', height) + + #Untested + def savemempool(self): + """Save full mempool to disk. Will fail until + Previous dump is loaded.""" + self._call('savemempool') + + def scantxoutset(self, action, objects): + """Scans current utxo set + Actions: "start", "abort", "status" + objects: + (json array, required) Array of scan objects + Every scan object is either a string descriptor or an object + """ + return self._call('scantxoutset', action, objects) - addr - The address. (CBitcoinAddress instance) + def verifychain(self, checklevel=3, nblocks=6): + """Returns a bool upon verifying chain + Checklevel - thoroughness of verification (0-4) + nblocks - number of blocks to check (0=all) + """ + return self._call('verifychain', checklevel, nblocks) - minconf - Only include transactions confirmed at least this many times. - (default=1) + def verifytxoutproof(self, proof): + """Verifies txoutproof. + returns txid if verified + returns [] on fail """ - r = self._call('getreceivedbyaddress', str(addr), minconf) - return int(r * COIN) + #Had several timeouts on this function. Might be natural + if type(proof) != str: + proof = proof.hex() + r = self._call('verifytxoutproof', proof) + return [lx(txid) for txid in r] + + # == Control == + def getmemoryinfo(self, mode=None): + """Returns a JSON object of memory usage stats: + Modes: "stats", "mallocinfo""" + return self._call('getmemoryinfo', mode) + + def getrpcinfo(self): + """Returns a JSON object of rpc info""" + return self._call('getrpcinfo') + + def help(self, command=""): + """Return Help Text""" + return self._call('help', command) + + #Breaks connection with node. Bitcoin Core still thinks it's + #Running but all commands (from this client and from cmd line) + #stop working + # def stop(self): + # """Stops bitcoind""" + # self._call('stop') + + def uptime(self): + """Returns int of uptime""" + return self._call('uptime') - def gettransaction(self, txid): - """Get detailed information about in-wallet transaction txid + def logging(self, includes=None, excludes=None): + """Returns a JSON object of log info""" + return self._call('logging', includes, excludes) - Raises IndexError if transaction not found in the wallet. + # == Generating == + def generate(self, numblocks): + """ + DEPRECATED (will be removed in bitcoin-core v0.19) + + Mine blocks immediately (before the RPC call returns) - FIXME: Returned data types are not yet converted. + numblocks - How many blocks are generated immediately. + + Returns iterable of block hashes generated. """ - try: - r = self._call('gettransaction', b2lx(txid)) - except InvalidAddressOrKeyError as ex: - raise IndexError('%s.getrawtransaction(): %s (%d)' % - (self.__class__.__name__, ex.error['message'], ex.error['code'])) - return r + r = self._call('generate', numblocks) + return (lx(blk_hash) for blk_hash in r) - def gettxout(self, outpoint, includemempool=True): - """Return details about an unspent transaction output. + def generatetoaddress(self, numblocks, addr): + """Mine blocks immediately (before the RPC call returns) and + allocate block reward to passed address. Replaces deprecated + "generate(self,numblocks)" method. - Raises IndexError if outpoint is not found or was spent. + numblocks - How many blocks are generated immediately. + addr - Address to receive block reward (CBitcoinAddress instance) - includemempool - Include mempool txouts + Returns iterable of block hashes generated. """ - r = self._call('gettxout', b2lx(outpoint.hash), outpoint.n, includemempool) + r = self._call('generatetoaddress', numblocks, str(addr)) + return (lx(blk_hash) for blk_hash in r) - if r is None: - raise IndexError('%s.gettxout(): unspent txout %r not found' % (self.__class__.__name__, outpoint)) + # == Mining == + # ALL MINING untested + def getblocktemplate(self, template_request=None): + """Returns a JSON object for a blocktemplate with which to mine: + template_request: + { + "mode": "str", (string, optional) This must be set to "template", "proposal" (see BIP 23), or omitted + "capabilities": [ (json array, optional) A list of strings + "support", (string) client side supported feature, 'longpoll', 'coinbasetxn', 'coinbasevalue', 'proposal', 'serverlist', 'workid' + ... + ], + "rules": [ (json array, required) A list of strings + "support", (string) client side supported softfork deployment + ... + ], + } + Result: JSON - r['txout'] = CTxOut(int(r['value'] * COIN), - CScript(unhexlify(r['scriptPubKey']['hex']))) - del r['value'] - del r['scriptPubKey'] - r['bestblock'] = lx(r['bestblock']) - return r + """ + return self._call('getblocktemplate', template_request) - def importaddress(self, addr, label='', rescan=True): - """Adds an address or pubkey to wallet without the associated privkey.""" - addr = str(addr) + def getmininginfo(self): + """Return a JSON object containing mining-related information""" + return self._call('getmininginfo') - r = self._call('importaddress', addr, label, rescan) - return r + def getnetworkhashps(self, nblocks=None, height=None): + """Returns a int estimate of hashrate at block height + measured since nblocks (default=120) + """ + return self._call('getnetworkhashps', nblocks, height) - def listunspent(self, minconf=0, maxconf=9999999, addrs=None): - """Return unspent transaction outputs in wallet + def prioritisetransaction(self, txid, fee_delta, dummy=""): + """Returns true. Prioritises transaction for mining""" + if type(txid) != str: + txid = b2lx(txid) + return self._call('prioritisetransaction', txid, dummy, fee_delta) - Outputs will have between minconf and maxconf (inclusive) - confirmations, optionally filtered to only include txouts paid to - addresses in addrs. + def submitblock(self, block, params=None): + """Submit a new block to the network. + + params is optional and is currently ignored by bitcoind. See + https://en.bitcoin.it/wiki/BIP_0022 for full specification. """ - r = None - if addrs is None: - r = self._call('listunspent', minconf, maxconf) + # Allow for hex directly + if type(block) == str: + hexblock = block else: - addrs = [str(addr) for addr in addrs] - r = self._call('listunspent', minconf, maxconf, addrs) + hexblock = hexlify(block.serialize()) + if params is not None: + return self._call('submitblock', hexblock, params) + else: + return self._call('submitblock', hexblock) - r2 = [] + def submitheader(self, hexdata): + """Submit block to the network.""" + try: + r = self._call('submitblock', hex_data) + except VerifyError as ex: + raise VerifyError('%s.submitheader() - Invalid Header: %s (%d)' % + (self.__class__.__name__, ex.error['message'], ex.error['code'])) + return r + + # == Network == + def _addnode(self, node, arg): + r = self._call('addnode', node, arg) + return r + + def addnode(self, node): + return self._addnode(node, 'add') + + def addnodeonetry(self, node): + return self._addnode(node, 'onetry') + + def removenode(self, node): + return self._addnode(node, 'remove') + + def clearbanned(self): + """Clear list of banned IPs""" + self._call('clearbanned') + + def disconnectnode(self, address="", nodeid=None): + """Disconnect from node + 1. address (string, optional, default=fallback to nodeid) The IP address/port of the node + 2. nodeid (numeric, optional, default=fallback to address) The node ID (see getpeerinfo for node IDs) + """ + self._call('disconnectnode', address, nodeid) + + def getaddednodeinfo(self, nodeid=None): + """Returns a JSON object of added nodes (excluding onetry added nodes)""" + return self._call('getaddednodeinfo', nodeid) + + def getconnectioncount(self): + """Return int of connection count""" + return self._call('getconnectioncount') + + def getnettotals(self): + """Returns a JSON object of net totals""" + return self._call('getnettotals') + + def getnetworkinfo(self): + """Returns a JSON object of network info""" + return self._call('getnetworkinfo') + + def getnodeaddresses(self, count=None): + """Returns a JSON object of node addresses""" + return self._call('getnodeaddresses', count) + + def getpeerinfo(self): + """Returns a JSON object of peer info""" + return self._call('getpeerinfo') + + def listbanned(self): + """Returns a JSON object of banned peers""" + return self._call('listbanned') + + def ping(self): + """Ping all connections and record ping time in + getpeerinfo + """ + return self._call('ping') + + def setban(self, subnet, command, bantime=None, absolute=None): + """Add or remove nodes from banlist""" + return self._call('setban', subnet, command, bantime, absolute) + + def setnetworkactive(self, state): + """Enable/Disable all p2p connections""" + return self._call('setnetworkactive', state) + + # == Rawtransactions == + # PSBT + def analyzepsbt(self, psbt_b64): + #TODO create PSBT object to pass instead of psbt_b64 + """Return a JSON object of PSBT""" + return self._call('analyzepsbt', psbt_b64) + + def combinepsbt(self, psbt_b64s): + #is passing a list the best way? + #TODO when PSBT object exists, decode this. + """Return a base64 encoded PSBT""" + return self._call('combinepsbt', psbt_b64s) + + def converttopsbt(self, tx, permitsigdata=None, iswitness=None): + """Returns a base64 encoded PSBT""" + if type(tx) != str: + tx = hexlify(tx.serialize()) + return self._call('converttopsbt', tx, permitsigdata, iswitness) + + # Python API is different from RPC API: data + def createpsbt(self, vins, vouts, data="", locktime=0, replaceable=False): + """Returns a base64-encoded PSBT object + This is probably not the best implementation, + but no existing object is suitable for vin or vout. + vins - list of CTxIn or {"txid": "hex","vout": n,"sequence": n} + vouts - list of CTxOut or {"address": amount}, + data - hex data NOT JSON + """ + if isinstance(vins[0], CTxIn): + ins = [] + for i in vins: + txid = b2lx(i.prevout.hash) + vout = i.prevout.n + sequence = i.nSequence + ins.append({"txid": txid, "vout": vout, "sequence": sequence}) + vins = ins #Allow for JSON data to be passed straight to vins + if isinstance(vouts[0], COutPoint): + outs = [] + for o in vouts: + try: + addr = CBitcoinAddress.from_scriptPubKey(o.scriptPubKey) + amount = o.nValue + outs.append({str(addr): amount/COIN}) + except CBitcoinAddressError: + raise CBitcoinAddressError("Invalid output: %s" % repr(o)) + vouts = outs + if data: + vouts.append({"data": data}) + return self._call('createpsbt', vins, vouts, locktime, replaceable) + + def decodepsbt(self, psbt_b64): + """Returns a JSON object of PSBT. + Should return a PSBT object when created. + """ + return self._call('decodepsbt', psbt_b64) + + def finalizepsbt(self, psbt_b64, extract=None): + """Returns an extracted transaction hex or a PSBT, depending on + extract + { + "psbt" : "value", + "hex" : "value", + "complete" : true|false, + ] + } + """ + r = self._call('finalizepsbt', psbt_b64, extract) + if extract: + r = CTransaction.deserialize(unhexlify(r)) + else: + r['tx'] = CTransaction.deserialize(unhexlify(r['hex'])) + del r['hex'] + return r + + def joinpsbts(self, psbt_b64s): + """Return a base64-encoded PSBT""" + return self._call('joinpsbts', psbt_b64s) + + def utxoupdatepsbt(self, psbt_b64, data): + """Return base64-encoded PSBT""" + return self._call('utxoupdatepsbt', psbt_b64, data) + + #RAW TX + def combinerawtransaction(self, hextxs): + """Return raw hex of combined transaction""" + if type(hextxs[0]) != str: + hextxs = [hexlify(tx.serialize()) for tx in hextxs] + return self._call('combinerawtransaction', hextxs) + + def createrawtransaction(self, vins, vouts, locktime=0, replaceable=False): + """Returns a Transaction Object + Again object should be created to allow vins and vouts + """ + r = self._call('createrawtransactions', vins, vouts, locktime, replaceable) + return CTransaction.deserialize(unhexlify(r)) + + def getrawtransaction(self, txid, verbose=False, block_hash=None): + """Return transaction with hash txid + + Raises IndexError if transaction not found. + + verbose - If true a dict is returned instead with additional + information on the transaction. + + Note that if all txouts are spent and the transaction index is not + enabled the transaction may not be available. + """ + #Timeout issues depending on tx / machine + # Allow handling strings. Desirable? + if type(txid) != str: + txid = b2lx(txid) + if type(block_hash) != str: + block_hash = b2lx(block_hash) + try: + r = self._call('getrawtransaction', txid, 1 if verbose else 0, block_hash) + except InvalidAddressOrKeyError as ex: + raise IndexError('%s.getrawtransaction(): %s (%d)' % + (self.__class__.__name__, ex.error['message'], ex.error['code'])) + if verbose: + r['tx'] = CTransaction.deserialize(unhexlify(r['hex'])) + del r['hex'] + del r['txid'] + del r['version'] + del r['locktime'] + del r['vin'] + del r['vout'] + r['blockhash'] = lx(r['blockhash']) if 'blockhash' in r else None + else: + r = CTransaction.deserialize(unhexlify(r)) + + return r + + def sendrawtransaction(self, tx, allowhighfees=False): + """Submit transaction to local node and network. + + allowhighfees - Allow even if fees are unreasonably high. + """ + hextx = hexlify(tx.serialize()) + r = None + if allowhighfees: + r = self._call('sendrawtransaction', hextx, True) + else: + r = self._call('sendrawtransaction', hextx) + return lx(r) + + def sendrawtransactionv0_19(self, tx, maxfeerate=None): + """Submit transaction to local node and network. + + maxfeerate - numeric or string for max fee rate + """ + if type(tx) != str: + tx = hexlify(tx.serialize()) + r = self._call('sendrawtransaction', tx, maxfeerate) + return lx(r) + + def signrawtransaction(self, tx, *args): + """Sign inputs for transaction + + FIXME: implement options + """ + hextx = hexlify(tx.serialize()) + r = self._call('signrawtransaction', hextx, *args) + r['tx'] = CTransaction.deserialize(unhexlify(r['hex'])) + del r['hex'] + return r + + def decoderawtransaction(self, hex_data, iswitness=None): + """Return a JSON object representing the transaction""" + return self._call('decoderawtransaction', hex_data, iswitness) + + def decodescript(self, hex_data): + """Returns a JSON object with script info""" + return self._call('decodescript', hex_data) + + def fundrawtransaction(self, tx, include_watching=False): + """Add inputs to a transaction until it has enough in value to meet its out value. + + include_watching - Also select inputs which are watch only + + Returns dict: + + {'tx': Resulting tx, + 'fee': Fee the resulting transaction pays, + 'changepos': Position of added change output, or -1, + } + """ + hextx = hexlify(tx.serialize()) + r = self._call('fundrawtransaction', hextx, include_watching) + + r['tx'] = CTransaction.deserialize(unhexlify(r['hex'])) + del r['hex'] + + r['fee'] = int(r['fee'] * COIN) + + return r + + def fundrawtransactionv0_19(self, tx, options=None, iswitness=None): + """ + Options - a JSON dictionary of options. if True is passed, watch-only is included. + + Returns a dict: + {'tx': Resulting tx + 'fee': Fee the resulting transaction pays, + 'changepos': Position of added change output, or -1, + } + """ + if type(tx) != str: + tx = hexlify(tx.serialize()) + r = self._call('fundrawtransaction', tx, options, iswitness) + r['tx'] = CTransaction.deserialize(unhexlify(r['hex'])) + del r['hex'] + r['fee'] = int(r['fee'] * COIN) # BTC -> sats + return r + + def signrawtransactionwithkey(self, tx, privkeys, prevtxs=None, sighashtype=None): + """Return a transaction object + privkeys - list of CBitcoinSecret objects or list of base58-encoded privkeys (str) + prevtxs - JSON object containing info + sighashtype - numeric sighashtype default=SIGHASH_ALL + """ + # THIS ALLOWS FOR str, bytes, and CBitcoinSecret. desirable? + if type(tx) != str: + tx = hexlify(tx.serialize()) + if isinstance(privkeys[0], CBitcoinSecret): # IS THIS CORRECT + privkeys = [str(sk) for sk in privkeys] + elif isinstance(privkeys[0], bytes): # ALLOW FOR BYTES + privkeys = [sk.hex() for sk in privkeys] + r = self._call('signrawtransactionwithkey', privkeys, prevtxs, ) + r['tx'] = CTransaction.deserialize(unhexlify(r['hex'])) + del r['hex'] + return r + + def testmempoolaccept(self, txs, maxfeerate=None): + """Return a JSON object of each transaction's acceptance info""" + if type(txs[0]) != str: + txs = [hexlify(tx.serialize()) for tx in txs] + return self._call('testmempoolaccept', txs, maxfeerate) + + # == Util == + + def validateaddress(self, address): + """Return information about an address""" + r = self._call('validateaddress', str(address)) + if r['isvalid']: + r['address'] = CBitcoinAddress(r['address']) + if 'pubkey' in r: + r['pubkey'] = unhexlify(r['pubkey']) + return r + + def createmultisig(self, nrequired, keys, address_type=None): + """Return a json object with the address and redeemScript + nrequired - int required sigs + keys - list of keys as str or CPubKey + address_type - Options are "legacy", "p2sh-segwit", and "bech32" + + return: + { + "address": CBitcoinAddress, + "redeemScript": CScript + } + + """ + if type(keys[0]) != str: + keys = [str(k) for k in keys] + r = self._call('createmultisig', nrequired, keys, address_type) + # PLEASE CHECK + redeemScript = CScript.fromhex(r['redeemScript']) + r['redeemScript'] = redeemScript + r['address'] = CBitcoinAddress.from_scriptPubKey(redeemScript.to_p2sh_scriptPubKey()) + return r + + def deriveaddresses(self, descriptor, _range=None): + """Returns addresses from descriptor + + """ + #TODODescriptors need Implementing + return self._call('deriveaddresses', descriptor, _range) + + def estimatesmartfee(self, conf_target, estimate_mode=None): + """Returns a JSON object with feerate, errors, and block estimate + #Fix description? + conf_target - attempted number of blocks from current tip to place tx + estimate_mode: + "UNSET" + "ECONOMICAL" + default="CONSERVATIVE" + """ + return self._call('estimatesmartfee', conf_target, estimate_mode) + + def getdescriptorinfo(self, descriptor): + """Returns a JSON object with info about the descriptor: + { + "descriptor" : "desc", (string) The descriptor in canonical form, without private keys + "checksum" : "chksum", (string) The checksum for the input descriptor + "isrange" : true|false, (boolean) Whether the descriptor is ranged + "issolvable" : true|false, (boolean) Whether the descriptor is solvable + "hasprivatekeys" : true|false, (boolean) Whether the input descriptor contained at least one private key + } + """ + return self._call('getdescriptorinfo', descriptor) + + def signmessagewithprivkey(self, privkey, message): + """Return signature of signed message + WARNING: only works with legacy keys. Not P2SH or SegWit + """ + #TODO THIS SHOULD BE TURNED INTO DERSignature object + return self._call('signmessagewithprivkey', str(privkey), message) + + def verifymessage(self, address, signature, message): + """Return true/false if message signature is valid""" + return self._call('verifymessage', str(address), str(signature), message) + + # == Wallet == + def abandontransaction(self, txid): + """Marks in-wallet transaction as abandoned, allowing utxos to be 'respent'""" + self._call('abandontransaction', b2lx(txid)) + + def abortrescan(self): + """Aborts wallet rescan triggered by an RPC call (ie. privkey)""" + self._call('abortrescan') + + def addmultisigaddress(self, nrequired, keys, label=None, address_type=None): + """Add a NON-watch-only multisig address to the wallet. Requires new backup.""" + #Works for both addresses and pubkeys, but hex() vs str() is annoying. + #TODO see if CPubKey.__str__() is used elsewhere or can be changed. + if isinstance(keys[0], CBitcoinAddress): + keys = [str(k) for k in keys] + #included CPubKey for clarity. Could possibly remove + elif isinstance(keys[0], (CPubKey, bytes)): + keys = [k.hex() for k in keys] + r = self._call('addmultisigaddress', nrequired, keys, label, address_type) + r['address'] = CBitcoinAddress(r['address']) + r['redeemScript'] = CScript.fromhex(r['redeemScript']) + return r + + def backupwallet(self, destination): + """copies current wallet file to destination + destination - path to directory with or without filename + """ + self._call('backupwallet', destination) + + def bumpfee(self, txid, options=None): + """Bump fee of transation in mempool""" + if type(txid) != str: + txid = b2lx(txid) + return self._call('bumpfee', txid, options) + + def createwallet(self, wallet_name, disable_priv_keys=None, blank=None, passphrase=None, avoid_reuse=None ): + """Create a new Wallet + wallet_name - name + disable_priv_keys - watch_only, default=False + blank - create a blank wallet with no seed or keys + passphrase - encrypt wallet with passphrase + avoid_reuse - segregate reused and clean coins. Better privacy + Return a JSON object about the new wallet + """ + return self._call('createwallet', wallet_name, disable_priv_keys, blank, passphrase, avoid_reuse) + + def dumpprivkey(self, addr): + """Return the private key matching an address + """ + r = self._call('dumpprivkey', str(addr)) + return CBitcoinSecret(r) + + def dumpwallet(self, filename): + """Dump all wallet keys and imported keys to a file. + NO OVERWRITING ALLOWED + returns a JSON object with full absolute path + """ + self._call('dumpwallet', filename) + + def encryptwallet(self, passphrase): + """ + Encrypts wallet for the first time. + This passphrase will be required for all signing after call. + """ + self._call('encryptwallet', passphrase) + + def getaddressesbylabel(self, label): + """Return a JSON object with addresses as keys""" + # Convert to CBitcoinAddress? + # not converting addresses makes the dict searchable. + return self._call('getaddressbylabel', label) + + def getaccountaddress(self, account=None): + """Return the current Bitcoin address for receiving payments to this + account.""" + r = self._call('getaccountaddress', account) + return CBitcoinAddress(r) + + def getaddressinfo(self, address): + """Return a JSON object of info about address""" + if type(address) != str: + address = str(address) + r = self._call('getaddressinfo', address) + if r['script'] == 'scripthash': + r['redeemScript'] = CScript.fromhex(r['hex']) + # Keeping with previous style. why not CPubKey? + r['pubkey'] = unhexlify(r['pubkey']) + # PERHAPS ALSO CHANGE ScriptPubKey to CScript? + return r + + def getbalance(self, account='*', minconf=1, include_watchonly=False): + """Get the balance + + account - The selected account. Defaults to "*" for entire wallet. It + may be the default account using "". + + minconf - Only include transactions confirmed at least this many times. + (default=1) + + include_watchonly - Also include balance in watch-only addresses (see 'importaddress') + (default=False) + """ + r = self._call('getbalance', account, minconf, include_watchonly) + return int(r*COIN) + + def getbalances(self): + """Returns a JSON object of balances of all wallets and imported keys + All balances shown in sats + """ + r = self._call('getbalances') + for k in r['mine'].keys(): + r['mine'][k] = int(r['mine'][k]* COIN) + if 'watchonly' in r: + for k in r['watchonly'].keys(): + r['watchonly'][k] = int(r['watchonly'][k]* COIN) + return r + + def getnewaddress(self, account=None, address_type=None): + """Return a new Bitcoin address for receiving payments. + + If account is not None, it is added to the address book so payments + received with the address will be credited to account. + + address_type: + "legacy" + """ + r = None + if account is not None or address_type is not None: + r = self._call('getnewaddress', account, address_type) + else: + r = self._call('getnewaddress') + + return CBitcoinAddress(r) + + def getrawchangeaddress(self): + """Returns a new Bitcoin address, for receiving change. + + This is for use with raw transactions, NOT normal use. + """ + r = self._call('getrawchangeaddress') + return CBitcoinAddress(r) + + def getreceivedbyaddress(self, addr, minconf=1): + """Return total amount received by given a (wallet) address + + Get the amount received by <address> in transactions with at least + [minconf] confirmations. + + Works only for addresses in the local wallet; other addresses will + always show zero. + + addr - The address. (CBitcoinAddress instance) + + minconf - Only include transactions confirmed at least this many times. + (default=1) + """ + r = self._call('getreceivedbyaddress', str(addr), minconf) + return int(r * COIN) + + def gettransaction(self, txid): + """Get detailed information about in-wallet transaction txid + + Raises IndexError if transaction not found in the wallet. + + FIXME: Returned data types are not yet converted. + """ + try: + r = self._call('gettransaction', b2lx(txid)) + except InvalidAddressOrKeyError as ex: + raise IndexError('%s.getrawtransaction(): %s (%d)' % + (self.__class__.__name__, ex.error['message'], ex.error['code'])) + return r + + def getunconfirmedbalance(self): + """Deprecated in v0.19.0.1""" + r = None + try: + r = int(self._call('getunconfirmedbalance') * COIN) + return r + except: + raise DeprecationWarning("Use %s.getbalances().mine.untrusted_pending" % self.__class__.__name__) + + def getwalletinfo(self): + """Returns a JSON with wallet info + Results vary by version. + """ + r = self._call('getwalletinfo') + r['paytxfee'] = int(r['paytxfee']*COIN) + try: # Deprecated + r['balance'] = int(r['balance']*COIN) + r['unconfirmed_balance'] = int(r['unconfirmed_balance']*COIN) + r['immature_balance'] = int(r['immature_balance']*COIN) + except KeyError: + pass + return r + + #TODO ADD P2SH arg. This will cause JSONRPCError on older versions + def importaddress(self, addr, label='', rescan=True): + """Adds an address or pubkey to wallet without the associated privkey.""" + + addr = str(addr) + + r = self._call('importaddress', addr, label, rescan) + return r + + #Since Options is only rescan (bool), change this API? + def importmulti(self, requests, options=None): + """Import several pubkeys, privkeys, or scripts + requests - a JSON object + options - a JSON object + return a JSON + """ + # The Requests JSON is so large, I decided not + # to allow CObjects in the JSON. + # TODO Fix this? + return self._call('importmulti', requests, options) + + def importprivkey(self, privkey, label=None, rescan=True): + """Import a privkey and optionally rescan""" + self._call('importprivkey', str(privkey), label, rescan) + + def importprunedfunds(self, tx, txout_proof): + """Import a transaction. Address must already be in wallet. + User must import subsequent transactions or rescan + + #TODO should txout_proof be an obj? + """ + if type(tx) != str: + tx = hexlify(tx.serialize()) + return self._call('importprunedfunds', tx, txout_proof) + + def importpubkey(self, pubkey, label=None, rescan=None): + """Import pubkey as watchonly""" + if type(pubkey) != str: + pubkey = pubkey.hex() + self._call('importpubkey', pubkey, label, rescan) + + def importwallet(self, filename): + """Import wallet by filename""" + self._call('importwallet') + + def keypoolrefill(self, new_size=100): + """Add more keys to keypool + new_size - int total size of pool after call + """ + self._call('keypoolrefill') + + def listaddressgroupings(self): + """Lists groups of addresses which have common ownership + exposed by joint use + Returns a JSON object with list of address groupings (lists) + """ + # Make into address or leave readable/searchable? + return self._call('listaddressgroupings') + + def listlabels(self, purpose=None): + """List all labels that are assigned to addresses with specific purposes""" + return self._call('listlabels') + + def listlockunspent(self): + """Returns list of temporarily unspendable outputs.""" + r = self._call('listlockunspent') + for unspent in r: + unspent['outpoint'] = COutPoint(lx(unspent['txid']), unspent['vout']) + del unspent['txid'] + del unspent['vout'] + return r + + def listreceivedbyaddress(self, minconf=1, include_empty=None, include_watchonly=None, address_filter=None): + """List balances by receiving address + Return a JSON of address infos + """ + r = self._call('listreceivedbyaddress', minconf, include_empty, include_watchonly, address_filer) + for recd in r: + recd['address'] = CBitcoinAddress(recd['address']) + recd['amount'] = int(recd['amount']*COIN) + recd['txid'] = [lx(txid) for txid in recd['txid']] + return r + + def listreceivedbylabel(self, minconf=1, include_empty=False, include_watchonly=None): + """List balances by label + Return a JSON of address infos + """ + r = self._call('listreceivedbylabel', minconf, include_empty, include_watchonly) + for recd in r: + recd['address'] = CBitcoinAddress(recd['address']) + recd['amount'] = int(recd['amount']*COIN) + #listreceivedbylabel doesn't return TXIDs. + # I will be PR'ing Core to change this in Future. + #recd['txid'] = [lx(txid) for txid in recd['txid']] + return r + + def listsinceblock(self, block_hash=None, conf_target=1, include_watchonly=None, include_removed=True): + """List balances since block (determined by block_hash) + """ + r = self._call('listsinceblock', block_hash, conf_target, include_watchonly, include_removed) + for tx in r['transactions']: + tx['address'] = CBitcoinAddress(tx['address']) + tx['amount'] = int(tx['amount']*COIN) + if 'fee' in tx: + tx['fee'] = int(tx['fee']*COIN) + tx['outpoint'] = COutPoint(lx(tx['txid']), tx['vout']) + del tx['txid'] + del tx['vout'] + if 'removed' in r: # Only present if include_removed + for tx in r['removed']: + tx['address'] = CBitcoinAddress(tx['address']) + tx['amount'] = int(tx['amount']*COIN) + if 'fee' in tx: + tx['fee'] = int(tx['fee']*COIN) + tx['outpoint'] = COutPoint(lx(tx['txid']), tx['vout']) + del tx['txid'] + del tx['vout'] + return r + + def listtransactions(self, label=None, count=None, skip=None, include_watchonly=None): + """List all transactions""" + r = self._call('listtransaction', label, count, skip, include_watchonly) + for tx in r['transactions']: + tx['address'] = CBitcoinAddress(tx['address']) + tx['amount'] = int(tx['amount']*COIN) + if 'fee' in tx: + tx['fee'] = int(tx['fee']*COIN) + tx['outpoint'] = COutPoint(lx(tx['txid']), tx['vout']) + del tx['txid'] + del tx['vout'] + if 'removed' in r: # Only present if include_removed + for tx in r['removed']: + tx['address'] = CBitcoinAddress(tx['address']) + tx['amount'] = int(tx['amount']*COIN) + if 'fee' in tx: + tx['fee'] = int(tx['fee']*COIN) + tx['outpoint'] = COutPoint(lx(tx['txid']), tx['vout']) + del tx['txid'] + del tx['vout'] + return r + + #TODO add include_unsafe, query_options + def listunspent(self, minconf=0, maxconf=9999999, addrs=None): + """Return unspent transaction outputs in wallet + + Outputs will have between minconf and maxconf (inclusive) + confirmations, optionally filtered to only include txouts paid to + addresses in addrs. + """ + r = None + if addrs is None: + r = self._call('listunspent', minconf, maxconf) + else: + addrs = [str(addr) for addr in addrs] + r = self._call('listunspent', minconf, maxconf, addrs) + + r2 = [] for unspent in r: unspent['outpoint'] = COutPoint(lx(unspent['txid']), unspent['vout']) del unspent['txid'] @@ -672,25 +1505,39 @@ def listunspent(self, minconf=0, maxconf=9999999, addrs=None): r2.append(unspent) return r2 + def listwalletdir(self): + """Return a JSON object of wallets in wallet directory""" + return self._call('listwalletdir') + + def listwallets(self): + """Return a list of currently loaded wallets""" + return self._call('listwallets') + + def loadwallet(self, filename): + """Load a wallet from filename or directory name + Returns a JSON object of result + """ + return self._call('loadwallet', filename) + def lockunspent(self, unlock, outpoints): """Lock or unlock outpoints""" json_outpoints = [{'txid':b2lx(outpoint.hash), 'vout':outpoint.n} for outpoint in outpoints] return self._call('lockunspent', unlock, json_outpoints) - def sendrawtransaction(self, tx, allowhighfees=False): - """Submit transaction to local node and network. + def removeprunedfunds(self, txid): + """Remove pruned utxos from wallet""" + if type(txid) != str: + txid = b2lx(txid) + self._call('removeprunedfunds', txid) - allowhighfees - Allow even if fees are unreasonably high. + def rescanblockchain(self, start_height=0, stop_height=None): + """Begin rescan of blockchain + Return a JSON object of result """ - hextx = hexlify(tx.serialize()) - r = None - if allowhighfees: - r = self._call('sendrawtransaction', hextx, True) - else: - r = self._call('sendrawtransaction', hextx) - return lx(r) + return self._call('rescanblockchain') + #TODO API updates for sendmany and sendtoaddress def sendmany(self, fromaccount, payments, minconf=1, comment='', subtractfeefromamount=[]): """Send amount to given addresses. @@ -708,16 +1555,43 @@ def sendtoaddress(self, addr, amount, comment='', commentto='', subtractfeefroma r = self._call('sendtoaddress', addr, amount, comment, commentto, subtractfeefromamount) return lx(r) - def signrawtransaction(self, tx, *args): - """Sign inputs for transaction + def sethdseed(self, newkeypool=True, seed=None): + """Set HD Seed of Wallet + newkeypool - bool flush old unused addresses, including change addresses + seed - WIF Private Key. random seed if none + """ + self._call('sethdseed', newkeypool, str(seed)) - FIXME: implement options + def setlabel(self, address, label): + """Apply a label to an existing address""" + self._call('setlabel', str(address), label) + + def settxfee(self, amount): + """Set fee for transactions of this wallet + amount - int sats/Bytes + return bool of success """ - hextx = hexlify(tx.serialize()) - r = self._call('signrawtransaction', hextx, *args) - r['tx'] = CTransaction.deserialize(unhexlify(r['hex'])) - del r['hex'] - return r + # Convert from sats/B to BTC/kB + amount = (amount/COIN)*1000 + return self._call('settxfee', amount) + + def setwalletflag(self, flag, value=True): + """Change state of a given flag for a wallet + flag - options: "avoid_reuse" + value - bool new value for flag + + returns a JSON objection with flag and new value + """ + return self._call('setwalletflag', flag, value) + + def signmessage(self, address, message): + """Sign a message using privkey associated with given address + address - CBitcoinAddress or str + message - full message to be signed + return signature in base64 + #TODO convert base64 to DERSignature obj + """ + return self._call('signmessage', str(address), message) def signrawtransactionwithwallet(self, tx, *args): """Sign inputs for transaction @@ -731,27 +1605,51 @@ def signrawtransactionwithwallet(self, tx, *args): del r['hex'] return r - def submitblock(self, block, params=None): - """Submit a new block to the network. + def unloadwallet(self, wallet_name=None): + """Unload wallet""" + self._call('unloadwallet') - params is optional and is currently ignored by bitcoind. See - https://en.bitcoin.it/wiki/BIP_0022 for full specification. - """ - hexblock = hexlify(block.serialize()) - if params is not None: - return self._call('submitblock', hexblock, params) - else: - return self._call('submitblock', hexblock) + # Python API is different from RPC API: data + def walletcreatefundedpsbt(self, vins, vouts, data=None, locktime=0, options=None, bip32derivs=None): + """Create funded PSBT from wallet funds + vins - a list of CTxIn + vouts - a list of CTxOut + locktime - raw locktime (block height or unix timestamp) + options - a JSON object + bip32derivs - bool include BIP32 paths in PSBT - def validateaddress(self, address): - """Return information about an address""" - r = self._call('validateaddress', str(address)) - if r['isvalid']: - r['address'] = CBitcoinAddress(r['address']) - if 'pubkey' in r: - r['pubkey'] = unhexlify(r['pubkey']) + returns a JSON object with base64-encoded PSBT, fee, and changepos + """ + if isinstance(vins[0], CTxIn): + ins = [] + for i in vins: + txid = b2lx(i.prevout.hash) + vout = i.prevout.n + sequence = i.nSequence + ins.append({"txid": txid, "vout": vout, "sequence": sequence}) + vins = ins #Allow for JSON to be passed directly + if isinstance(vouts[0], CTxOut): + outs = [] + for o in vouts: + try: + addr = CBitcoinAddress.from_scriptPubKey(o.scriptPubKey) + amount = o.nValue + outs.append({str(addr): amount/COIN}) + except CBitcoinAddressError: + raise CBitcoinAddressError("Invalid output: %s" % repr(o)) + vouts = outs + if data: + vouts.append({"data": data}) + #TODO allow for addresses in options + + r = self._call('walletcreatefundedpsbt', vins, vouts, locktime, options, bip32derivs) + r['fee'] = int(r['fee'] * COIN) return r + def walletlock(self): + """locks wallet. Password will be required for future signing""" + self._call('walletlock') + def unlockwallet(self, password, timeout=60): """Stores the wallet decryption key in memory for 'timeout' seconds. @@ -761,20 +1659,38 @@ def unlockwallet(self, password, timeout=60): (default=60) """ r = self._call('walletpassphrase', password, timeout) + #FIXME as of v0.19.0.1 no return return r - def _addnode(self, node, arg): - r = self._call('addnode', node, arg) - return r + def walletpassphrase(self, password, timeout=60): + """Same as unlockwallet""" + return self.unlockwallet(password, timeout) - def addnode(self, node): - return self._addnode(node, 'add') + def walletpassphrasechange(self, oldpassphrase, newpassphrase): + """Change passphrase from oldpassphrase to newpassphrase""" + self._call('walletpassphrasechange') - def addnodeonetry(self, node): - return self._addnode(node, 'onetry') + def walletprocesspsbt(self, psbt, sign=True, sighashtype=None, bip32derivs=None): + """Process base64-encoded PSBT, add info and sign vins that belong to this wallet + Return a base64-encoded PSBT + """ + return self._call('walletprocesspsbt', psbt, sign, sighashtype, bip32derivs) + + def getinfo(self): + """Return a JSON object containing various state info""" + try: + r = self._call('getinfo') + if 'balance' in r: + r['balance'] = int(r['balance'] * COIN) + if 'paytxfee' in r: + r['paytxfee'] = int(r['paytxfee'] * COIN) + return r + except: + warnings.warn( + "getinfo is deprecated from version 0.16.0 use getnetworkinfo instead", DeprecationWarning + ) + - def removenode(self, node): - return self._addnode(node, 'remove') __all__ = ( 'JSONRPCError', diff --git a/bitcoin/tests/test_rpc.py b/bitcoin/tests/test_rpc.py index 07d1f964..f148cc61 100644 --- a/bitcoin/tests/test_rpc.py +++ b/bitcoin/tests/test_rpc.py @@ -13,24 +13,174 @@ import unittest -from bitcoin.rpc import Proxy +import bitcoin.rpc +from bitcoin.core import CBlock, CBlockHeader, lx, b2lx, COutPoint +from bitcoin.core.script import CScript +from bitcoin.wallet import CBitcoinAddress, CBitcoinSecret + +def is_active(): + """ + Proxy raises ValueError if cookie file not found + #FIXME is there a better way of doing this? + """ + try: + p = bitcoin.rpc.Proxy() + return True + except ValueError: + return False class Test_RPC(unittest.TestCase): + _IS_ACTIVE = is_active() # Tests disabled, see discussion below. # "Looks like your unit tests won't work if Bitcoin Core isn't running; # maybe they in turn need to check that and disable the test if core isn't available?" # https://github.com/petertodd/python-bitcoinlib/pull/10 - pass - -# def test_can_validate(self): -# working_address = '1CB2fxLGAZEzgaY4pjr4ndeDWJiz3D3AT7' -# p = Proxy() -# r = p.validateAddress(working_address) -# self.assertEqual(r['address'], working_address) -# self.assertEqual(r['isvalid'], True) -# -# def test_cannot_validate(self): -# non_working_address = 'LTatMHrYyHcxhxrY27AqFN53bT4TauR86h' -# p = Proxy() -# r = p.validateAddress(non_working_address) -# self.assertEqual(r['isvalid'], False) + # Sachin Meier: "I've changed it so each test checks against the " + #pass + + def test_getbestblockhash_and_header(self): + if self._IS_ACTIVE: + proxy = bitcoin.rpc.Proxy() + blockhash = proxy.getbestblockhash() + header = proxy.getblockheader(blockhash) + self.assertTrue(isinstance(header, CBlockHeader)) + else: + pass + + def test_getblock(self): + if self._IS_ACTIVE: + proxy = bitcoin.rpc.Proxy() + blockhash = proxy.getbestblockhash() + # Test from bytes + block1 = proxy.getblock(blockhash) + self.assertTrue(isinstance(block1, CBlock)) + # Test from str + block2 = proxy.getblock("0000000000000000000b4b0daf89eac9d84138fc900b8c473d4da70742e93dd0") + self.assertTrue(isinstance(block2, CBlock)) + else: + pass + + def test_getblockcount_et_al(self): + # This test could possibly false-fail if new blocks arrive. + # Highly unlikely since they're quick calls + if self._IS_ACTIVE: + proxy = bitcoin.rpc.Proxy() + blockhash = proxy.getbestblockhash() + height_from_hash = proxy.getblockstats(blockhash)["height"] + height_from_count = proxy.getblockcount() + height_from_chaintips = proxy.getchaintips()[0]["height"] + height_from_chaintxstats = proxy.getchaintxstats()["window_final_block_height"] + self.assertEqual(height_from_count, height_from_hash) + self.assertEqual(height_from_chaintips, height_from_chaintxstats) + self.assertEqual(height_from_chaintips, height_from_count) + else: + pass + + def test_txoutproofs(self): + if self._IS_ACTIVE: + proxy = bitcoin.rpc.Proxy() + blockhash = "0000000000000000000317612505ebdbe2686856535903bb0a05d4629670d518" + c_txid = "468564cfeba24ae321ee142e8786a53005f33051222e42f06fb2e9f048d0dba5" + c_proof = "00e0ff3749d01e6bebeb55a3dc983f194a1e232dc7149aff308d0d000000000000000000e331c7b03923f8b98074c7abeb10f609804ea18a53389b310c560c555b5c7d90ac8f315ff8b41017108dcd6a2c0b00000da53b7cc71139618dee5368d2075cd50badb97b0b4e4ca07b3ff749006280ff05a5dbd048f0e9b26ff0422e225130f30530a586872e14ee21e34aa2ebcf648546a03eab6796b5ff607266d66cf75d10454ac8370f0630c02395da56e8a3f9bc07b5adf47993ba6e33a625a7243c87111a93b592627efe4d6a1c3385685e8b0ae37e05b93e0de10db4c82466baf83da8c32a599fe2b6cace3c7a1b0b59e591071a656f93a19c08a4cc93d95e511220db284c72da6669355aa49226d61287e6048166e87bf93847a39f1c7552088c1831aabb4a6f29aaaa951eaafeaca21aea982068a2a51ff1088df84cdb7d5cdfbad8f91f2f75f45403d78b0fee2e68fdf5f076e8cff72482184a62b37e5af25b9227f27bedd3ebef27d01b0f99e0c456922f8fd16ad36445ca52bde44e42b145803130a420feb6fc0d8d9f2b9e12954ad8ea537ea843e7bddad228f7a754df0bf4337361f6bde81304d9cae789adaafd7b607ac49e422c5b01b3b859f777bb86f69e4047b9fe9752db5822becaa579b0066dbfaced20ab383ea8caa113437564dbcef9c04f224b352364baaddd7bfdb517383a04ff2f0000" + proof = proxy.gettxoutproof([c_txid], blockhash) + txid = b2lx(proxy.verifytxoutproof(proof)[0]) + self.assertEqual(c_txid, txid) # CHECK THAT TXID comes out well. maybe hex it + self.assertEqual(c_proof, proof) + else: + pass + + def test_can_validate(self): + if self._IS_ACTIVE: + p = bitcoin.rpc.Proxy() + working_address = '1CB2fxLGAZEzgaY4pjr4ndeDWJiz3D3AT7' + r = p.validateaddress(working_address) + self.assertEqual(str(r['address']), working_address) + self.assertEqual(r['isvalid'], True) + else: + pass + + def test_cannot_validate(self): + if self._IS_ACTIVE: + non_working_address = 'LTatMHrYyHcxhxrY27AqFN53bT4TauR86h' + p = bitcoin.rpc.Proxy() + r = p.validateaddress(non_working_address) + self.assertEqual(r['isvalid'], False) + else: + pass + + def test_deterministic_multisig(self): + if self._IS_ACTIVE: + p = bitcoin.rpc.Proxy() + pubkeys = ["02389e049d7baf3b4170ddb5c85f0ac22198572d76e0fee3fdb6c434ac689f270d", + "0364ca1b46c1aaee3f40a35b5d32937b2616ace2914fdacdc1bf95f53fe06514d0", + "03eac5ba66377c3bc1a92d1db3c22dc8cd0626a17f22c13d481fd14ca1fa2cf7f6"] + multisig_addr = "39NHQCfNjGRLGuAH5tuPXfERJsDncYehyH" + redeemScript = "522102389e049d7baf3b4170ddb5c85f0ac22198572d76e0fee3fdb6c434ac689f270d210364ca1b46c1aaee3f40a35b5d32937b2616ace2914fdacdc1bf95f53fe06514d02103eac5ba66377c3bc1a92d1db3c22dc8cd0626a17f22c13d481fd14ca1fa2cf7f653ae" + r = p.createmultisig(2, pubkeys) + self.assertEqual(str(r['address']), multisig_addr) + self.assertEqual(r['redeemScript'].hex(), redeemScript) + else: + pass + + def test_signmessagewithprivkey(self): + """As of now, signmessagewithprivkey returns string of + signature. Later this should change + """ + if self._IS_ACTIVE: + proxy = bitcoin.rpc.Proxy() + c_sig = "Hy+OtvwJnE0ylgORtqG8/U9ZP11IW38GaSCxIvlAcrLVGWJV61Zxfb/h/A51VPEJZkIFogqxceIMTCppfEOyl5I=" + privkey_txt = "Kya9eoTsoct6rsztC5rSLfuU2S4Dw5xtgCy2uPJgbkSLXd4FqquD" + privkey = CBitcoinSecret(privkey_txt) + msg = "So Long as Men Die" + # Check from CBitcoinSecret + sig = proxy.signmessagewithprivkey(privkey, msg) + self.assertEqual(sig, c_sig) + # Check from str + sig2 = proxy.signmessagewithprivkey(privkey_txt, msg) + self.assertEqual(sig2, c_sig) + else: + pass + + def test_verifymessage(self): + if self._IS_ACTIVE: + proxy = bitcoin.rpc.Proxy() + sig = "ILRG2SnP6oPIofrfEDVk71J8rvM2KKbXU+D4+xWB2RRST4I2ilCTc7rXCS0Zu1/ousOX4aFhCrF815De71xZyxY=" + addr_txt = "14wCZ9KpTuXB35kdYH2Loy1oP1ak1BT3JH" # Not corresponding addr as signwithprivkey + addr = CBitcoinAddress(addr_txt) + msg = "So Long as Men Die" + #Check with both address and str + self.assertTrue(proxy.verifymessage(addr_txt, sig, msg)) + self.assertTrue(proxy.verifymessage(addr, sig, msg)) + return proxy.verifymessage(addr, sig, msg) + else: + pass + + # def test_setxfee(self): + # """ This test will change settings of user's core instance, so + # It is commented out for now. + # """ + # if self._IS_ACTIVE: + # proxy = bitcoin.rpc.Proxy() + # self.assertTrue( proxy.settxfee(2) ) + # else: + # pass + + def test_gettxout(self): + """Txout disappears if spent, so difficult to set static test""" + if self._IS_ACTIVE: + proxy = bitcoin.rpc.Proxy() + txo = COutPoint(lx("2700507d971a25728a257ed208ba409e7510f861dec928a478ee92f5ef2b4527"), 0) + r = proxy.gettxout(txo) + script = CScript.fromhex("76a9147179f4af7439435720637ee3276aabed1440719188ac") + self.assertEqual(r['txout'].scriptPubKey, script) + else: + pass + + + def test_getmininginfo(self): + if self._IS_ACTIVE: + proxy = bitcoin.rpc.Proxy() + proxy.getmininginfo() + else: + pass + From f496441a47a3636979a2cbdc2ddba36c0c680474 Mon Sep 17 00:00:00 2001 From: SachinMeier <sachin.meier@gmail.com> Date: Sun, 16 Aug 2020 02:54:47 -0400 Subject: [PATCH 2/4] add OP_TRUE OP_FALSE and small edits --- .DS_Store | Bin 0 -> 6148 bytes bitcoin/core/__init__.py | 2 +- bitcoin/core/script.py | 2 + bitcoin/rpc.py | 79 +++++++++++++++++--------------------- bitcoin/tests/test_rpc.py | 20 +++++----- 5 files changed, 48 insertions(+), 55 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..e29464d9edf8db61d04e267dc2f171e15b9c075c GIT binary patch literal 6148 zcmeHKISv9b4733WBpOP}e1RWC2wuPk5YQkYwgCFAco$D&d=#LC4hqm%awc&+i894{ zEh0L<>}Dbp5gEY^<!VFMY~Q?Ny^JUjjx!qB%W-$w9yU!U`*py$L)n0LmhB_IIoKAB z3Qz$mKn17(75K0MSzt%w51-59r~noCeFg0MP~e6&u?_T32L^8efCGfxF!x>pSS$dn ziESVvFbyg&sG2Q?1|9K|c{Q;O47zAGADTC7b|~t%<NV_3qBW2s6`%so3iM++vHHJ+ zzv=&<NnB9@D)3hd=wPv0%<-hGt)0hNtu631+;VPkGt8ZW!OJnw%P|&Kj;Ed!dBx_~ WuZeA-(-C(%kUs;a3ylhVTY)>(8WoKI literal 0 HcmV?d00001 diff --git a/bitcoin/core/__init__.py b/bitcoin/core/__init__.py index 272bec5c..08ca1842 100644 --- a/bitcoin/core/__init__.py +++ b/bitcoin/core/__init__.py @@ -308,7 +308,7 @@ class CMutableTxOut(CTxOut): @classmethod def from_txout(cls, txout): - """Create a fullly mutable copy of an existing TxOut""" + """Create a fully mutable copy of an existing TxOut""" return cls(txout.nValue, txout.scriptPubKey) diff --git a/bitcoin/core/script.py b/bitcoin/core/script.py index 46b83bd7..a8449a3b 100644 --- a/bitcoin/core/script.py +++ b/bitcoin/core/script.py @@ -375,12 +375,14 @@ def __new__(cls, n): OPCODES_BY_NAME = { 'OP_0': OP_0, + 'OP_FALSE': OP_0, 'OP_PUSHDATA1': OP_PUSHDATA1, 'OP_PUSHDATA2': OP_PUSHDATA2, 'OP_PUSHDATA4': OP_PUSHDATA4, 'OP_1NEGATE': OP_1NEGATE, 'OP_RESERVED': OP_RESERVED, 'OP_1': OP_1, + 'OP_TRUE': OP_1, 'OP_2': OP_2, 'OP_3': OP_3, 'OP_4': OP_4, diff --git a/bitcoin/rpc.py b/bitcoin/rpc.py index be7e691f..a0a32a94 100644 --- a/bitcoin/rpc.py +++ b/bitcoin/rpc.py @@ -383,11 +383,11 @@ def getblockheader(self, block_hash, verbose=False): Raises IndexError if block_hash is not valid. """ - if type(block_hash) != str: + if not isinstance(block_hash, str): try: block_hash = b2lx(block_hash) except TypeError: - raise TypeError('%s.getblockheader(): block_hash must be bytes; got %r instance' % + raise TypeError('%s.getblockheader(): block_hash must be bytes or str; got %r instance' % (self.__class__.__name__, block_hash.__class__)) try: r = self._call('getblockheader', block_hash, verbose) @@ -418,8 +418,7 @@ def getblockfilter(self, block_hash, filter_type="basic"): Default filter_type must be changed #UNTESTED """ - # ALLOWED FOR str blockhash as well - if type(block_hash) != str: + if not isinstance(block_hash, str): try: block_hash = b2lx(block_hash) except TypeError: @@ -441,7 +440,7 @@ def getblock(self, block_hash): Raises IndexError if block_hash is not valid. """ - if type(block_hash) != str: + if not isinstance(block_hash, str): try: block_hash = b2lx(block_hash) except TypeError: @@ -476,9 +475,9 @@ def getblockstats(self, hash_or_height, *args): # On clients before PR #17831, passing hash as bytes will result in Block not found """Return a JSON object containing block stats""" - try: + if isinstance(hash_or_height, bytes): hval = b2lx(hash_or_height) - except TypeError: + else: #int or str of block_hash or height hval = hash_or_height try: r = self._call('getblockstats', hval, args) @@ -494,7 +493,7 @@ def getchaintips(self): def getchaintxstats(self, nblocks=None, block_hash=None): """Compute stats about transactions in chain""" if block_hash is not None: - if type(block_hash) != str: + if not isinstance(block_hash, str): block_hash = b2lx(block_hash) return self._call('getchaintxstats', nblocks, block_hash) @@ -503,7 +502,7 @@ def getdifficulty(self): def getmempoolancestors(self, txid, verbose=False): """Returns a list of txids for ancestor transactions""" - if type(txid) != str: + if not isinstance(txid, str): try: txid = b2lx(txid) except TypeError: @@ -517,8 +516,7 @@ def getmempoolancestors(self, txid, verbose=False): def getmempooldescendants(self, txid, verbose=False): """Returns a list of txids for descendant transactions""" - # Added str capacity - if type(txid) != str: + if not isinstance(txid, str): try: txid = b2lx(txid) except TypeError: @@ -532,7 +530,7 @@ def getmempooldescendants(self, txid, verbose=False): def getmempoolentry(self, txid): """Returns a JSON object for mempool transaction""" - if type(txid) != str: + if not isinstance(txid, str): try: txid = b2lx(txid) except TypeError: @@ -561,12 +559,11 @@ def getrawmempool(self, verbose=False): def gettxout(self, outpoint, includemempool=True): """Return details about an unspent transaction output. - + outpoint - COutPoint or tuple (<txid>, n) Raises IndexError if outpoint is not found or was spent. includemempool - Include mempool txouts """ - # CHANGED TO ALLOW TUPLE (str(<txid>), n) if isinstance(outpoint, COutPoint): r = self._call('gettxout', b2lx(outpoint.hash), outpoint.n, includemempool) else: @@ -583,9 +580,9 @@ def gettxout(self, outpoint, includemempool=True): def gettxoutproof(self, txids, block_hash=None): """Returns a hex string object of proof of inclusion in block""" - if type(txids[0]) != str: + if not isinstance(txids[0], str): txids = [b2lx(txid) for txid in txids] - if type(block_hash) != str: + if not isinstance(block_hash, str): block_hash = b2lx(block_hash) return self._call('gettxoutproof', txids, block_hash) @@ -597,7 +594,7 @@ def gettxoutsetinfo(self): #Untested def preciousblock(self, block_hash): """Marks a block as precious. No return""" - if type(block_hash) != str: + if not isinstance(block_hash, str): block_hash = b2lx(block_hash) self._call('preciousblock', block_hash) @@ -633,7 +630,7 @@ def verifytxoutproof(self, proof): returns [] on fail """ #Had several timeouts on this function. Might be natural - if type(proof) != str: + if not isinstance(proof,str): proof = proof.hex() r = self._call('verifytxoutproof', proof) return [lx(txid) for txid in r] @@ -727,7 +724,7 @@ def getnetworkhashps(self, nblocks=None, height=None): def prioritisetransaction(self, txid, fee_delta, dummy=""): """Returns true. Prioritises transaction for mining""" - if type(txid) != str: + if not isinstance(txid, str): txid = b2lx(txid) return self._call('prioritisetransaction', txid, dummy, fee_delta) @@ -737,8 +734,7 @@ def submitblock(self, block, params=None): params is optional and is currently ignored by bitcoind. See https://en.bitcoin.it/wiki/BIP_0022 for full specification. """ - # Allow for hex directly - if type(block) == str: + if not isinstance(block, str): hexblock = block else: hexblock = hexlify(block.serialize()) @@ -838,7 +834,7 @@ def combinepsbt(self, psbt_b64s): def converttopsbt(self, tx, permitsigdata=None, iswitness=None): """Returns a base64 encoded PSBT""" - if type(tx) != str: + if not isinstance(tx, str): tx = hexlify(tx.serialize()) return self._call('converttopsbt', tx, permitsigdata, iswitness) @@ -858,7 +854,7 @@ def createpsbt(self, vins, vouts, data="", locktime=0, replaceable=False): vout = i.prevout.n sequence = i.nSequence ins.append({"txid": txid, "vout": vout, "sequence": sequence}) - vins = ins #Allow for JSON data to be passed straight to vins + vins = ins if isinstance(vouts[0], COutPoint): outs = [] for o in vouts: @@ -908,7 +904,7 @@ def utxoupdatepsbt(self, psbt_b64, data): #RAW TX def combinerawtransaction(self, hextxs): """Return raw hex of combined transaction""" - if type(hextxs[0]) != str: + if not isinstance(hextxs[0], str): hextxs = [hexlify(tx.serialize()) for tx in hextxs] return self._call('combinerawtransaction', hextxs) @@ -931,10 +927,9 @@ def getrawtransaction(self, txid, verbose=False, block_hash=None): enabled the transaction may not be available. """ #Timeout issues depending on tx / machine - # Allow handling strings. Desirable? - if type(txid) != str: + if not isinstance(txid, str): txid = b2lx(txid) - if type(block_hash) != str: + if not isinstance(block_hash, str): block_hash = b2lx(block_hash) try: r = self._call('getrawtransaction', txid, 1 if verbose else 0, block_hash) @@ -973,7 +968,7 @@ def sendrawtransactionv0_19(self, tx, maxfeerate=None): maxfeerate - numeric or string for max fee rate """ - if type(tx) != str: + if not isinstance(tx, str): tx = hexlify(tx.serialize()) r = self._call('sendrawtransaction', tx, maxfeerate) return lx(r) @@ -1029,7 +1024,7 @@ def fundrawtransactionv0_19(self, tx, options=None, iswitness=None): 'changepos': Position of added change output, or -1, } """ - if type(tx) != str: + if not isinstance(tx, str): tx = hexlify(tx.serialize()) r = self._call('fundrawtransaction', tx, options, iswitness) r['tx'] = CTransaction.deserialize(unhexlify(r['hex'])) @@ -1043,12 +1038,11 @@ def signrawtransactionwithkey(self, tx, privkeys, prevtxs=None, sighashtype=None prevtxs - JSON object containing info sighashtype - numeric sighashtype default=SIGHASH_ALL """ - # THIS ALLOWS FOR str, bytes, and CBitcoinSecret. desirable? - if type(tx) != str: + if not isinstance(tx, str): tx = hexlify(tx.serialize()) - if isinstance(privkeys[0], CBitcoinSecret): # IS THIS CORRECT + if isinstance(privkeys[0], CBitcoinSecret): privkeys = [str(sk) for sk in privkeys] - elif isinstance(privkeys[0], bytes): # ALLOW FOR BYTES + elif isinstance(privkeys[0], bytes): privkeys = [sk.hex() for sk in privkeys] r = self._call('signrawtransactionwithkey', privkeys, prevtxs, ) r['tx'] = CTransaction.deserialize(unhexlify(r['hex'])) @@ -1057,7 +1051,7 @@ def signrawtransactionwithkey(self, tx, privkeys, prevtxs=None, sighashtype=None def testmempoolaccept(self, txs, maxfeerate=None): """Return a JSON object of each transaction's acceptance info""" - if type(txs[0]) != str: + if not isinstance(txs[0],str): txs = [hexlify(tx.serialize()) for tx in txs] return self._call('testmempoolaccept', txs, maxfeerate) @@ -1085,7 +1079,7 @@ def createmultisig(self, nrequired, keys, address_type=None): } """ - if type(keys[0]) != str: + if not isinstance(keys[0], str): keys = [str(k) for k in keys] r = self._call('createmultisig', nrequired, keys, address_type) # PLEASE CHECK @@ -1098,12 +1092,11 @@ def deriveaddresses(self, descriptor, _range=None): """Returns addresses from descriptor """ - #TODODescriptors need Implementing + #TODO Descriptors need Implementing return self._call('deriveaddresses', descriptor, _range) def estimatesmartfee(self, conf_target, estimate_mode=None): """Returns a JSON object with feerate, errors, and block estimate - #Fix description? conf_target - attempted number of blocks from current tip to place tx estimate_mode: "UNSET" @@ -1150,7 +1143,6 @@ def addmultisigaddress(self, nrequired, keys, label=None, address_type=None): #TODO see if CPubKey.__str__() is used elsewhere or can be changed. if isinstance(keys[0], CBitcoinAddress): keys = [str(k) for k in keys] - #included CPubKey for clarity. Could possibly remove elif isinstance(keys[0], (CPubKey, bytes)): keys = [k.hex() for k in keys] r = self._call('addmultisigaddress', nrequired, keys, label, address_type) @@ -1166,7 +1158,7 @@ def backupwallet(self, destination): def bumpfee(self, txid, options=None): """Bump fee of transation in mempool""" - if type(txid) != str: + if not isinstance(txid, str): txid = b2lx(txid) return self._call('bumpfee', txid, options) @@ -1215,8 +1207,7 @@ def getaccountaddress(self, account=None): def getaddressinfo(self, address): """Return a JSON object of info about address""" - if type(address) != str: - address = str(address) + address = str(address) r = self._call('getaddressinfo', address) if r['script'] == 'scripthash': r['redeemScript'] = CScript.fromhex(r['hex']) @@ -1362,13 +1353,13 @@ def importprunedfunds(self, tx, txout_proof): #TODO should txout_proof be an obj? """ - if type(tx) != str: + if not isinstance(tx, str): tx = hexlify(tx.serialize()) return self._call('importprunedfunds', tx, txout_proof) def importpubkey(self, pubkey, label=None, rescan=None): """Import pubkey as watchonly""" - if type(pubkey) != str: + if not isinstance(pubkey, str): pubkey = pubkey.hex() self._call('importpubkey', pubkey, label, rescan) @@ -1527,7 +1518,7 @@ def lockunspent(self, unlock, outpoints): def removeprunedfunds(self, txid): """Remove pruned utxos from wallet""" - if type(txid) != str: + if not isinstance(txid, str): txid = b2lx(txid) self._call('removeprunedfunds', txid) diff --git a/bitcoin/tests/test_rpc.py b/bitcoin/tests/test_rpc.py index f148cc61..f7820058 100644 --- a/bitcoin/tests/test_rpc.py +++ b/bitcoin/tests/test_rpc.py @@ -165,16 +165,16 @@ def test_verifymessage(self): # else: # pass - def test_gettxout(self): - """Txout disappears if spent, so difficult to set static test""" - if self._IS_ACTIVE: - proxy = bitcoin.rpc.Proxy() - txo = COutPoint(lx("2700507d971a25728a257ed208ba409e7510f861dec928a478ee92f5ef2b4527"), 0) - r = proxy.gettxout(txo) - script = CScript.fromhex("76a9147179f4af7439435720637ee3276aabed1440719188ac") - self.assertEqual(r['txout'].scriptPubKey, script) - else: - pass + # def test_gettxout(self): + # """Txout disappears if spent, so difficult to set static test""" + # if self._IS_ACTIVE: + # proxy = bitcoin.rpc.Proxy() + # txo = COutPoint(lx("2700507d971a25728a257ed208ba409e7510f861dec928a478ee92f5ef2b4527"), 0) + # r = proxy.gettxout(txo) + # script = CScript.fromhex("76a9147179f4af7439435720637ee3276aabed1440719188ac") + # self.assertEqual(r['txout'].scriptPubKey, script) + # else: + # pass def test_getmininginfo(self): From 2bed3eda78f2de3efa153df1d9ccf6c8720405bb Mon Sep 17 00:00:00 2001 From: Pillagr <Pillagr6@gmail.com> Date: Sat, 17 Oct 2020 23:03:13 -0700 Subject: [PATCH 3/4] fix getaddressinfo --- bitcoin/rpc.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/bitcoin/rpc.py b/bitcoin/rpc.py index a0a32a94..fad62328 100644 --- a/bitcoin/rpc.py +++ b/bitcoin/rpc.py @@ -1209,11 +1209,12 @@ def getaddressinfo(self, address): """Return a JSON object of info about address""" address = str(address) r = self._call('getaddressinfo', address) - if r['script'] == 'scripthash': - r['redeemScript'] = CScript.fromhex(r['hex']) - # Keeping with previous style. why not CPubKey? - r['pubkey'] = unhexlify(r['pubkey']) - # PERHAPS ALSO CHANGE ScriptPubKey to CScript? + if r['isscript']: + if r['script'] == 'scripthash': + r['redeemScript'] = CScript.fromhex(r['hex']) + # Keeping with previous style. why not CPubKey? + r['pubkey'] = unhexlify(r['pubkey']) + # PERHAPS ALSO CHANGE ScriptPubKey to CScript? return r def getbalance(self, account='*', minconf=1, include_watchonly=False): From 850e180ae88b98d9a1e5e7f0bad03e9f55d34517 Mon Sep 17 00:00:00 2001 From: Pillagr <Pillagr6@gmail.com> Date: Sun, 18 Oct 2020 12:56:33 -0700 Subject: [PATCH 4/4] fix is_active --- bitcoin/tests/test_rpc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bitcoin/tests/test_rpc.py b/bitcoin/tests/test_rpc.py index f7820058..cb97f8fc 100644 --- a/bitcoin/tests/test_rpc.py +++ b/bitcoin/tests/test_rpc.py @@ -25,6 +25,7 @@ def is_active(): """ try: p = bitcoin.rpc.Proxy() + p.help() return True except ValueError: return False @@ -35,7 +36,7 @@ class Test_RPC(unittest.TestCase): # "Looks like your unit tests won't work if Bitcoin Core isn't running; # maybe they in turn need to check that and disable the test if core isn't available?" # https://github.com/petertodd/python-bitcoinlib/pull/10 - # Sachin Meier: "I've changed it so each test checks against the " + # Sachin Meier: "I've changed it so each test checks against the _IS_ACTIVE variable " #pass def test_getbestblockhash_and_header(self):