diff --git a/electrumx/lib/coins.py b/electrumx/lib/coins.py index e5f2fcfa..95d9b1e0 100644 --- a/electrumx/lib/coins.py +++ b/electrumx/lib/coins.py @@ -283,6 +283,14 @@ def bucket_estimatefee_block_target(cls, n: int) -> int: return n +class AtomicalsCoinMixin: + ATOMICALS_ACTIVATION_HEIGHT: int + ATOMICALS_ACTIVATION_HEIGHT_DMINT: int + ATOMICALS_ACTIVATION_HEIGHT_COMMITZ: int + ATOMICALS_ACTIVATION_HEIGHT_DENSITY: int + ATOMICALS_ACTIVATION_HEIGHT_DFT_BITWORK_ROLLOVER: int + + class AuxPowMixin: STATIC_BLOCK_HEADERS = False DESERIALIZER = lib_tx.DeserializerAuxPow @@ -629,7 +637,7 @@ def warn_old_client_on_tx_broadcast(cls, client_ver): return False -class Bitcoin(BitcoinMixin, Coin): +class Bitcoin(BitcoinMixin, AtomicalsCoinMixin, Coin): NAME = "Bitcoin" DESERIALIZER = lib_tx.DeserializerSegWit MEMPOOL_HISTOGRAM_REFRESH_SECS = 120 @@ -913,7 +921,7 @@ class BitcoinSVRegtest(BitcoinSVTestnet): GENESIS_ACTIVATION = 10_000 -class BitcoinTestnet(BitcoinTestnetMixin, Coin): +class BitcoinTestnet(BitcoinTestnetMixin, AtomicalsCoinMixin, Coin): '''Bitcoin Testnet for Core bitcoind >= 0.13.1.''' NAME = "Bitcoin" DESERIALIZER = lib_tx.DeserializerSegWit diff --git a/electrumx/server/block_processor.py b/electrumx/server/block_processor.py index 94971c9a..bf8e9e4b 100644 --- a/electrumx/server/block_processor.py +++ b/electrumx/server/block_processor.py @@ -11,7 +11,7 @@ import asyncio import os import time -from typing import Sequence, Tuple, List, Callable, Optional, TYPE_CHECKING, Type +from typing import Sequence, Tuple, List, Callable, Optional, TYPE_CHECKING, Type, Union from aiorpcx import run_in_thread, CancelledError @@ -78,7 +78,7 @@ import copy if TYPE_CHECKING: - from electrumx.lib.coins import Coin + from electrumx.lib.coins import Coin, AtomicalsCoinMixin from electrumx.server.env import Env from electrumx.server.controller import Notifications @@ -101,7 +101,7 @@ class Prefetcher: def __init__( self, daemon: 'Daemon', - coin: Type['Coin'], + coin: Type[Union['Coin', 'AtomicalsCoinMixin']], blocks_event: asyncio.Event, *, polling_delay_secs, @@ -297,12 +297,13 @@ def __init__(self, env: 'Env', db: DB, daemon: Daemon, notifications: 'Notificat "dft": 20, "dat": 21, "split": 22, "splat": 23, "seal": 24, "evt": 25, "mod": 26, "transfer": 30, - "payment-subrealm": 40, "payment-dmitem": 41, + "payment-subrealm": 40, "payment-dmitem": 41, "payment-subrealm-failed": 42, "payment-dmitem-failed": 43, "mint-dft-failed": 51, "mint-ft-failed": 52, "mint-nft-failed": 53, "mint-nft-realm-failed": 54, "mint-nft-subrealm-failed": 55, "mint-nft-container-failed": 56, "mint-nft-dmitem-failed": 57, "invalid-mint": 59, "burn": 70, } + self.op_list_vk = {v: k for k, v in self.op_list.items()} async def run_in_thread_with_lock(self, func, *args): # Run in a thread to prevent blocking. Shielded so that @@ -658,7 +659,7 @@ def get_expected_subrealm_payment_info(self, found_atomical_id_for_potential_sub assert(request_parent_realm_id_compact == parent_realm_id_compact) if isinstance(parent_realm_id_compact, str) and is_compact_atomical_id(parent_realm_id_compact): # We have a validated potential parent id, now look it up to see if the parent is a valid atomical - found_parent_mint_info = self.get_base_mint_info_by_atomical_id(parent_realm_id) + found_parent_mint_info = self.get_base_mint_info_by_atomical_id(parent_realm_id, height=current_height) if found_parent_mint_info: # We have found the parent atomical, which may or may not be a valid realm # Do the basic check for $request_realm which indicates it succeeded the basic validity checks @@ -747,8 +748,7 @@ def get_expected_dmitem_payment_info(self, found_atomical_id_for_potential_dmite self.logger.info(f'get_expected_dmitem_payment_info: parent_container_id_compact not string or compact atomical id {location_id_bytes_to_compact(found_atomical_id_for_potential_dmitem)} parent_container_id_compact={parent_container_id_compact}') return None, None, None, None # We have a validated potential parent id, now look it up to see if the parent is a valid atomical - # found_parent_mint_info = self.get_base_mint_info_by_atomical_id(parent_container_id) - found_parent_mint_info = self.get_base_mint_info_by_atomical_id(parent_container_id) + found_parent_mint_info = self.get_base_mint_info_by_atomical_id(parent_container_id, height=current_height) if not found_parent_mint_info: self.logger.info(f'get_expected_dmitem_payment_info: not found_parent_mint_info found_atomical_id_for_potential_dmitem={location_id_bytes_to_compact(found_atomical_id_for_potential_dmitem)} parent_container_id_compact={parent_container_id_compact} found_atomical_mint_info_for_potential_dmitem={found_atomical_mint_info_for_potential_dmitem}') return None, None, None, None @@ -2209,7 +2209,8 @@ def get_raw_mint_info_by_atomical_id_notused(self, atomical_id): # Get the atomical details base info # Does not retrieve the active b'a' locations in this method because there could be many thousands (in the case of FTs) # Another method is provided to layer on the active location and gives the user control over whether to retrieve them - def get_base_mint_info_by_atomical_id(self, atomical_id): + def get_base_mint_info_by_atomical_id(self, atomical_id, height: Optional[int] = None): + height = height or self.height init_mint_info = self.get_atomicals_id_mint_info(atomical_id, True) if not init_mint_info: return None @@ -2329,9 +2330,9 @@ def get_base_mint_info_by_atomical_id(self, atomical_id): atomical['$parents'] = parents # Resolve any name like details such as realms, subrealms, containers and tickers - self.populate_extended_atomical_subtype_info(atomical) + self.populate_extended_atomical_subtype_info(atomical, height) self.populate_sealed_status(atomical) - self.populate_container_dmint_status(atomical) + self.populate_container_dmint_status(atomical, height) return atomical @@ -2349,10 +2350,10 @@ def populate_sealed_status(self, atomical): atomical['$sealed'] = location_id_bytes_to_compact(sealed_location) # Populate the sealed status of an atomical - def populate_container_dmint_status(self, atomical): + def populate_container_dmint_status(self, atomical, height: int): if not atomical.get('$container'): return - status = self.make_container_dmint_status_by_atomical_id_at_height(atomical['atomical_id'], self.height) + status = self.make_container_dmint_status_by_atomical_id_at_height(atomical['atomical_id'], height) if not status: return atomical['$container_dmint_status'] = status @@ -2396,9 +2397,9 @@ def build_atomical_id_to_candidate_map(self, raw_candidate_entries): return atomical_id_to_candidates_map # Populate the requested full realm name to provide context for a subrealm request - def populate_request_full_realm_name(self, atomical, pid, request_subrealm): + def populate_request_full_realm_name(self, atomical, pid, request_subrealm, height: Optional[int] = None): # Resolve the parent realm to get the parent realm path and construct the full_realm_name - parent_realm = self.get_base_mint_info_by_atomical_id(pid) + parent_realm = self.get_base_mint_info_by_atomical_id(pid, height) if not parent_realm: atomical_id = atomical['mint_info']['id'] raise IndexError(f'populate_request_full_realm_name: parent realm not found atomical_id={atomical_id}, parent_realm={parent_realm}') @@ -2466,11 +2467,10 @@ def build_applicable_rule_map_dmitem(self, all_entries, arg_pid, arg_request_dmi return applicable_rule_map # Populate the specific name or request type for containers, tickers, and realms (sub-realms excluded) - def populate_name_subtype_specific_fields(self, atomical, type_str, get_effective_name_func): + def populate_name_subtype_specific_fields(self, atomical, type_str, get_effective_name_func, height: int): request_name = atomical['mint_info'].get('$request_' + type_str) if not request_name: return None, None - height = self.height status, candidate_id, raw_candidate_entries = get_effective_name_func(request_name, height) atomical['$' + type_str + '_candidates'] = format_name_type_candidates_to_rpc(raw_candidate_entries, self.build_atomical_id_to_candidate_map(raw_candidate_entries)) atomical['$request_' + type_str + '_status'] = get_name_request_candidate_status(atomical, status, candidate_id, type_str) @@ -2479,18 +2479,17 @@ def populate_name_subtype_specific_fields(self, atomical, type_str, get_effectiv return request_name, status == 'verified' and atomical['atomical_id'] == candidate_id # Populate the specific subrealm request type information - def populate_subrealm_subtype_specific_fields(self, atomical): + def populate_subrealm_subtype_specific_fields(self, atomical, height: int): # Check if the effective subrealm is for the current atomical and also resolve its parent. request_subrealm = atomical['mint_info'].get('$request_subrealm') if not request_subrealm: return None, None pid_compact = atomical['mint_info']['$parent_realm'] pid = compact_to_location_id_bytes(pid_compact) - height = self.height status, candidate_id, raw_candidate_entries = self.get_effective_subrealm(pid, request_subrealm, height) atomical['subtype'] = 'request_subrealm' # Will change to 'subrealm' if it is found to be valid # Populate the requested full realm name - self.populate_request_full_realm_name(atomical, pid, request_subrealm) + self.populate_request_full_realm_name(atomical, pid, request_subrealm, height) # Build the applicable rule set mapping of atomical_id to the rule that will need to be matched and paid. # We use this information to display to each candidate what rule would apply to their mint # and how much to pay and by which block height they must submit their payment @@ -2515,7 +2514,7 @@ def populate_subrealm_subtype_specific_fields(self, atomical): atomical['$request_subrealm'] = atomical['mint_info'].get('$request_subrealm') atomical['$parent_realm'] = pid_compact # Resolve the parent realm to get the parent realm path and construct the `full_realm_name`. - parent_realm = self.get_base_mint_info_by_atomical_id(pid) + parent_realm = self.get_base_mint_info_by_atomical_id(pid, height) if not parent_realm: atomical_id = atomical['mint_info']['id'] raise IndexError( @@ -2538,14 +2537,13 @@ def populate_subrealm_subtype_specific_fields(self, atomical): return request_subrealm, False # Populate the specific dmitem request type information - def populate_dmitem_subtype_specific_fields(self, atomical): + def populate_dmitem_subtype_specific_fields(self, atomical, height: int): # Check if the effective dmitem is for the current atomical and also resolve its parent. request_dmitem = atomical['mint_info'].get('$request_dmitem') if not request_dmitem: return None, None pid_compact = atomical['mint_info']['$parent_container'] pid = compact_to_location_id_bytes(pid_compact) - height = self.height status, candidate_id, raw_candidate_entries = self.get_effective_dmitem(pid, request_dmitem, height) atomical['subtype'] = 'request_dmitem' # Will change to 'dmitem' if it is found to be valid. # Build the applicable rule set mapping of atomical_id to the rule that will need to be matched and paid. @@ -2574,7 +2572,7 @@ def populate_dmitem_subtype_specific_fields(self, atomical): atomical['$request_dmitem'] = atomical['mint_info'].get('$request_dmitem') atomical['$parent_container'] = pid_compact # Resolve the parent to get the parent path and construct the `parent_container_name`. - parent_container = self.get_base_mint_info_by_atomical_id(pid) + parent_container = self.get_base_mint_info_by_atomical_id(pid, height) if not parent_container: atomical_id = atomical['mint_info']['id'] raise IndexError( @@ -2596,11 +2594,11 @@ def populate_dmitem_subtype_specific_fields(self, atomical): # Populate the subtype information such as realms, subrealms, containers and tickers # An atomical can have a naming element if it passed all the validity checks of the assignment # and for that reason there is the concept of "effective" name which is based on a commit/reveal delay pattern - def populate_extended_atomical_subtype_info(self, atomical): + def populate_extended_atomical_subtype_info(self, atomical, height: int): # # TOP-REALM (TLR) Type Fields # - the_name_request, is_atomical_name_verified_found = self.populate_name_subtype_specific_fields(atomical, 'realm', self.get_effective_realm) + the_name_request, is_atomical_name_verified_found = self.populate_name_subtype_specific_fields(atomical, 'realm', self.get_effective_realm, height) if is_atomical_name_verified_found: atomical['subtype'] = 'realm' atomical['$realm'] = the_name_request @@ -2613,7 +2611,7 @@ def populate_extended_atomical_subtype_info(self, atomical): # # CONTAINER Type Fields # - the_name_request, is_atomical_name_verified_found = self.populate_name_subtype_specific_fields(atomical, 'container', self.get_effective_container) + the_name_request, is_atomical_name_verified_found = self.populate_name_subtype_specific_fields(atomical, 'container', self.get_effective_container, height) if is_atomical_name_verified_found: atomical['subtype'] = 'container' atomical['$container'] = the_name_request @@ -2625,7 +2623,7 @@ def populate_extended_atomical_subtype_info(self, atomical): # # TICKER NAME FIELDS # - the_name_request, is_atomical_name_verified_found = self.populate_name_subtype_specific_fields(atomical, 'ticker', self.get_effective_ticker) + the_name_request, is_atomical_name_verified_found = self.populate_name_subtype_specific_fields(atomical, 'ticker', self.get_effective_ticker, height) if is_atomical_name_verified_found: atomical['$ticker'] = the_name_request return atomical @@ -2636,12 +2634,13 @@ def populate_extended_atomical_subtype_info(self, atomical): # SUBREALM type fields # # The method populates all the fields and nothing more needs to be done at this level for subrealms - self.populate_subrealm_subtype_specific_fields(atomical) + self.populate_subrealm_subtype_specific_fields(atomical, height) # # DMITEM type fields # # The method populates all the fields and nothing more needs to be done at this level for dmitems - self.populate_dmitem_subtype_specific_fields(atomical) + self.populate_dmitem_subtype_specific_fields(atomical, height) + return atomical def is_dft_bitwork_rollover_activated(self, height): @@ -2831,8 +2830,8 @@ def build_atomicals_spent_at_inputs_for_validation_only(self, tx): if len(atomicals_transferred_list): atomicals_spent_at_inputs[txin_index] = atomicals_transferred_list txin_index += 1 - return atomicals_spent_at_inputs - + return atomicals_spent_at_inputs + # Builds a map of the atomicals spent at a tx # It uses the spend_atomicals_utxo method but with live_run == False def build_atomicals_receive_at_ouutput_for_validation_only(self, tx, txid): @@ -2853,7 +2852,7 @@ def advance_txs( is_unspendable: Callable[[bytes], bool], header, height - ) -> Sequence[bytes]: + ) -> tuple[list[bytes], list[bytes]]: self.tx_hashes.append(b''.join(tx_hash for tx, tx_hash in txs)) self.atomicals_rpc_format_cache.clear() self.atomicals_rpc_general_cache.clear() @@ -2876,8 +2875,8 @@ def advance_txs( prev_atomicals_block_hash = self.get_general_data_with_cache(b'tt' + pack_le_uint32(height - 1)) concatenation_of_tx_hashes_with_valid_atomical_operation.append(block_header_hash + prev_atomicals_block_hash) # Use local vars for speed in the loops - undo_info = [] - atomicals_undo_info = [] + undo_info: list[bytes] = [] + atomicals_undo_info: list[bytes] = [] tx_num = self.tx_count atomical_num = self.atomical_count script_hashX = self.coin.hashX_from_script @@ -2893,7 +2892,7 @@ def advance_txs( to_le_uint32 = pack_le_uint32 to_le_uint64 = pack_le_uint64 to_be_uint64 = pack_be_uint64 - + # track which dft tickers have mints to perform a sanity check at the end atomical_ids_which_have_valid_dft_mints = {} # Speed up distmint processing by caching the ticker mint request info @@ -2913,7 +2912,7 @@ def advance_txs( cache_value = spend_utxo(txin.prev_hash, txin.prev_idx) undo_info_append(cache_value) append_hashX(cache_value[:HASHX_LEN]) - + # Only search and spend atomicals utxos if activated if self.is_atomicals_activated(height): # Find all the existing transferred atomicals and spend the Atomicals utxos @@ -2930,7 +2929,7 @@ def advance_txs( atomicals_undo_info_extend(reformatted_for_undo_entries) txin_index = txin_index + 1 - + # Add the new UTXOs for idx, txout in enumerate(tx.outputs): # Ignore unspendable outputs @@ -2940,7 +2939,7 @@ def advance_txs( hashX = self.coin.hashX_from_script(txout.pk_script) append_hashX(hashX) put_utxo(tx_hash + to_le_uint32(idx), hashX + tx_numb + to_le_uint64(txout.value)) - + # Only create Atomicals if the activation height is reached if self.is_atomicals_activated(height): # Save the tx number for the current tx @@ -2962,7 +2961,7 @@ def advance_txs( reveal_location_txid = atomicals_operations_found_at_inputs['reveal_location_txid'] reveal_location_index = atomicals_operations_found_at_inputs['reveal_location_index'] self.logger.debug(f'advance_txs: atomicals_operations_found_at_inputs operation_found={operation_found}, operation_input_index={operation_input_index}, size_payload={size_payload}, tx_hash={hash_to_hex_str(tx_hash)}, commit_txid={hash_to_hex_str(commit_txid)}, commit_index={commit_index}, reveal_location_txid={hash_to_hex_str(reveal_location_txid)}, reveal_location_index={reveal_location_index}') - + # Color the outputs of any transferred NFT/FT atomicals according to the rules blueprint_builder = self.color_atomicals_outputs(atomicals_operations_found_at_inputs, atomicals_spent_at_inputs, tx, tx_hash, tx_num, height) for atomical_id in blueprint_builder.get_atomical_ids_spent(): @@ -2970,23 +2969,23 @@ def advance_txs( self.logger.debug(f'advance_txs: color_atomicals_outputs atomical_ids_transferred. atomical_id={atomical_id.hex()}, tx_hash={hash_to_hex_str(tx_hash)}') # Double hash the atomical_id to add it to the history to leverage the existing history db for all operations involving the atomical append_hashX(double_sha256(atomical_id)) - + # Track whether we encountered a valid operation so we can skip other steps in the processing pipeline for efficiency already_found_valid_operation = False - + atomical_id_of_distmint = self.create_or_delete_decentralized_mint_output(atomicals_operations_found_at_inputs, tx_num, tx_hash, tx, height, distmint_ticker_cache, False) if atomical_id_of_distmint: dft_count += 1 - already_found_valid_operation = True + already_found_valid_operation = True atomical_ids_which_have_valid_dft_mints[atomical_id_of_distmint] = True has_at_least_one_valid_atomicals_operation = True # Double hash the atomical_id_of_distmint to add it to the history to leverage the existing history db for all operations involving the atomical append_hashX(double_sha256(atomical_id_of_distmint)) self.logger.debug(f'advance_txs: create_or_delete_decentralized_mint_output:atomical_id_of_distmint - atomical_id={atomical_id_of_distmint.hex()}, tx_hash={hash_to_hex_str(tx_hash)}') - + if dft_count % 100 == 0: self.logger.info(f'height={height}, dft_count={dft_count}') - + # Create NFT/FT atomicals if it is defined in the tx if not already_found_valid_operation: created_atomical_id = self.create_or_delete_atomical(atomicals_operations_found_at_inputs, atomicals_spent_at_inputs, header, height, tx_num, atomical_num, tx, tx_hash, False) @@ -3003,27 +3002,74 @@ def advance_txs( if self.create_or_delete_data_location(tx_hash, atomicals_operations_found_at_inputs): self.put_op_data(tx_num, tx_hash, "dat") has_at_least_one_valid_atomicals_operation = True - already_found_valid_operation = True + already_found_valid_operation = True - # Note: We do not skip checking for payment tx's even if already_found_valid_operation = True because there could be valid mints - # in one and the same tx as making a payment. It's not advisable to do so, but it's a valid possibility + # Note: We do not skip checking for payment tx's even if already_found_valid_operation = True + # because there could be valid mints in one and the same tx as making a payment. + # It's not advisable to do so, but it's a valid possibility. # Check if there were any payments for subrealms in tx - payment_tx_hash = self.create_or_delete_subname_payment_output_if_valid(tx_hash, tx, tx_num, height, atomicals_operations_found_at_inputs, atomicals_spent_at_inputs, b'spay', self.subrealmpay_data_cache, self.get_expected_subrealm_payment_info, False) - if payment_tx_hash: - self.logger.info(f'advance_txs: found valid subrealm payment create_or_delete_subname_payment_output_if_valid {hash_to_hex_str(tx_hash)}') - append_hashX(double_sha256(payment_tx_hash)) - self.put_op_data(tx_num, tx_hash, "payment-subrealm") + subrealm_payment_tx_hash, verified = self.create_or_delete_subname_payment_output_if_valid( + tx_hash, + tx, + tx_num, + height, + atomicals_operations_found_at_inputs, + atomicals_spent_at_inputs, + b'spay', + self.subrealmpay_data_cache, + self.get_expected_subrealm_payment_info, + False + ) + if subrealm_payment_tx_hash: + if verified: + self.logger.info( + 'advance_txs: found valid subrealm payment ' + 'create_or_delete_subname_payment_output_if_valid ' + f'{hash_to_hex_str(tx_hash)}' + ) + self.put_op_data(tx_num, tx_hash, "payment-subrealm") + else: + self.logger.info( + 'advance_txs: found invalid subrealm payment ' + 'create_or_delete_subname_payment_output_if_valid ' + f'{hash_to_hex_str(tx_hash)}' + ) + self.put_op_data(tx_num, tx_hash, "payment-subrealm-failed") + append_hashX(double_sha256(subrealm_payment_tx_hash)) has_at_least_one_valid_atomicals_operation = True # Check if there were any payments for dmitems in tx - payment_tx_hash = self.create_or_delete_subname_payment_output_if_valid(tx_hash, tx, tx_num, height, atomicals_operations_found_at_inputs, atomicals_spent_at_inputs, b'dmpay', self.dmpay_data_cache, self.get_expected_dmitem_payment_info, False) - if payment_tx_hash: - self.logger.info(f'advance_txs: found valid dmitem payment create_or_delete_subname_payment_output_if_valid {hash_to_hex_str(tx_hash)}') - append_hashX(double_sha256(payment_tx_hash)) - self.put_op_data(tx_num, tx_hash, "payment-dmitem") + dmitem_payment_tx_hash, verified = self.create_or_delete_subname_payment_output_if_valid( + tx_hash, + tx, + tx_num, + height, + atomicals_operations_found_at_inputs, + atomicals_spent_at_inputs, + b'dmpay', + self.dmpay_data_cache, + self.get_expected_dmitem_payment_info, + False + ) + if dmitem_payment_tx_hash: + if verified: + self.logger.info( + 'advance_txs: found valid dmitem payment ' + 'create_or_delete_subname_payment_output_if_valid ' + f'{hash_to_hex_str(tx_hash)}' + ) + self.put_op_data(tx_num, tx_hash, "payment-dmitem") + else: + self.logger.info( + 'advance_txs: found invalid dmitem payment ' + 'create_or_delete_subname_payment_output_if_valid ' + f'{hash_to_hex_str(tx_hash)}' + ) + self.put_op_data(tx_num, tx_hash, "payment-dmitem-failed") + append_hashX(double_sha256(dmitem_payment_tx_hash)) has_at_least_one_valid_atomicals_operation = True - + # Create a proof of work record if there was valid proof of work attached if self.create_or_delete_pow_records(tx_hash, tx_num, height, atomicals_operations_found_at_inputs): has_at_least_one_valid_atomicals_operation = True @@ -3043,7 +3089,7 @@ def advance_txs( put_general_data(b'rtx' + tx_hash, raw_tx) del _tx del _tx_hash - + append_hashXs(hashXs) update_touched(hashXs) tx_num += 1 @@ -3059,15 +3105,15 @@ def advance_txs( self.db.tx_counts.append(tx_num) self.atomical_count = atomical_num self.db.atomical_counts.append(atomical_num) - + if self.is_atomicals_activated(height): # Save the atomicals hash for the current block current_height_atomicals_block_hash = self.coin.header_hash(b''.join(concatenation_of_tx_hashes_with_valid_atomical_operation)) put_general_data(b'tt' + pack_le_uint32(height), current_height_atomicals_block_hash) - self.logger.info(f'height={height}, atomicals_block_hash={hash_to_hex_str(current_height_atomicals_block_hash)}') - + self.logger.info(f'height={height}, atomicals_block_hash={hash_to_hex_str(current_height_atomicals_block_hash)}') + return undo_info, atomicals_undo_info - + # Sanity safety check method to call at end of block processing to ensure no dft token inflation def validate_no_dft_inflation(self, atomical_id_map, height): for atomical_id_of_dft_ticker, notused in atomical_id_map.items(): @@ -3084,54 +3130,54 @@ def validate_no_dft_inflation(self, atomical_id_map, height): decentralized_mints = self.get_distmints_count_by_atomical_id(height, atomical_id_of_dft_ticker, False) if decentralized_mints > max_mints: raise IndexError(f'validate_no_dft_inflation - inflation_bug_found: atomical_id_of_dft_ticker={location_id_bytes_to_compact(atomical_id_of_dft_ticker)} decentralized_mints={decentralized_mints} max_mints={max_mints}') - + def create_or_delete_subname_payment_output_if_valid(self, tx_hash, tx, tx_num, height, operations_found_at_inputs, atomicals_spent_at_inputs, db_prefix, subname_data_cache, get_expected_subname_payment_info, Delete=False): atomical_id_for_payment, payment_marker_idx, entity_type = AtomicalsTransferBlueprintBuilder.get_atomical_id_for_payment_marker_if_found(tx) if not atomical_id_for_payment: - return None + return None, False # Make sure the payment type for the right type subrealm or dmitem is correct if entity_type == 'subrealm' and db_prefix != b'spay': - return None + return None, False if entity_type == 'dmitem' and db_prefix != b'dmpay': - return None - + return None, False + # Rebuild the blueprint builder here blueprint_builder = AtomicalsTransferBlueprintBuilder(self.logger, atomicals_spent_at_inputs, operations_found_at_inputs, tx_hash, tx, self.get_atomicals_id_mint_info, self.is_dmint_activated(height)) if blueprint_builder.is_split_operation(): self.logger.warning(f'create_or_delete_subname_payment_output_if_valid: invalid payment split op found tx_hash={hash_to_hex_str(tx_hash)}') - return None - + return tx_hash, False + matched_price_point, parent_id, request_subname, subname_type = get_expected_subname_payment_info(atomical_id_for_payment, height) - + # An expected payment amount might not be set if there is no valid subrealm minting rules, or something invalid was found if not matched_price_point: self.logger.warning(f'create_or_delete_subname_payment_output_if_valid: {hash_to_hex_str(tx_hash)} NOT MATCHED PRICE - create_or_delete_subrealm_payment_output_if_valid atomical_id_for_payment={location_id_bytes_to_compact(atomical_id_for_payment)}') - return None - + return tx_hash, False + regex = matched_price_point['matched_rule']['p'] if not is_valid_regex(regex): self.logger.warning(f'create_or_delete_subname_payment_output_if_valid invalid matched regex. regex={regex} atomical_id_for_payment={location_id_bytes_to_compact(atomical_id_for_payment)}') - return None - + return tx_hash, False + # The pattern should have already matched, sanity check valid_pattern = re.compile(rf"{regex}") if not valid_pattern.match(request_subname): raise IndexError(f'create_or_delete_subname_payment_output_if_valid: valid pattern failed. DeveloperError request_subname={request_subname}, regex={regex}') - + if not blueprint_builder.are_payments_satisfied(matched_price_point['matched_rule'].get('o')): self.logger.warning(f'create_or_delete_subname_payment_output_if_valid: payments not satisfied. request_subname={request_subname}, regex={regex} atomical_id_for_payment={location_id_bytes_to_compact(atomical_id_for_payment)}') - return None - + return tx_hash, False + # Delete or create the record based on whether we are reorg rollback or creating new payment_outpoint = tx_hash + pack_le_uint32(payment_marker_idx) - not_initated_by_parent = b'00' # Used to indicate it was minted according to rules payment match + not_initated_by_parent = b'00' # Used to indicate it was minted according to rules payment match if Delete: self.delete_pay_record(atomical_id_for_payment, tx_num, payment_outpoint + not_initated_by_parent, db_prefix, subname_data_cache) else: self.put_pay_record(atomical_id_for_payment, tx_num, payment_outpoint + not_initated_by_parent, db_prefix, subname_data_cache) - - return tx_hash - + + return tx_hash, True + def backup_blocks(self, raw_blocks: Sequence[bytes]): '''Backup the raw blocks and flush. diff --git a/electrumx/server/daemon.py b/electrumx/server/daemon.py index b5dce0f1..fc392a02 100644 --- a/electrumx/server/daemon.py +++ b/electrumx/server/daemon.py @@ -13,7 +13,7 @@ import time from calendar import timegm from struct import pack -from typing import TYPE_CHECKING, Type +from typing import TYPE_CHECKING, Type, Union import aiohttp from aiorpcx import JSONRPC @@ -25,7 +25,7 @@ unpack_le_uint16_from) if TYPE_CHECKING: - from electrumx.lib.coins import Coin + from electrumx.lib.coins import Coin, AtomicalsCoinMixin class DaemonError(Exception): @@ -48,7 +48,7 @@ class Daemon: def __init__( self, - coin: Type['Coin'], + coin: Type[Union['Coin', 'AtomicalsCoinMixin']], url, *, max_workqueue=10, @@ -307,12 +307,14 @@ async def height(self): '''Query the daemon for its current height.''' self._height = await self._send_single('getblockcount') return self._height + # return self.coin.ATOMICALS_ACTIVATION_HEIGHT - 1 def cached_height(self): '''Return the cached daemon height. If the daemon has not been queried yet this returns None.''' return self._height + # return self.coin.ATOMICALS_ACTIVATION_HEIGHT - 1 class DashDaemon(Daemon): diff --git a/electrumx/server/db.py b/electrumx/server/db.py index 1d290c71..efcfc30c 100644 --- a/electrumx/server/db.py +++ b/electrumx/server/db.py @@ -1226,7 +1226,7 @@ def get_op_by_tx_num(self, tx_num): op_key = b'op' + pack_le_uint64(tx_num) op_data = self.utxo_db.get(op_key) if not op_data: - tx_hash, tx_height = self.fs_tx_hash(tx_num) + # tx_hash, tx_height = self.fs_tx_hash(tx_num) # self.logger.error(f'get_op {hash_to_hex_str(tx_hash)} tx_num not found') return None op_res = [] diff --git a/electrumx/server/env.py b/electrumx/server/env.py index 7299ca9e..52d8e887 100644 --- a/electrumx/server/env.py +++ b/electrumx/server/env.py @@ -10,10 +10,10 @@ import re from ipaddress import IPv4Address, IPv6Address -from typing import Type +from typing import Type, Union from aiorpcx import Service, ServicePart -from electrumx.lib.coins import Coin +from electrumx.lib.coins import Coin, AtomicalsCoinMixin from electrumx.lib.env_base import EnvBase @@ -32,7 +32,7 @@ class Env(EnvBase): SSL_PROTOCOLS = {'ssl', 'wss'} KNOWN_PROTOCOLS = {'ssl', 'tcp', 'ws', 'wss', 'rpc', 'http'} - coin: Type[Coin] + coin: Type[Union['Coin', 'AtomicalsCoinMixin']] def __init__(self, coin=None): super().__init__() diff --git a/electrumx/server/http_session.py b/electrumx/server/http_session.py index d40603e5..82a5f56f 100644 --- a/electrumx/server/http_session.py +++ b/electrumx/server/http_session.py @@ -1775,231 +1775,10 @@ async def atomicals_get_holders(self, request): }) return formatted_results - # analysis the transaction detail by txid - # might be mint-dft, dmint, transfer, burn... - async def get_transaction_detail(self, txid, height=None, tx_num=-1): - tx_hash = hex_str_to_hash(txid) - res = self.session_mgr._tx_detail_cache.get(tx_hash) - if res: - # txid maybe the same, this key should add height add key prefix - self.logger.debug(f"read transation detail from cache {txid}") - return res - if not height: - tx_num, height = self.db.get_tx_num_height_from_tx_hash(tx_hash) - - res = {} - raw_tx = self.db.get_raw_tx_by_tx_hash(tx_hash) - if not raw_tx: - raw_tx = await self.daemon_request('getrawtransaction', txid, False) - raw_tx = bytes.fromhex(raw_tx) - tx, _tx_hash = self.coin.DESERIALIZER(raw_tx, 0).read_tx_and_hash() - assert(tx_hash == _tx_hash) - - operation_found_at_inputs = parse_protocols_operations_from_witness_array(tx, tx_hash, True) - atomicals_spent_at_inputs = self.session_mgr.bp.build_atomicals_spent_at_inputs_for_validation_only(tx) - atomicals_receive_at_outputs = self.session_mgr.bp.build_atomicals_receive_at_ouutput_for_validation_only(tx, tx_hash) - blueprint_builder = AtomicalsTransferBlueprintBuilder(self.logger, atomicals_spent_at_inputs, operation_found_at_inputs, tx_hash, tx, self.session_mgr.bp.get_atomicals_id_mint_info, True) - is_burned = blueprint_builder.are_fts_burned - is_cleanly_assigned = blueprint_builder.cleanly_assigned - # format burned_fts - raw_burned_fts = blueprint_builder.get_fts_burned() - burned_fts = {} - for ft_key, ft_value in raw_burned_fts.items(): - burned_fts[location_id_bytes_to_compact(ft_key)] = ft_value - - res = { - "op": "", - "txid": txid, - "height": height, - "tx_num": tx_num, - "info": {}, - "transfers":{ - "inputs": {}, - "outputs": {}, - "is_burned": is_burned, - "burned_fts": burned_fts, - "is_cleanly_assigned": is_cleanly_assigned - } - } - if operation_found_at_inputs: - res["info"]["payload"] = operation_found_at_inputs.get("payload", {}) - if blueprint_builder.is_mint and operation_found_at_inputs and operation_found_at_inputs["op"] in ["dmt", "ft"]: - if operation_found_at_inputs["op"] == "dmt": - res["op"] = "mint-dft" - if operation_found_at_inputs["op"] == "ft": - res["op"] = "mint-ft" - expected_output_index = 0 - txout = tx.outputs[expected_output_index] - location = tx_hash + util.pack_le_uint32(expected_output_index) - # if save into the db, it means mint success - has_atomicals = self.db.get_atomicals_by_location_long_form(location) - if len(has_atomicals): - ticker_name = operation_found_at_inputs.get("payload", {}).get("args", {}).get("mint_ticker", "") - status, candidate_atomical_id, _ = self.session_mgr.bp.get_effective_ticker(ticker_name, self.session_mgr.bp.height) - if status: - atomical_id = location_id_bytes_to_compact(candidate_atomical_id) - res["info"] = { - "atomical_id": atomical_id, - "location_id": location_id_bytes_to_compact(location), - "payload": operation_found_at_inputs.get("payload"), - "outputs": { - expected_output_index: [{ - "address": get_address_from_output_script(txout.pk_script), - "atomical_id": atomical_id, - "type": "FT", - "index": expected_output_index, - "value": txout.value - }] - } - } - else: - res["op"] = f"{res['op']}-failed" - elif operation_found_at_inputs and operation_found_at_inputs and operation_found_at_inputs["op"] == "nft": - mint_info = operation_found_at_inputs.get("payload", {}).get("args", {}) - if mint_info.get('request_realm'): - res["op"] = "mint-nft-realm" - elif mint_info.get('request_subrealm'): - res["op"] = "mint-nft-subrealm" - elif mint_info.get('request_container'): - res["op"] = "mint-nft-container" - elif mint_info.get('request_dmitem'): - res["op"] = "mint-nft-dmitem" - else: - res["op"] = "mint-nft" - if atomicals_receive_at_outputs: - expected_output_index = 0 - location = tx_hash + util.pack_le_uint32(expected_output_index) - txout = tx.outputs[expected_output_index] - atomical_id = location_id_bytes_to_compact(atomicals_receive_at_outputs[expected_output_index][-1]["atomical_id"]) - res["info"] = { - "atomical_id": atomical_id, - "location_id": location_id_bytes_to_compact(location), - "payload": operation_found_at_inputs.get("payload"), - "outputs": { - expected_output_index: [{ - "address": get_address_from_output_script(txout.pk_script), - "atomical_id": atomical_id, - "type": "NFT", - "index": expected_output_index, - "value": txout.value - }] - } - } - else: - res["op"] = f"{res['op']}-failed" - elif operation_found_at_inputs and operation_found_at_inputs["op"] == "dft": - res["op"] = "dft" - elif operation_found_at_inputs and operation_found_at_inputs["op"] == "sl": - res["op"] = "seal" - elif operation_found_at_inputs and operation_found_at_inputs["op"] == "x": - res["op"] = "splat" - elif operation_found_at_inputs and operation_found_at_inputs["op"] == "y": - res["op"] = "split" - elif operation_found_at_inputs and operation_found_at_inputs["op"] == "evt": - res["op"] = "evt" - elif operation_found_at_inputs and operation_found_at_inputs["op"] == "mod": - res["op"] = "mod" - elif operation_found_at_inputs and operation_found_at_inputs["op"] == "dat": - res["op"] = "dat" - # no operation_found_at_inputs, it will be transfer. - if blueprint_builder.ft_atomicals and atomicals_spent_at_inputs: - if not operation_found_at_inputs: - res["op"] = "transfer" - for atomical_id, input_ft in blueprint_builder.ft_atomicals.items(): - compact_atomical_id = location_id_bytes_to_compact(atomical_id) - for i in input_ft.input_indexes: - prev_txid = hash_to_hex_str(tx.inputs[i.txin_index].prev_hash) - prev_raw_tx = self.db.get_raw_tx_by_tx_hash(hex_str_to_hash(prev_txid)) - if not prev_raw_tx: - prev_raw_tx = await self.daemon_request('getrawtransaction', prev_txid, False) - prev_raw_tx = bytes.fromhex(prev_raw_tx) - self.session_mgr.bp.general_data_cache[b'rtx' + hex_str_to_hash(prev_txid)] = prev_raw_tx - prev_tx, _ = self.coin.DESERIALIZER(prev_raw_tx, 0).read_tx_and_hash() - ft_data = { - "address": get_address_from_output_script(prev_tx.outputs[tx.inputs[i.txin_index].prev_idx].pk_script), - "atomical_id": compact_atomical_id, - "type": "FT", - "index": i.txin_index, - "value": prev_tx.outputs[tx.inputs[i.txin_index].prev_idx].value - } - if i.txin_index not in res["transfers"]["inputs"]: - res["transfers"]["inputs"][i.txin_index] = [ft_data] - else: - res["transfers"]["inputs"][i.txin_index].append(ft_data) - for k, v in blueprint_builder.ft_output_blueprint.outputs.items(): - for atomical_id, output_ft in v['atomicals'].items(): - compact_atomical_id = location_id_bytes_to_compact(atomical_id) - ft_data = { - "address": get_address_from_output_script(tx.outputs[k].pk_script), - "atomical_id": compact_atomical_id, - "type": "FT", - "index": k, - "value": output_ft.satvalue - } - if k not in res["transfers"]["outputs"]: - res["transfers"]["outputs"][k] = [ft_data] - else: - res["transfers"]["outputs"][k].append(ft_data) - if blueprint_builder.nft_atomicals and atomicals_spent_at_inputs: - if not operation_found_at_inputs: - res["op"] = "transfer" - for atomical_id, input_nft in blueprint_builder.nft_atomicals.items(): - compact_atomical_id = location_id_bytes_to_compact(atomical_id) - for i in input_nft.input_indexes: - prev_txid = hash_to_hex_str(tx.inputs[i.txin_index].prev_hash) - prev_raw_tx = self.db.get_raw_tx_by_tx_hash(hex_str_to_hash(prev_txid)) - if not prev_raw_tx: - prev_raw_tx = await self.daemon_request('getrawtransaction', prev_txid, False) - prev_raw_tx = bytes.fromhex(prev_raw_tx) - self.session_mgr.bp.general_data_cache[b'rtx' + hex_str_to_hash(prev_txid)] = prev_raw_tx - prev_tx, _ = self.coin.DESERIALIZER(prev_raw_tx, 0).read_tx_and_hash() - nft_data = { - "address": get_address_from_output_script(prev_tx.outputs[tx.inputs[i.txin_index].prev_idx].pk_script), - "atomical_id": compact_atomical_id, - "type": "NFT", - "index": i.txin_index, - "value": prev_tx.outputs[tx.inputs[i.txin_index].prev_idx].value - } - if i.txin_index not in res["transfers"]["inputs"]: - res["transfers"]["inputs"][i.txin_index] = [nft_data] - else: - res["transfers"]["inputs"][i.txin_index].append(nft_data) - for k, v in blueprint_builder.nft_output_blueprint.outputs.items(): - for atomical_id, output_nft in v['atomicals'].items(): - compact_atomical_id = location_id_bytes_to_compact(atomical_id) - nft_data = { - "address": get_address_from_output_script(tx.outputs[k].pk_script), - "atomical_id": compact_atomical_id, - "type": output_nft.type, - "index": k, - "value": output_nft.total_satsvalue - } - if k not in res["transfers"]["outputs"]: - res["transfers"]["outputs"][k] = [nft_data] - else: - res["transfers"]["outputs"][k].append(nft_data) - - atomical_id_for_payment, payment_marker_idx, entity_type = AtomicalsTransferBlueprintBuilder.get_atomical_id_for_payment_marker_if_found(tx) - if atomical_id_for_payment: - res["info"]["payment"] = { - "atomical_id": location_id_bytes_to_compact(atomical_id_for_payment), - "payment_marker_idx": payment_marker_idx - } - if entity_type == 'subrealm': - res["op"] = "payment-subrealm" - if entity_type == 'dmitem': - res["op"] = "payment-dmitem" - - if res.get("op") and height: - self.session_mgr._tx_detail_cache[tx_hash] = res - - # Recursively encode the result. - return auto_encode_bytes_elements(res) - async def atomicals_transaction(self, request): params = await self.format_params(request) txid = params.get(0, "") - return await self.get_transaction_detail(txid) + return await self.session_mgr.get_transaction_detail(txid) async def get_transaction_detail_by_height(self, height, limit, offset, op_type, reverse=True): res = [] @@ -2016,7 +1795,7 @@ async def get_transaction_detail_by_height(self, height, limit, offset, op_type, txs_list.sort(key=lambda x: x['tx_num'], reverse=reverse) for tx in txs_list: - data = await self.get_transaction_detail(tx["tx_hash"], height, tx["tx_num"]) + data = await self.session_mgr.get_transaction_detail(tx["tx_hash"], height, tx["tx_num"]) if (op_type and op_type == data["op"]) or (not op_type and data["op"]): res.append(data) total = len(res) @@ -2061,7 +1840,7 @@ async def transaction_by_atomical_id(self, request): history_data, total = await self.session_mgr.get_history_op(hashX, limit, offset, None, reverse) for history in history_data: tx_hash, tx_height = self.db.fs_tx_hash(history["tx_num"]) - data = await self.get_transaction_detail(hash_to_hex_str(tx_hash), tx_height, history["tx_num"]) + data = await self.session_mgr.get_transaction_detail(hash_to_hex_str(tx_hash), tx_height, history["tx_num"]) if data and data["op"]: if (op_type and data["op"] == op_type) or not op_type: res.append(data) @@ -2086,7 +1865,7 @@ async def transaction_by_scripthash(self, request): for history in history_data: tx_hash, tx_height = self.db.fs_tx_hash(history["tx_num"]) - data = await self.get_transaction_detail(hash_to_hex_str(tx_hash), tx_height, history["tx_num"]) + data = await self.session_mgr.get_transaction_detail(hash_to_hex_str(tx_hash), tx_height, history["tx_num"]) if data and data["op"]: if data["op"] and (data["op"] == op_type or not op_type): res.append(data) @@ -2099,28 +1878,4 @@ async def transaction_global(self, request): offset = params.get(1, 0) op_type = params.get(2, None) reverse = params.get(3, True) - height = self.session_mgr.bp.height - - res = [] - count = 0 - history_list = [] - for current_height in range(height, self.coin.ATOMICALS_ACTIVATION_HEIGHT, -1): - txs = self.db.get_atomicals_block_txs(current_height) - for tx in txs: - tx_num, _ = self.db.get_tx_num_height_from_tx_hash(hex_str_to_hash(tx)) - history_list.append({ - "tx_num": tx_num, - "tx_hash": tx, - "height": current_height - }) - count += 1 - if count >= offset + limit: - break - history_list.sort(key=lambda x: x['tx_num'], reverse=reverse) - - for history in history_list: - data = await self.get_transaction_detail(history["tx_hash"], history["height"], history["tx_num"]) - if (op_type and op_type == data["op"]) or (not op_type and data["op"]): - res.append(data) - total = len(res) - return {"result": res[offset:offset+limit], "total": total, "limit": limit, "offset": offset} + return await self.session_mgr.transaction_global(limit, offset, op_type, reverse) diff --git a/electrumx/server/mempool.py b/electrumx/server/mempool.py index b8c7e74b..e2aed33a 100644 --- a/electrumx/server/mempool.py +++ b/electrumx/server/mempool.py @@ -12,7 +12,7 @@ from abc import ABC, abstractmethod from asyncio import Lock from collections import defaultdict -from typing import Sequence, Tuple, TYPE_CHECKING, Type, Dict +from typing import Sequence, Tuple, TYPE_CHECKING, Type, Dict, Union import math import attr @@ -27,7 +27,7 @@ from electrumx.lib.hash import hash_to_hex_str, HASHX_LEN, double_sha256 if TYPE_CHECKING: - from electrumx.lib.coins import Coin + from electrumx.lib.coins import Coin, AtomicalsCoinMixin @attr.s(slots=True) @@ -112,7 +112,7 @@ class MemPool: def __init__( self, - coin: Type['Coin'], + coin: Type[Union['Coin', 'AtomicalsCoinMixin']], api: MemPoolAPI, *, refresh_secs=5.0, diff --git a/electrumx/server/session.py b/electrumx/server/session.py index a34562c2..47c4b05a 100644 --- a/electrumx/server/session.py +++ b/electrumx/server/session.py @@ -206,18 +206,6 @@ def __init__( # Event triggered when electrumx is listening for incoming requests. self.server_listening = Event() self.session_event = Event() - self.op_list = { - "mint-dft": 1, "mint-ft": 2, "mint-nft": 3, "mint-nft-realm": 4, - "mint-nft-subrealm": 5, "mint-nft-container": 6, "mint-nft-dmitem": 7, - "dft": 20, "dat": 21, "split": 22, "splat": 23, - "seal": 24, "evt": 25, "mod": 26, - "transfer": 30, - "payment-subrealm": 40, "payment-dmitem": 41, - "mint-dft-failed": 51, "mint-ft-failed": 52, "mint-nft-failed": 53, "mint-nft-realm-failed": 54, - "mint-nft-subrealm-failed": 55, "mint-nft-container-failed": 56, "mint-nft-dmitem-failed": 57, - "invalid-mint": 59, - "burn": 70, - } # Set up the RPC request handlers cmds = ('add_peer daemon_url disconnect getinfo groups log peers ' @@ -1043,6 +1031,236 @@ async def get_history_op(self, hashX, limit=10, offset=0, op=None, reverse=True) history_data = list(filter(lambda x: x["op"], history_data)) return history_data[offset:limit+offset], len(history_data) + # Analysis the transaction detail by txid. + # See BlockProcessor.op_list for the complete op list. + async def get_transaction_detail(self, txid: str, height=None, tx_num=-1): + tx_hash = hex_str_to_hash(txid) + res = self._tx_detail_cache.get(tx_hash) + if res: + # txid maybe the same, this key should add height add key prefix + self.logger.debug(f"read transation detail from cache {txid}") + return res + if not height: + tx_num, height = self.db.get_tx_num_height_from_tx_hash(tx_hash) + + raw_tx = self.db.get_raw_tx_by_tx_hash(tx_hash) + if not raw_tx: + raw_tx = await self.daemon_request('getrawtransaction', txid, False) + raw_tx = bytes.fromhex(raw_tx) + tx, _tx_hash = self.env.coin.DESERIALIZER(raw_tx, 0).read_tx_and_hash() + assert tx_hash == _tx_hash + ops = self.db.get_op_by_tx_num(tx_num) + op_raw = self.bp.op_list_vk[ops[0]] if ops else "" + + operation_found_at_inputs = parse_protocols_operations_from_witness_array(tx, tx_hash, True) + atomicals_spent_at_inputs = self.bp.build_atomicals_spent_at_inputs_for_validation_only(tx) + atomicals_receive_at_outputs = self.bp.build_atomicals_receive_at_ouutput_for_validation_only(tx, tx_hash) + blueprint_builder = AtomicalsTransferBlueprintBuilder( + self.logger, + atomicals_spent_at_inputs, + operation_found_at_inputs, + tx_hash, + tx, + self.bp.get_atomicals_id_mint_info, + True + ) + is_burned = blueprint_builder.are_fts_burned + is_cleanly_assigned = blueprint_builder.cleanly_assigned + # format burned_fts + raw_burned_fts = blueprint_builder.get_fts_burned() + burned_fts = {} + for ft_key, ft_value in raw_burned_fts.items(): + burned_fts[location_id_bytes_to_compact(ft_key)] = ft_value + + res = { + "txid": txid, + "height": height, + "tx_num": tx_num, + "info": {}, + "transfers": { + "inputs": {}, + "outputs": {}, + "is_burned": is_burned, + "burned_fts": burned_fts, + "is_cleanly_assigned": is_cleanly_assigned + } + } + operation_type = operation_found_at_inputs.get("op", "") if operation_found_at_inputs else "" + if operation_found_at_inputs: + payload = operation_found_at_inputs.get("payload") + payload_not_none = payload or {} + res["info"]["payload"] = payload_not_none + if blueprint_builder.is_mint and operation_type in ["dmt", "ft"]: + expected_output_index = 0 + txout = tx.outputs[expected_output_index] + location = tx_hash + util.pack_le_uint32(expected_output_index) + # if save into the db, it means mint success + has_atomicals = self.db.get_atomicals_by_location_long_form(location) + if len(has_atomicals): + ticker_name = payload_not_none.get("args", {}).get("mint_ticker", "") + status, candidate_atomical_id, _ = self.bp.get_effective_ticker(ticker_name, self.bp.height) + if status: + atomical_id = location_id_bytes_to_compact(candidate_atomical_id) + res["info"] = { + "atomical_id": atomical_id, + "location_id": location_id_bytes_to_compact(location), + "payload": payload, + "outputs": { + expected_output_index: [{ + "address": get_address_from_output_script(txout.pk_script), + "atomical_id": atomical_id, + "type": "FT", + "index": expected_output_index, + "value": txout.value + }] + } + } + elif operation_type == "nft": + if atomicals_receive_at_outputs: + expected_output_index = 0 + location = tx_hash + util.pack_le_uint32(expected_output_index) + txout = tx.outputs[expected_output_index] + atomical_id = location_id_bytes_to_compact( + atomicals_receive_at_outputs[expected_output_index][-1]["atomical_id"] + ) + res["info"] = { + "atomical_id": atomical_id, + "location_id": location_id_bytes_to_compact(location), + "payload": payload, + "outputs": { + expected_output_index: [{ + "address": get_address_from_output_script(txout.pk_script), + "atomical_id": atomical_id, + "type": "NFT", + "index": expected_output_index, + "value": txout.value + }] + } + } + # no operation_found_at_inputs, it will be transfer. + if blueprint_builder.ft_atomicals and atomicals_spent_at_inputs: + if not operation_type and not op_raw: + op_raw = "transfer" + for atomical_id, input_ft in blueprint_builder.ft_atomicals.items(): + compact_atomical_id = location_id_bytes_to_compact(atomical_id) + for i in input_ft.input_indexes: + prev_txid = hash_to_hex_str(tx.inputs[i.txin_index].prev_hash) + prev_raw_tx = self.db.get_raw_tx_by_tx_hash(hex_str_to_hash(prev_txid)) + if not prev_raw_tx: + prev_raw_tx = await self.daemon_request('getrawtransaction', prev_txid, False) + prev_raw_tx = bytes.fromhex(prev_raw_tx) + self.bp.general_data_cache[b'rtx' + hex_str_to_hash(prev_txid)] = prev_raw_tx + prev_tx, _ = self.env.coin.DESERIALIZER(prev_raw_tx, 0).read_tx_and_hash() + ft_data = { + "address": get_address_from_output_script(prev_tx.outputs[tx.inputs[i.txin_index].prev_idx].pk_script), + "atomical_id": compact_atomical_id, + "type": "FT", + "index": i.txin_index, + "value": prev_tx.outputs[tx.inputs[i.txin_index].prev_idx].value + } + if i.txin_index not in res["transfers"]["inputs"]: + res["transfers"]["inputs"][i.txin_index] = [ft_data] + else: + res["transfers"]["inputs"][i.txin_index].append(ft_data) + for k, v in blueprint_builder.ft_output_blueprint.outputs.items(): + for atomical_id, output_ft in v['atomicals'].items(): + compact_atomical_id = location_id_bytes_to_compact(atomical_id) + ft_data = { + "address": get_address_from_output_script(tx.outputs[k].pk_script), + "atomical_id": compact_atomical_id, + "type": "FT", + "index": k, + "value": output_ft.satvalue + } + if k not in res["transfers"]["outputs"]: + res["transfers"]["outputs"][k] = [ft_data] + else: + res["transfers"]["outputs"][k].append(ft_data) + if blueprint_builder.nft_atomicals and atomicals_spent_at_inputs: + if not operation_type and not op_raw: + op_raw = "transfer" + for atomical_id, input_nft in blueprint_builder.nft_atomicals.items(): + compact_atomical_id = location_id_bytes_to_compact(atomical_id) + for i in input_nft.input_indexes: + prev_txid = hash_to_hex_str(tx.inputs[i.txin_index].prev_hash) + prev_raw_tx = self.db.get_raw_tx_by_tx_hash(hex_str_to_hash(prev_txid)) + if not prev_raw_tx: + prev_raw_tx = await self.daemon_request('getrawtransaction', prev_txid, False) + prev_raw_tx = bytes.fromhex(prev_raw_tx) + self.bp.general_data_cache[b'rtx' + hex_str_to_hash(prev_txid)] = prev_raw_tx + prev_tx, _ = self.env.coin.DESERIALIZER(prev_raw_tx, 0).read_tx_and_hash() + nft_data = { + "address": get_address_from_output_script(prev_tx.outputs[tx.inputs[i.txin_index].prev_idx].pk_script), + "atomical_id": compact_atomical_id, + "type": "NFT", + "index": i.txin_index, + "value": prev_tx.outputs[tx.inputs[i.txin_index].prev_idx].value + } + if i.txin_index not in res["transfers"]["inputs"]: + res["transfers"]["inputs"][i.txin_index] = [nft_data] + else: + res["transfers"]["inputs"][i.txin_index].append(nft_data) + for k, v in blueprint_builder.nft_output_blueprint.outputs.items(): + for atomical_id, output_nft in v['atomicals'].items(): + compact_atomical_id = location_id_bytes_to_compact(atomical_id) + nft_data = { + "address": get_address_from_output_script(tx.outputs[k].pk_script), + "atomical_id": compact_atomical_id, + "type": output_nft.type, + "index": k, + "value": output_nft.total_satsvalue + } + if k not in res["transfers"]["outputs"]: + res["transfers"]["outputs"][k] = [nft_data] + else: + res["transfers"]["outputs"][k].append(nft_data) + + atomical_id_for_payment, payment_marker_idx, _ = AtomicalsTransferBlueprintBuilder.get_atomical_id_for_payment_marker_if_found(tx) + if atomical_id_for_payment: + res["info"]["payment"] = { + "atomical_id": location_id_bytes_to_compact(atomical_id_for_payment), + "payment_marker_idx": payment_marker_idx + } + + if op_raw and height: + self._tx_detail_cache[tx_hash] = res + res["op"] = op_raw + + # Recursively encode the result. + return auto_encode_bytes_elements(res) + + async def transaction_global( + self, + limit: int = 10, + offset: int = 0, + op_type: Optional[str] = None, + reverse: bool = True + ): + height = self.bp.height + res = [] + count = 0 + history_list = [] + for current_height in range(height, self.env.coin.ATOMICALS_ACTIVATION_HEIGHT, -1): + txs = self.db.get_atomicals_block_txs(current_height) + for tx in txs: + tx_num, _ = self.db.get_tx_num_height_from_tx_hash(hex_str_to_hash(tx)) + history_list.append({ + "tx_num": tx_num, + "tx_hash": tx, + "height": current_height + }) + count += 1 + if count >= offset + limit: + break + history_list.sort(key=lambda x: x['tx_num'], reverse=reverse) + + for history in history_list: + data = await self.get_transaction_detail(history["tx_hash"], history["height"], history["tx_num"]) + if (op_type and op_type == data["op"]) or (not op_type and data["op"]): + res.append(data) + total = len(res) + return {"result": res[offset:offset+limit], "total": total, "limit": limit, "offset": offset} + async def _notify_sessions(self, height, touched): '''Notify sessions about height changes and touched addresses.''' height_changed = height != self.notified_height @@ -2790,226 +3008,8 @@ async def compact_fee_histogram(self): self.bump_cost(1.0) return await self.mempool.compact_fee_histogram() - async def get_transaction_detail(self, txid, height=None, tx_num=-1): - tx_hash = hex_str_to_hash(txid) - res = self.session_mgr._tx_detail_cache.get(tx_hash) - if res: - # txid maybe the same, this key should add height add key prefix - self.logger.debug(f"read transation detail from cache {txid}") - return res - if not height: - tx_num, height = self.db.get_tx_num_height_from_tx_hash(tx_hash) - - raw_tx = self.db.get_raw_tx_by_tx_hash(tx_hash) - if not raw_tx: - raw_tx = await self.daemon_request('getrawtransaction', txid, False) - raw_tx = bytes.fromhex(raw_tx) - tx, _tx_hash = self.coin.DESERIALIZER(raw_tx, 0).read_tx_and_hash() - assert tx_hash == _tx_hash - - operation_found_at_inputs = parse_protocols_operations_from_witness_array(tx, tx_hash, True) - atomicals_spent_at_inputs = self.session_mgr.bp.build_atomicals_spent_at_inputs_for_validation_only(tx) - atomicals_receive_at_outputs = self.session_mgr.bp.build_atomicals_receive_at_ouutput_for_validation_only(tx, tx_hash) - blueprint_builder = AtomicalsTransferBlueprintBuilder(self.logger, atomicals_spent_at_inputs, operation_found_at_inputs, tx_hash, tx, self.session_mgr.bp.get_atomicals_id_mint_info, True) - is_burned = blueprint_builder.are_fts_burned - is_cleanly_assigned = blueprint_builder.cleanly_assigned - # format burned_fts - raw_burned_fts = blueprint_builder.get_fts_burned() - burned_fts = {} - for ft_key, ft_value in raw_burned_fts.items(): - burned_fts[location_id_bytes_to_compact(ft_key)] = ft_value - - res = { - "op": "", - "txid": txid, - "height": height, - "tx_num": tx_num, - "info": {}, - "transfers": { - "inputs": {}, - "outputs": {}, - "is_burned": is_burned, - "burned_fts": burned_fts, - "is_cleanly_assigned": is_cleanly_assigned - } - } - if operation_found_at_inputs: - res["info"]["payload"] = operation_found_at_inputs.get("payload", {}) - if blueprint_builder.is_mint and operation_found_at_inputs and operation_found_at_inputs["op"] in ["dmt", "ft"]: - if operation_found_at_inputs["op"] == "dmt": - res["op"] = "mint-dft" - if operation_found_at_inputs["op"] == "ft": - res["op"] = "mint-ft" - expected_output_index = 0 - txout = tx.outputs[expected_output_index] - location = tx_hash + util.pack_le_uint32(expected_output_index) - # if save into the db, it means mint success - has_atomicals = self.db.get_atomicals_by_location_long_form(location) - if len(has_atomicals): - ticker_name = operation_found_at_inputs.get("payload", {}).get("args", {}).get("mint_ticker", "") - status, candidate_atomical_id, _ = self.session_mgr.bp.get_effective_ticker(ticker_name, self.session_mgr.bp.height) - if status: - atomical_id = location_id_bytes_to_compact(candidate_atomical_id) - res["info"] = { - "atomical_id": atomical_id, - "location_id": location_id_bytes_to_compact(location), - "payload": operation_found_at_inputs.get("payload"), - "outputs": { - expected_output_index: [{ - "address": get_address_from_output_script(txout.pk_script), - "atomical_id": atomical_id, - "type": "FT", - "index": expected_output_index, - "value": txout.value - }] - } - } - else: - res["op"] = f"{res['op']}-failed" - elif operation_found_at_inputs and operation_found_at_inputs and operation_found_at_inputs["op"] == "nft": - mint_info = operation_found_at_inputs.get("payload", {}).get("args", {}) - if mint_info.get('request_realm'): - res["op"] = "mint-nft-realm" - elif mint_info.get('request_subrealm'): - res["op"] = "mint-nft-subrealm" - elif mint_info.get('request_container'): - res["op"] = "mint-nft-container" - elif mint_info.get('request_dmitem'): - res["op"] = "mint-nft-dmitem" - else: - res["op"] = "mint-nft" - if atomicals_receive_at_outputs: - expected_output_index = 0 - location = tx_hash + util.pack_le_uint32(expected_output_index) - txout = tx.outputs[expected_output_index] - atomical_id = location_id_bytes_to_compact(atomicals_receive_at_outputs[expected_output_index][-1]["atomical_id"]) - res["info"] = { - "atomical_id": atomical_id, - "location_id": location_id_bytes_to_compact(location), - "payload": operation_found_at_inputs.get("payload"), - "outputs": { - expected_output_index: [{ - "address": get_address_from_output_script(txout.pk_script), - "atomical_id": atomical_id, - "type": "NFT", - "index": expected_output_index, - "value": txout.value - }] - } - } - else: - res["op"] = f"{res['op']}-failed" - elif operation_found_at_inputs and operation_found_at_inputs["op"] == "dft": - res["op"] = "dft" - elif operation_found_at_inputs and operation_found_at_inputs["op"] == "sl": - res["op"] = "seal" - elif operation_found_at_inputs and operation_found_at_inputs["op"] == "x": - res["op"] = "splat" - elif operation_found_at_inputs and operation_found_at_inputs["op"] == "y": - res["op"] = "split" - elif operation_found_at_inputs and operation_found_at_inputs["op"] == "evt": - res["op"] = "evt" - elif operation_found_at_inputs and operation_found_at_inputs["op"] == "mod": - res["op"] = "mod" - elif operation_found_at_inputs and operation_found_at_inputs["op"] == "dat": - res["op"] = "dat" - # no operation_found_at_inputs, it will be transfer. - if blueprint_builder.ft_atomicals and atomicals_spent_at_inputs: - if not operation_found_at_inputs: - res["op"] = "transfer" - for atomical_id, input_ft in blueprint_builder.ft_atomicals.items(): - compact_atomical_id = location_id_bytes_to_compact(atomical_id) - for i in input_ft.input_indexes: - prev_txid = hash_to_hex_str(tx.inputs[i.txin_index].prev_hash) - prev_raw_tx = self.db.get_raw_tx_by_tx_hash(hex_str_to_hash(prev_txid)) - if not prev_raw_tx: - prev_raw_tx = await self.daemon_request('getrawtransaction', prev_txid, False) - prev_raw_tx = bytes.fromhex(prev_raw_tx) - self.session_mgr.bp.general_data_cache[b'rtx' + hex_str_to_hash(prev_txid)] = prev_raw_tx - prev_tx, _ = self.coin.DESERIALIZER(prev_raw_tx, 0).read_tx_and_hash() - ft_data = { - "address": get_address_from_output_script(prev_tx.outputs[tx.inputs[i.txin_index].prev_idx].pk_script), - "atomical_id": compact_atomical_id, - "type": "FT", - "index": i.txin_index, - "value": prev_tx.outputs[tx.inputs[i.txin_index].prev_idx].value - } - if i.txin_index not in res["transfers"]["inputs"]: - res["transfers"]["inputs"][i.txin_index] = [ft_data] - else: - res["transfers"]["inputs"][i.txin_index].append(ft_data) - for k, v in blueprint_builder.ft_output_blueprint.outputs.items(): - for atomical_id, output_ft in v['atomicals'].items(): - compact_atomical_id = location_id_bytes_to_compact(atomical_id) - ft_data = { - "address": get_address_from_output_script(tx.outputs[k].pk_script), - "atomical_id": compact_atomical_id, - "type": "FT", - "index": k, - "value": output_ft.satvalue - } - if k not in res["transfers"]["outputs"]: - res["transfers"]["outputs"][k] = [ft_data] - else: - res["transfers"]["outputs"][k].append(ft_data) - if blueprint_builder.nft_atomicals and atomicals_spent_at_inputs: - if not operation_found_at_inputs: - res["op"] = "transfer" - for atomical_id, input_nft in blueprint_builder.nft_atomicals.items(): - compact_atomical_id = location_id_bytes_to_compact(atomical_id) - for i in input_nft.input_indexes: - prev_txid = hash_to_hex_str(tx.inputs[i.txin_index].prev_hash) - prev_raw_tx = self.db.get_raw_tx_by_tx_hash(hex_str_to_hash(prev_txid)) - if not prev_raw_tx: - prev_raw_tx = await self.daemon_request('getrawtransaction', prev_txid, False) - prev_raw_tx = bytes.fromhex(prev_raw_tx) - self.session_mgr.bp.general_data_cache[b'rtx' + hex_str_to_hash(prev_txid)] = prev_raw_tx - prev_tx, _ = self.coin.DESERIALIZER(prev_raw_tx, 0).read_tx_and_hash() - nft_data = { - "address": get_address_from_output_script(prev_tx.outputs[tx.inputs[i.txin_index].prev_idx].pk_script), - "atomical_id": compact_atomical_id, - "type": "NFT", - "index": i.txin_index, - "value": prev_tx.outputs[tx.inputs[i.txin_index].prev_idx].value - } - if i.txin_index not in res["transfers"]["inputs"]: - res["transfers"]["inputs"][i.txin_index] = [nft_data] - else: - res["transfers"]["inputs"][i.txin_index].append(nft_data) - for k, v in blueprint_builder.nft_output_blueprint.outputs.items(): - for atomical_id, output_nft in v['atomicals'].items(): - compact_atomical_id = location_id_bytes_to_compact(atomical_id) - nft_data = { - "address": get_address_from_output_script(tx.outputs[k].pk_script), - "atomical_id": compact_atomical_id, - "type": output_nft.type, - "index": k, - "value": output_nft.total_satsvalue - } - if k not in res["transfers"]["outputs"]: - res["transfers"]["outputs"][k] = [nft_data] - else: - res["transfers"]["outputs"][k].append(nft_data) - - atomical_id_for_payment, payment_marker_idx, entity_type = AtomicalsTransferBlueprintBuilder.get_atomical_id_for_payment_marker_if_found(tx) - if atomical_id_for_payment: - res["info"]["payment"] = { - "atomical_id": location_id_bytes_to_compact(atomical_id_for_payment), - "payment_marker_idx": payment_marker_idx - } - if entity_type == 'subrealm': - res["op"] = "payment-subrealm" - if entity_type == 'dmitem': - res["op"] = "payment-dmitem" - - if res.get("op"): - self.session_mgr._tx_detail_cache[tx_hash] = res - - # Recursively encode the result. - return auto_encode_bytes_elements(res) - async def atomicals_transaction(self, txid): - return await self.get_transaction_detail(txid) + return await self.session_mgr.get_transaction_detail(txid) async def get_transaction_detail_by_height(self, height, limit, offset, op_type, reverse=True): res = [] @@ -3026,7 +3026,7 @@ async def get_transaction_detail_by_height(self, height, limit, offset, op_type, txs_list.sort(key=lambda x: x['tx_num'], reverse=reverse) for tx in txs_list: - data = await self.get_transaction_detail(tx["tx_hash"], height, tx["tx_num"]) + data = await self.session_mgr.get_transaction_detail(tx["tx_hash"], height, tx["tx_num"]) if (op_type and op_type == data["op"]) or (not op_type and data["op"]): res.append(data) total = len(res) @@ -3057,7 +3057,7 @@ async def transaction_by_atomical_id(self, compact_atomical_id_or_atomical_numbe history_data, total = await self.session_mgr.get_history_op(hashX, limit, offset, None, reverse) for history in history_data: tx_hash, tx_height = self.db.fs_tx_hash(history["tx_num"]) - data = await self.get_transaction_detail(hash_to_hex_str(tx_hash), tx_height, history["tx_num"]) + data = await self.session_mgr.get_transaction_detail(hash_to_hex_str(tx_hash), tx_height, history["tx_num"]) if data and data["op"]: if (op_type and data["op"] == op_type) or not op_type: res.append(data) @@ -3075,7 +3075,7 @@ async def transaction_by_scripthash(self, scripthash, limit=10, offset=0, op_typ for history in history_data: tx_hash, tx_height = self.db.fs_tx_hash(history["tx_num"]) - data = await self.get_transaction_detail(hash_to_hex_str(tx_hash), tx_height, history["tx_num"]) + data = await self.session_mgr.get_transaction_detail(hash_to_hex_str(tx_hash), tx_height, history["tx_num"]) if data and data["op"]: if data["op"] and (data["op"] == op_type or not op_type): res.append(data) @@ -3142,6 +3142,7 @@ def set_request_handlers(self, ptuple): 'blockchain.atomicals.find_containers': self.atomicals_search_containers, 'blockchain.atomicals.get_holders': self.atomicals_get_holders, 'blockchain.atomicals.transaction': self.atomicals_transaction, + 'blockchain.atomicals.transaction_global': self.session_mgr.transaction_global, 'blockchain.atomicals.transaction_by_height': self.transaction_by_height, 'blockchain.atomicals.transaction_by_atomical_id': self.transaction_by_atomical_id, 'blockchain.atomicals.transaction_by_scripthash': self.transaction_by_scripthash,