class BtcBlockMapper(object): def __init__(self, transaction_mapper=None): if transaction_mapper is None: self.transaction_mapper = BtcTransactionMapper() else: self.transaction_mapper = transaction_mapper def json_dict_to_block(self, json_dict): block = BtcBlock() block.hash = json_dict.get('hash') block.size = json_dict.get('size') block.stripped_size = json_dict.get('strippedsize') block.weight = json_dict.get('weight') block.number = json_dict.get('height') block.version = json_dict.get('version') block.merkle_root = json_dict.get('merkleroot') block.timestamp = json_dict.get('time') # bitcoin and all clones except zcash return integer nonce, zcash return hex string block.nonce = to_hex(json_dict.get('nonce')) block.bits = json_dict.get('bits') raw_transactions = json_dict.get('tx') if raw_transactions is not None and len(raw_transactions) > 0: if isinstance(raw_transactions[0], dict): block.transactions = [ self.transaction_mapper.json_dict_to_transaction( tx, block) for tx in raw_transactions ] else: # Transaction hashes block.transactions = raw_transactions block.transaction_count = len(raw_transactions) return block def block_to_dict(self, block): return { 'type': 'block', 'hash': block.hash, 'size': block.size, 'stripped_size': block.stripped_size, 'weight': block.weight, 'number': block.number, 'version': block.version, 'merkle_root': block.merkle_root, 'timestamp': block.timestamp, 'nonce': block.nonce, 'bits': block.bits, 'coinbase_param': block.coinbase_param, 'transaction_count': len(block.transactions) }
class BtcService(object): def __init__(self, bitcoin_rpc, chain=Chain.BITCOIN, coin_price_type=CoinPriceType.empty): self.bitcoin_rpc = bitcoin_rpc self.block_mapper = BtcBlockMapper() self.transaction_mapper = BtcTransactionMapper() self.chain = chain self.coin_price_type = coin_price_type self.cached_prices = {} def get_block(self, block_number, with_transactions=False): block_hashes = self.get_block_hashes([block_number]) blocks = self.get_blocks_by_hashes(block_hashes, with_transactions) return blocks[0] if len(blocks) > 0 else None def get_genesis_block(self, with_transactions=False): return self.get_block(0, with_transactions) def get_latest_block(self, with_transactions=False): block_number = self.bitcoin_rpc.getblockcount() return self.get_block(block_number, with_transactions) def get_blocks(self, block_number_batch, with_transactions=False): if not block_number_batch: return [] block_hashes = self.get_block_hashes(block_number_batch) return self.get_blocks_by_hashes(block_hashes, with_transactions) def get_blocks_by_hashes(self, block_hash_batch, with_transactions=True): if not block_hash_batch: return [] # get block details by hash block_detail_rpc = list( generate_get_block_by_hash_json_rpc(block_hash_batch, with_transactions, self.chain)) block_detail_response = self.bitcoin_rpc.batch(block_detail_rpc) block_detail_results = list( rpc_response_batch_to_results(block_detail_response)) blocks = [ self.block_mapper.json_dict_to_block(block_detail_result) for block_detail_result in block_detail_results ] if self.chain in Chain.HAVE_OLD_API and with_transactions: self._fetch_transactions(blocks) self._add_coin_price_to_blocks(blocks, self.coin_price_type) for block in blocks: self._remove_coinbase_input(block) if block.has_full_transactions(): for transaction in block.transactions: self._add_coin_price_to_transaction( transaction, block.coin_price_usd) self._add_non_standard_addresses(transaction) if self.chain == Chain.ZCASH: self._add_shielded_inputs_and_outputs(transaction) return blocks def get_block_hashes(self, block_number_batch): block_hash_rpc = list( generate_get_block_hash_by_number_json_rpc(block_number_batch)) block_hashes_response = self.bitcoin_rpc.batch(block_hash_rpc) block_hashes = rpc_response_batch_to_results(block_hashes_response) return block_hashes def get_transactions_by_hashes(self, hashes): if hashes is None or len(hashes) == 0: return [] raw_transactions = self._get_raw_transactions_by_hashes_batched(hashes) transactions = [ self.transaction_mapper.json_dict_to_transaction(tx) for tx in raw_transactions ] for transaction in transactions: self._add_non_standard_addresses(transaction) if self.chain == Chain.ZCASH: self._add_shielded_inputs_and_outputs(transaction) return transactions def _fetch_transactions(self, blocks): all_transaction_hashes = [block.transactions for block in blocks] flat_transaction_hashes = [ hash for transaction_hashes in all_transaction_hashes for hash in transaction_hashes ] raw_transactions = self._get_raw_transactions_by_hashes_batched( flat_transaction_hashes) for block in blocks: raw_block_transactions = [ tx for tx in raw_transactions if tx.get('txid') in block.transactions ] block.transactions = [ self.transaction_mapper.json_dict_to_transaction(tx, block) for tx in raw_block_transactions ] def _get_raw_transactions_by_hashes_batched(self, hashes): if hashes is None or len(hashes) == 0: return [] result = [] batch_size = 100 for batch in dynamic_batch_iterator(hashes, lambda: batch_size): result.extend(self._get_raw_transactions_by_hashes(batch)) return result def _get_raw_transactions_by_hashes(self, hashes): if hashes is None or len(hashes) == 0: return [] genesis_transaction = GENESIS_TRANSACTIONS.get(self.chain) genesis_transaction_hash = genesis_transaction.get( 'txid') if genesis_transaction is not None else None filtered_hashes = [ transaction_hash for transaction_hash in hashes if transaction_hash != genesis_transaction_hash ] transaction_detail_rpc = list( generate_get_transaction_by_id_json_rpc(filtered_hashes)) transaction_detail_response = self.bitcoin_rpc.batch( transaction_detail_rpc) transaction_detail_results = rpc_response_batch_to_results( transaction_detail_response) raw_transactions = list(transaction_detail_results) if genesis_transaction_hash is not None and genesis_transaction_hash in hashes: raw_transactions.append(genesis_transaction) return raw_transactions def _remove_coinbase_input(self, block): if block.has_full_transactions(): for transaction in block.transactions: coinbase_inputs = [ input for input in transaction.inputs if input.is_coinbase() ] if len(coinbase_inputs) > 1: raise ValueError( 'There must be no more than 1 coinbase input in any transaction. Was {}, hash {}' .format(len(coinbase_inputs), transaction.hash)) coinbase_input = coinbase_inputs[0] if len( coinbase_inputs) > 0 else None if coinbase_input is not None: block.coinbase_param = coinbase_input.coinbase_param transaction.inputs = [ input for input in transaction.inputs if not input.is_coinbase() ] transaction.is_coinbase = True block.coinbase_param = coinbase_input.coinbase_param block.coinbase_param_decoded = bytes.fromhex( coinbase_input.coinbase_param).decode( 'utf-8', 'replace') block.coinbase_tx = transaction block.coinbase_txid = transaction.transaction_id block.block_reward = self.get_block_reward(block) transaction.input_count = 0 def _add_non_standard_addresses(self, transaction): for output in transaction.outputs: if output.addresses is None or len(output.addresses) == 0: output.type = 'nonstandard' output.addresses = [ script_hex_to_non_standard_address(output.script_hex) ] def _add_shielded_inputs_and_outputs(self, transaction): if transaction.join_splits is not None and len( transaction.join_splits) > 0: for join_split in transaction.join_splits: input_value = join_split.public_input_value or 0 output_value = join_split.public_output_value or 0 if input_value > 0: input = BtcTransactionInput() input.type = ADDRESS_TYPE_SHIELDED input.value = input_value transaction.add_input(input) if output_value > 0: output = BtcTransactionOutput() output.type = ADDRESS_TYPE_SHIELDED output.value = output_value transaction.add_output(output) if transaction.value_balance is not None and transaction.value_balance != 0: if transaction.value_balance > 0: input = BtcTransactionInput() input.type = ADDRESS_TYPE_SHIELDED input.value = transaction.value_balance transaction.add_input(input) if transaction.value_balance < 0: output = BtcTransactionOutput() output.type = ADDRESS_TYPE_SHIELDED output.value = -transaction.value_balance transaction.add_output(output) def get_block_reward(self, block): return block.coinbase_tx.calculate_output_value() def _add_coin_price_to_blocks(self, blocks, coin_price_type): from_currency_code = Chain.ticker_symbol(self.chain) if not from_currency_code or coin_price_type == CoinPriceType.empty: return elif coin_price_type == CoinPriceType.hourly: block_hour_ids = list( set([get_hour_id_from_ts(block.timestamp) for block in blocks])) block_hours_ts = { hour_id: get_ts_from_hour_id(hour_id) for hour_id in block_hour_ids } for hour_id, hour_ts in block_hours_ts.items(): if hour_id in self.cached_prices: continue self.cached_prices[hour_id] = get_coin_price( from_currency_code=from_currency_code, timestamp=hour_ts, resource="histohour") for block in blocks: block_hour_id = get_hour_id_from_ts(block.timestamp) block.coin_price_usd = self.cached_prices[block_hour_id] elif coin_price_type == CoinPriceType.daily: block_day_ids = list( set([get_day_id_from_ts(block.timestamp) for block in blocks])) block_days_ts = { day_id: get_ts_from_day_id(day_id) for day_id in block_day_ids } for day_id, day_ts in block_days_ts.items(): if day_id in self.cached_prices: continue self.cached_prices[day_id] = get_coin_price( from_currency_code=from_currency_code, timestamp=day_ts, resource="histoday") for block in blocks: block_day_id = get_day_id_from_ts(block.timestamp) block.coin_price_usd = self.cached_prices[block_day_id] def _add_coin_price_to_transaction(self, transaction, coin_price_usd): transaction.coin_price_usd = coin_price_usd
class BtcService(object): def __init__(self, bitcoin_rpc, chain=Chain.BITCOIN): self.bitcoin_rpc = bitcoin_rpc self.block_mapper = BtcBlockMapper() self.transaction_mapper = BtcTransactionMapper() self.chain = chain def get_block(self, block_number, with_transactions=False): block_hashes = self.get_block_hashes([block_number]) blocks = self.get_blocks_by_hashes(block_hashes, with_transactions) return blocks[0] if len(blocks) > 0 else None def get_genesis_block(self, with_transactions=False): return self.get_block(0, with_transactions) def get_latest_block(self, with_transactions=False): block_number = self.bitcoin_rpc.getblockcount() return self.get_block(block_number, with_transactions) def get_blocks(self, block_number_batch, with_transactions=False): if not block_number_batch: return [] block_hashes = self.get_block_hashes(block_number_batch) return self.get_blocks_by_hashes(block_hashes, with_transactions) def get_blocks_by_hashes(self, block_hash_batch, with_transactions=True): if not block_hash_batch: return [] # get block details by hash block_detail_rpc = list( generate_get_block_by_hash_json_rpc(block_hash_batch, with_transactions, self.chain)) block_detail_response = self.bitcoin_rpc.batch(block_detail_rpc) block_detail_results = list( rpc_response_batch_to_results(block_detail_response)) blocks = [ self.block_mapper.json_dict_to_block(block_detail_result) for block_detail_result in block_detail_results ] if self.chain in Chain.HAVE_OLD_API and with_transactions: self._fetch_transactions(blocks) for block in blocks: self._remove_coinbase_input(block) self._add_non_standard_addresses(block) if self.chain == Chain.ZCASH: self._add_shielded_inputs_and_outputs(block) return blocks def get_block_hashes(self, block_number_batch): block_hash_rpc = list( generate_get_block_hash_by_number_json_rpc(block_number_batch)) block_hashes_response = self.bitcoin_rpc.batch(block_hash_rpc) block_hashes = rpc_response_batch_to_results(block_hashes_response) return block_hashes def _fetch_transactions(self, blocks): all_transaction_hashes = [block.transactions for block in blocks] flat_transaction_hashes = [ hash for transaction_hashes in all_transaction_hashes for hash in transaction_hashes ] raw_transactions = self._get_raw_transactions_by_hashes_batched( flat_transaction_hashes) for block in blocks: raw_block_transactions = [ tx for tx in raw_transactions if tx.get('txid') in block.transactions ] block.transactions = [ self.transaction_mapper.json_dict_to_transaction(tx, block) for tx in raw_block_transactions ] def _get_raw_transactions_by_hashes_batched(self, hashes): if hashes is None or len(hashes) == 0: return [] result = [] batch_size = 100 for batch in dynamic_batch_iterator(hashes, lambda: batch_size): result.extend(self._get_raw_transactions_by_hashes(batch)) return result def _get_raw_transactions_by_hashes(self, hashes): if hashes is None or len(hashes) == 0: return [] genesis_transaction = GENESIS_TRANSACTIONS.get(self.chain) genesis_transaction_hash = genesis_transaction.get( 'txid') if genesis_transaction is not None else None filtered_hashes = [ transaction_hash for transaction_hash in hashes if transaction_hash != genesis_transaction_hash ] transaction_detail_rpc = list( generate_get_transaction_by_id_json_rpc(filtered_hashes)) transaction_detail_response = self.bitcoin_rpc.batch( transaction_detail_rpc) transaction_detail_results = rpc_response_batch_to_results( transaction_detail_response) raw_transactions = list(transaction_detail_results) if genesis_transaction_hash is not None and genesis_transaction_hash in hashes: raw_transactions.append(genesis_transaction) return raw_transactions def _remove_coinbase_input(self, block): if block.has_full_transactions(): for transaction in block.transactions: coinbase_inputs = [ input for input in transaction.inputs if input.is_coinbase() ] if len(coinbase_inputs) > 1: raise ValueError( 'There must be no more than 1 coinbase input in any transaction. Was {}, hash {}' .format(len(coinbase_inputs), transaction.hash)) coinbase_input = coinbase_inputs[0] if len( coinbase_inputs) > 0 else None if coinbase_input is not None: block.coinbase_param = coinbase_input.coinbase_param transaction.inputs = [ input for input in transaction.inputs if not input.is_coinbase() ] transaction.is_coinbase = True def _add_non_standard_addresses(self, block): if block.has_full_transactions(): for transaction in block.transactions: for output in transaction.outputs: if output.addresses is None or len(output.addresses) == 0: output.type = 'nonstandard' output.addresses = [ script_hex_to_non_standard_address( output.script_hex) ] def _add_shielded_inputs_and_outputs(self, block): if block.has_full_transactions(): for transaction in block.transactions: if transaction.join_splits is not None and len( transaction.join_splits) > 0: for join_split in transaction.join_splits: input_value = join_split.public_input_value or 0 output_value = join_split.public_output_value or 0 if input_value > 0: input = BtcTransactionInput() input.type = ADDRESS_TYPE_SHIELDED input.value = input_value transaction.add_input(input) if output_value > 0: output = BtcTransactionOutput() output.type = ADDRESS_TYPE_SHIELDED output.value = output_value transaction.add_output(output) if transaction.value_balance is not None and transaction.value_balance != 0: if transaction.value_balance > 0: input = BtcTransactionInput() input.type = ADDRESS_TYPE_SHIELDED input.value = transaction.value_balance transaction.add_input(input) if transaction.value_balance < 0: output = BtcTransactionOutput() output.type = ADDRESS_TYPE_SHIELDED output.value = transaction.value_balance transaction.add_output(output)
class BtcBlockMapper(object): def __init__(self, transaction_mapper=None): if transaction_mapper is None: self.transaction_mapper = BtcTransactionMapper() else: self.transaction_mapper = transaction_mapper def json_dict_to_block(self, json_dict): block = BtcBlock() block.hash = json_dict.get('hash') block.size = json_dict.get('size') block.stripped_size = json_dict.get('strippedsize') block.weight = json_dict.get('weight') block.number = json_dict.get('height') block.version = json_dict.get('version') block.merkle_root = json_dict.get('merkleroot') block.timestamp = json_dict.get('time') # bitcoin and all clones except zcash return integer nonce, zcash return hex string block.nonce = to_hex(json_dict.get('nonce')) block.bits = json_dict.get('bits') raw_transactions = json_dict.get('tx') if raw_transactions is not None and len(raw_transactions) > 0: if isinstance(raw_transactions[0], dict): block.transactions = [ self.transaction_mapper.json_dict_to_transaction( tx, block, idx) for idx, tx in enumerate(raw_transactions) ] block.transaction_ids = [ tx.transaction_id for tx in block.transactions ] else: # Transaction hashes block.transactions = raw_transactions block.transaction_ids = raw_transactions block.transaction_count = len(raw_transactions) # New fields block.transaction_count = json_dict.get("nTx") block.version_hex = json_dict.get("versionHex") block.median_timestamp = json_dict.get("mediantime") block.difficulty = int(json_dict.get("difficulty")) block.chain_work = json_dict.get("chainwork") block.coinbase_txid = json_dict.get("coinbase_txid") block.previous_block_hash = json_dict.get("previousblockhash") block.coin_price_usd = json_dict.get('coin_price_usd') return block def block_to_dict(self, block): return { 'type': 'block', 'hash': block.hash, 'size': block.size, 'stripped_size': block.stripped_size, 'weight': block.weight, 'number': block.number, 'version': block.version, 'merkle_root': block.merkle_root, 'timestamp': block.timestamp, 'nonce': block.nonce, 'bits': block.bits, 'coinbase_param': block.coinbase_param, 'coinbase_param_decoded': block.coinbase_param_decoded, 'coinbase_txid': block.coinbase_txid, 'transaction_count': block.transaction_count, 'block_reward': block.block_reward, 'version_hex': block.version_hex, 'median_timestamp': block.median_timestamp, 'difficulty': block.difficulty, 'chain_work': block.chain_work, 'previous_block_hash': block.previous_block_hash, "coin_price_usd": block.coin_price_usd, "transaction_ids": block.transaction_ids }