diff --git a/electrumx/lib/avm/avm.py b/electrumx/lib/avm/avm.py index e4c46e60..473e4418 100644 --- a/electrumx/lib/avm/avm.py +++ b/electrumx/lib/avm/avm.py @@ -19,6 +19,23 @@ from electrumx.lib.atomicals_blueprint_builder import AtomicalsTransferBlueprintBuilder from cbor2 import loads +# Updates the atomicals_spent_at_inputs structure for any consumed atomicals via either deploy or call +def remove_consumed_atomicals(ft_adds, nft_puts, atomicals_spent_at_inputs): + print(f'ft_adds={ft_adds}') + print(f'nft_puts={nft_puts}') + print(f'atomicals_spent_at_inputs={atomicals_spent_at_inputs}') + # Clean up and remove the atomical entry if it was consumed + for idx, atomical_entry_list in atomicals_spent_at_inputs.items(): + i = 0 + for atomical_entry in atomical_entry_list: + atomical_id = atomical_entry['atomical_id'] + # It was consumed therefore remove it from the input + if ft_adds.get(atomical_id) or nft_puts.get(atomical_id): + del atomical_entry[i] + break + i += 1 + return + class CallCommandResult: def __init__(self, success, reactor_context, error=None): self.success = success @@ -102,9 +119,7 @@ def execute(self): print(f'updated_reactor_state.ft_balances={loads(updated_reactor_state.ft_balances)}') print(f'updated_reactor_state.nft_balances={loads(updated_reactor_state.nft_balances)}') if updated_reactor_state: - # Todo: Only clear off the atomicals here that were actually accepted by the script - self.request_tx_context.atomicals_spent_at_inputs = {} - # Todo: Color the FT/NFT outputs + remove_consumed_atomicals(updated_reactor_state.ft_adds, updated_reactor_state.nft_puts, self.atomicals_spent_at_inputs) return CallCommandResult(True, updated_reactor_state) except AtomicalConsensusExecutionError as ex: print(f'AtomicalConsensusExecutionError ex={ex}') @@ -183,13 +198,13 @@ def execute(self): loads(updated_reactor_state.ft_balances) loads(updated_reactor_state.nft_balances) if updated_reactor_state: - self.request_tx_context.atomicals_spent_at_inputs = {} + remove_consumed_atomicals(updated_reactor_state.ft_adds, updated_reactor_state.nft_puts, self.atomicals_spent_at_inputs) return DeployCommandResult(True, updated_reactor_state) except AtomicalConsensusExecutionError as ex: print(f'AtomicalConsensusExecutionError ex={ex}') return DeployCommandResult(False, None, ex) raise ValueError(f'Critical call error') - + class AVMFactory: def __init__(self, logger, get_atomicals_id_mint_info, blockchain_context: RequestBlockchainContext, protocol_mint_data): self.logger = logger diff --git a/electrumx/server/block_processor.py b/electrumx/server/block_processor.py index 57891b0e..3b171dd6 100644 --- a/electrumx/server/block_processor.py +++ b/electrumx/server/block_processor.py @@ -1200,7 +1200,6 @@ def create_or_delete_protocol_entry_if_requested(self, mint_info, height, Delete self.put_name_element_template(b'pr', b'', request_protocol, mint_info['commit_tx_num'], mint_info['id'], self.protocol_data_cache) return True - # mint_info, tx_hash, tx, header, protocol_mint_data, height, Delete): def create_or_delete_contract_entry_if_requested(self, protocol_atomical_id, mint_info, tx_hash, tx, header, protocol_mint_data, operations_found_at_inputs, atomicals_spent_at_inputs, height, Delete=False): request_contract = mint_info.get('$request_contract') if not request_contract: @@ -1217,9 +1216,11 @@ def create_or_delete_contract_entry_if_requested(self, protocol_atomical_id, min return False if Delete: self.delete_name_element_template(b'cr', b'', request_contract, mint_info['commit_tx_num'], mint_info['id'], self.reactor_data_cache) + # We can use dummy reactor context for delete case + self.put_or_delete_reactor_states(mint_info['id'], None, height, True) else: # - # General blockchain context such as headers and height + # Add general blockchain context such as headers and height # headers = {} # Todo add the last N=1000 headers potentially @@ -1504,21 +1505,16 @@ def get_latest_reactor_states(self, reactor_id): return latest_height_db, latest_state_db return None, None - def put_or_delete_reactor_states(self, reactor_id, reactor_context: ReactorContext, height, Delete): + def put_or_delete_reactor_states(self, reactor_id, reactor_context, height, Delete): self.logger.info(f'put_or_delete_reactor_states reactor_id={location_id_bytes_to_compact(reactor_id)}') found_reactor_record = self.reactor_states_cache.get(reactor_id) - - if not reactor_context.state or not reactor_context.ft_balances or not reactor_context.nft_balances: - raise IndexError('Developer error') - if not Delete: if not found_reactor_record: self.reactor_states_cache[reactor_id] = {} - self.reactor_states_cache[reactor_id][height] = pickle.dumps(reactor_context) else: if found_reactor_record: - self.reactor_states_cache[reactor_id].pop(height, None) + del self.reactor_states_cache[reactor_id][height] db_key = b'rcs' + reactor_id + pack_be_uint32(height) self.db_deletes.append(db_key) @@ -1697,10 +1693,6 @@ def create_or_delete_atomical(self, operations_found_at_inputs, atomicals_spent_ return None if not Delete: - - # Absorbs all the atomicals tokens because the only way have gotten this far is if a payable method was called - atomicals_spent_at_inputs.clear() - self.logger.info(f'mint-reactor: {hash_to_hex_str(tx_hash)}') self.put_op_data(tx_num, tx_hash, "mint-reactor") @@ -3341,20 +3333,12 @@ def advance_txs( 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}') - # Todo ensure this call modifies the atomicals_spent_at_inputs if contract absorbs NFT/FT + # This call modifies the atomicals_spent_at_inputs if contract absorbs NFT/FT request_id = self.create_or_delete_call(atomicals_operations_found_at_inputs, atomicals_spent_at_inputs, tx, tx_hash, tx_num, header, height, False) if request_id: already_found_valid_operation = True has_at_least_one_valid_atomicals_operation = True - # 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(): - has_at_least_one_valid_atomicals_operation = True - 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 @@ -3373,6 +3357,7 @@ def advance_txs( # Create NFT/FT atomicals if it is defined in the tx if not already_found_valid_operation: + # An AVM deploy call modifies the atomicals_spent_at_inputs if contract absorbs NFT/FT 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) if created_atomical_id: already_found_valid_operation = True @@ -3382,6 +3367,17 @@ def advance_txs( append_hashX(double_sha256(created_atomical_id)) self.logger.debug(f'advance_txs: create_or_delete_atomical created_atomical_id atomical_id={created_atomical_id.hex()}, tx_hash={hash_to_hex_str(tx_hash)}') + # Color the outputs of any transferred NFT/FT atomicals according to the rules + # Note: this was moved AFTER create_or_delete_atomical because we must account for clearing entries in atomicals_spent_at_inputs if + # the deploy call absorbed atomicals on reactor contract mint. + # The atomicals not absorbed by the call or deploy, just get transferred the normal way according to the predefined 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(): + has_at_least_one_valid_atomicals_operation = True + 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)) + # Check if there were any regular 'dat' files definitions if not already_found_valid_operation: if self.create_or_delete_data_location(tx_hash, atomicals_operations_found_at_inputs):