Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New blockchain tools #418

Merged
merged 22 commits into from
Nov 16, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions counterpartyd.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def cli(method, params, unsigned):
if not bitcoin.is_valid(source):
raise exceptions.AddressError('Invalid address.')
if bitcoin.is_mine(source):
bitcoin.wallet_unlock()
util.wallet_unlock()
else:
# TODO: Do this only if the encoding method needs it.
print('Source not in backend wallet.')
Expand Down Expand Up @@ -390,7 +390,7 @@ def set_options (data_dir=None, backend_rpc_connect=None,
elif has_config and 'blockchain-service-name' in configfile['Default'] and configfile['Default']['blockchain-service-name']:
config.BLOCKCHAIN_SERVICE_NAME = configfile['Default']['blockchain-service-name']
else:
config.BLOCKCHAIN_SERVICE_NAME = 'blockr'
config.BLOCKCHAIN_SERVICE_NAME = 'jmcorgan'

# custom blockchain service API endpoint
# leave blank to use the default. if specified, include the scheme prefix and port, without a trailing slash (e.g. http://localhost:3001)
Expand Down Expand Up @@ -541,10 +541,7 @@ def balances (address):
address_data = get_address(db, address=address)
balances = address_data['balances']
table = PrettyTable(['Asset', 'Amount'])
if util.is_multisig(address):
btc_balance = '???'
else:
btc_balance = blockchain.getaddressinfo(address)['balance']
btc_balance = bitcoin.get_btc_balance(address)
table.add_row([config.BTC, btc_balance]) # BTC
for balance in balances:
asset = balance['asset']
Expand Down Expand Up @@ -1152,7 +1149,8 @@ def generate_move_random_hash(move):
for i in range(1, num_tries + 1):
try:
blockchain.check()
except: # TODO
except Exception as e: # TODO
logging.exception(e)
logging.warn("Blockchain backend (%s) not yet initialized. Waiting %i seconds and trying again (try %i of %i)..." % (
config.BLOCKCHAIN_SERVICE_NAME, time_wait, i, num_tries))
time.sleep(time_wait)
Expand Down
14 changes: 13 additions & 1 deletion lib/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from jsonrpc import dispatcher
import inspect

from . import (config, bitcoin, exceptions, util)
from . import (config, bitcoin, exceptions, util, blockchain)
from . import (send, order, btcpay, issuance, broadcast, bet, dividend, burn, cancel, callback, rps, rpsresolve, publish)

API_TABLES = ['balances', 'credits', 'debits', 'bets', 'bet_matches',
Expand Down Expand Up @@ -588,6 +588,18 @@ def get_holder_count(asset):
addresses.append(holder['address'])
return { asset: len(set(addresses)) }

@dispatcher.add_method
def search_raw_transactions(address):
return blockchain.searchrawtransactions(address)

@dispatcher.add_method
def get_unspent_txouts(address, return_confirmed=False):
result = bitcoin.get_unspent_txouts(address, return_confirmed=return_confirmed)
if return_confirmed:
return {'all': result[0], 'confirmed': result[1]}
else:
return result

def _set_cors_headers(response):
if config.RPC_ALLOW_CORS:
response.headers['Access-Control-Allow-Origin'] = '*'
Expand Down
197 changes: 66 additions & 131 deletions lib/bitcoin.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def pubkey_to_pubkeyhash(pubkey):
return pubkey
def pubkeyhash_to_pubkey(pubkeyhash):
# TODO: convert to python-bitcoinlib.
raw_transactions = search_raw_transactions(pubkeyhash)
raw_transactions = blockchain.searchrawtransactions(pubkeyhash)
for tx in raw_transactions:
for vin in tx['vin']:
scriptsig = vin['scriptSig']
Expand All @@ -59,50 +59,46 @@ def multisig_pubkeyhashes_to_pubkeys(address):
pubkeys = [pubkeyhash_to_pubkey(pubkeyhash) for pubkeyhash in pubkeyhashes]
return util.construct_array(signatures_required, pubkeys, signatures_possible)

bitcoin_rpc_session = None

def print_coin(coin):
return 'amount: {}; txid: {}; vout: {}; confirmations: {}'.format(coin['amount'], coin['txid'], coin['vout'], coin.get('confirmations', '?')) # simplify and make deterministic


# COMMON
def get_block_count():
return int(rpc('getblockcount', []))
return int(util.rpc('getblockcount', []))
def get_block_hash(block_index):
return rpc('getblockhash', [block_index])
return util.rpc('getblockhash', [block_index])
def get_raw_transaction (tx_hash):
return rpc('getrawtransaction', [tx_hash, 1])
return util.rpc('getrawtransaction', [tx_hash, 1])
def get_block (block_hash):
return rpc('getblock', [block_hash])
return util.rpc('getblock', [block_hash])
def get_block_hash (block_index):
return rpc('getblockhash', [block_index])
return util.rpc('getblockhash', [block_index])
def decode_raw_transaction (unsigned_tx_hex):
return rpc('decoderawtransaction', [unsigned_tx_hex])
return util.rpc('decoderawtransaction', [unsigned_tx_hex])
def get_info():
return rpc('getinfo', [])
return util.rpc('getinfo', [])

# UNCOMMON
def is_valid (address):
return rpc('validateaddress', [address])['isvalid']
return util.rpc('validateaddress', [address])['isvalid']
def is_mine (address):
return rpc('validateaddress', [address])['ismine']
return util.rpc('validateaddress', [address])['ismine']
def sign_raw_transaction (unsigned_tx_hex):
return rpc('signrawtransaction', [unsigned_tx_hex])
return util.rpc('signrawtransaction', [unsigned_tx_hex])
def send_raw_transaction (tx_hex):
return rpc('sendrawtransaction', [tx_hex])
return util.rpc('sendrawtransaction', [tx_hex])
def get_private_key (address):
return rpc('dumpprivkey', [address])
def search_raw_transactions (address):
return rpc('searchrawtransactions', [address, 1, 0, 9999999])
return util.rpc('dumpprivkey', [address])

def get_wallet ():
for group in rpc('listaddressgroupings', []):
for group in util.rpc('listaddressgroupings', []):
for bunch in group:
yield bunch
def get_mempool ():
return rpc('getrawmempool', [])
return util.rpc('getrawmempool', [])
def list_unspent ():
return rpc('listunspent', [0, 999999])
return util.rpc('listunspent', [0, 999999])
def backend_check (db):
"""Checks blocktime of last block to see if {} Core is running behind.""".format(config.BTC_NAME)
block_count = get_block_count()
Expand All @@ -113,74 +109,6 @@ def backend_check (db):
raise exceptions.BitcoindError('Bitcoind is running about {} seconds behind.'.format(round(time_behind)))


def connect (url, payload, headers):
global bitcoin_rpc_session
if not bitcoin_rpc_session: bitcoin_rpc_session = requests.Session()
TRIES = 12
for i in range(TRIES):
try:
response = bitcoin_rpc_session.post(url, data=json.dumps(payload), headers=headers, verify=config.BACKEND_RPC_SSL_VERIFY)
if i > 0: print('Successfully connected.', file=sys.stderr)
return response
except requests.exceptions.SSLError as e:
raise e
except requests.exceptions.ConnectionError:
logging.debug('Could not connect to Bitcoind. (Try {}/{})'.format(i+1, TRIES))
time.sleep(5)
return None

def wallet_unlock ():
getinfo = get_info()
if 'unlocked_until' in getinfo:
if getinfo['unlocked_until'] >= 60:
return True # Wallet is unlocked for at least the next 60 seconds.
else:
passphrase = getpass.getpass('Enter your Bitcoind[‐Qt] wallet passhrase: ')
print('Unlocking wallet for 60 (more) seconds.')
rpc('walletpassphrase', [passphrase, 60])
else:
return True # Wallet is unencrypted.

def rpc (method, params):
starttime = time.time()
headers = {'content-type': 'application/json'}
payload = {
"method": method,
"params": params,
"jsonrpc": "2.0",
"id": 0,
}

response = connect(config.BACKEND_RPC, payload, headers)
if response == None:
if config.TESTNET: network = 'testnet'
else: network = 'mainnet'
raise exceptions.BitcoindRPCError('Cannot communicate with {} Core. ({} is set to run on {}, is {} Core?)'.format(config.BTC_NAME, config.XCP_CLIENT, network, config.BTC_NAME))
elif response.status_code not in (200, 500):
raise exceptions.BitcoindRPCError(str(response.status_code) + ' ' + response.reason)

# Return result, with error handling.
response_json = response.json()
if 'error' not in response_json.keys() or response_json['error'] == None:
return response_json['result']
elif response_json['error']['code'] == -5: # RPC_INVALID_ADDRESS_OR_KEY
raise exceptions.BitcoindError('{} Is txindex enabled in {} Core?'.format(response_json['error'], config.BTC_NAME))
elif response_json['error']['code'] == -4: # Unknown private key (locked wallet?)
# If address in wallet, attempt to unlock.
address = params[0]
if is_valid(address):
if is_mine(address):
raise exceptions.BitcoindError('Wallet is locked.')
else: # When will this happen?
raise exceptions.BitcoindError('Source address not in wallet.')
else:
raise exceptions.AddressError('Invalid address. (Multi‐signature?)')
elif response_json['error']['code'] == -1 and response_json['message'] == 'Block number out of range.':
time.sleep(10)
return get_block_hash(block_index)
else:
raise exceptions.BitcoindError('{}'.format(response_json['error']))

def var_int (i):
if i < 0xfd:
return (i).to_bytes(1, byteorder='little')
Expand Down Expand Up @@ -651,56 +579,63 @@ def get_btc_supply(normalize=False):
blocks_remaining = 0
return total_supply if normalize else int(total_supply * config.UNIT)

def get_unspent_txouts(source):
def get_unspent_txouts(source, return_confirmed=False):
"""returns a list of unspent outputs for a specific address
@return: A list of dicts, with each entry in the dict having the following keys:
"""

# Get all coins.
outputs = {}
if util.is_multisig(source):
pubkeyhashes = util.pubkeyhash_array(source)
outputs = []
raw_transactions = search_raw_transactions(pubkeyhashes[1])
# Get all coins.
raw_transactions = blockchain.searchrawtransactions(pubkeyhashes[1])
else:
pubkeyhashes = [source]
raw_transactions = blockchain.searchrawtransactions(source)

for tx in raw_transactions:
for vout in tx['vout']:
scriptpubkey = vout['scriptPubKey']
if util.is_multisig(source) and scriptpubkey['type'] != 'multisig':
continue
elif 'addresses' in scriptpubkey.keys() and "".join(sorted(scriptpubkey['addresses'])) == "".join(sorted(pubkeyhashes)):
txid = tx['txid']
confirmations = tx['confirmations'] if 'confirmations' in tx else 0
if txid not in outputs or outputs[txid]['confirmations'] < confirmations:
coin = {'amount': float(vout['value']),
'confirmations': confirmations,
'scriptPubKey': scriptpubkey['hex'],
'txid': txid,
'vout': vout['n']
}
outputs[txid] = coin
outputs = outputs.values()

# Prune away spent coins.
unspent = []
confirmed_unspent = []
for output in outputs:
spent = False
confirmed_spent = False
for tx in raw_transactions:
for vout in tx['vout']:
scriptpubkey = vout['scriptPubKey']
if scriptpubkey['type'] == 'multisig' and 'addresses' in scriptpubkey.keys() and len(scriptpubkey['addresses']) == len(pubkeyhashes):
found = True
for pubkeyhash in pubkeyhashes:
if not pubkeyhash in scriptpubkey['addresses']:
found = False
if found:
coin = {'amount': vout['value'],
'confirmations': tx['confirmations'],
'scriptPubKey': scriptpubkey['hex'],
'txid': tx['txid'],
'vout': vout['n']
}
outputs.append(coin)
# Prune away spent coins.
unspent = []
for output in outputs:
spent = False
for tx in raw_transactions:
for vin in tx['vin']:
if (vin['txid'], vin['vout']) == (output['txid'], output['vout']):
spent = True
if not spent:
unspent.append(output)
for vin in tx['vin']:
if 'coinbase' in vin: continue
if (vin['txid'], vin['vout']) == (output['txid'], output['vout']):
spent = True
if 'confirmations' in tx and tx['confirmations'] > 0:
confirmed_spent = True
if not spent:
unspent.append(output)
if not confirmed_spent and output['confirmations'] > 0:
confirmed_unspent.append(output)

if return_confirmed:
return unspent, confirmed_unspent
else:
# TODO: remove account (and address?) fields
if is_mine(source):
wallet_unspent = list_unspent()
unspent = []
for output in wallet_unspent:
try:
if output['address'] == source:
unspent.append(output)
except KeyError:
pass
else:
unspent = blockchain.listunspent(source)
return unspent

return unspent
def get_btc_balance(address, confirmed=True):
all_unspent, confirmed_unspent = get_unspent_txouts(address, return_confirmed=True)
unspent = confirmed_unspent if confirmed else all_unspent
return sum(out['amount'] for out in unspent)

# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
19 changes: 3 additions & 16 deletions lib/blockchain/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,12 @@
import logging

from lib import config
from lib.blockchain import blockr, insight, sochain
from lib.blockchain import insight, jmcorgan, sochain, blockr

# http://test.insight.is/api/sync
def check():
logging.info('Status: Connecting to block explorer.')
return sys.modules['lib.blockchain.{}'.format(config.BLOCKCHAIN_SERVICE_NAME)].check()

# http://test.insight.is/api/status?q=getInfo
def getinfo():
return sys.modules['lib.blockchain.{}'.format(config.BLOCKCHAIN_SERVICE_NAME)].getinfo()

# example: http://test.insight.is/api/addr/mmvP3mTe53qxHdPqXEvdu8WdC7GfQ2vmx5/utxo
def listunspent(address):
return sys.modules['lib.blockchain.{}'.format(config.BLOCKCHAIN_SERVICE_NAME)].listunspent(address)

# example: http://test.insight.is/api/addr/mmvP3mTe53qxHdPqXEvdu8WdC7GfQ2vmx5
def getaddressinfo(address):
return sys.modules['lib.blockchain.{}'.format(config.BLOCKCHAIN_SERVICE_NAME)].getaddressinfo(address)

# example: http://test.insight.is/api/tx/c6b5368c5a256141894972fbd02377b3894aa0df7c35fab5e0eca90de064fdc1
def gettransaction(tx_hash):
return sys.modules['lib.blockchain.{}'.format(config.BLOCKCHAIN_SERVICE_NAME)].gettransaction(tx_hash)
def searchrawtransactions(address):
return sys.modules['lib.blockchain.{}'.format(config.BLOCKCHAIN_SERVICE_NAME)].searchrawtransactions(address)
Loading