From 80b16fad016e5e4281d45fd7c634631aab03a73b Mon Sep 17 00:00:00 2001 From: human058382928 <162091348+human058382928@users.noreply.github.com> Date: Mon, 9 Jun 2025 17:48:00 -0700 Subject: [PATCH] update --- agent-tools-ts | 2 +- services/runner/tasks/chain_state_monitor.py | 312 +++++++++++++++++-- 2 files changed, 294 insertions(+), 20 deletions(-) diff --git a/agent-tools-ts b/agent-tools-ts index 45cd17a1..2b0d11f5 160000 --- a/agent-tools-ts +++ b/agent-tools-ts @@ -1 +1 @@ -Subproject commit 45cd17a1daae780c1cf7451000995dfe0124558c +Subproject commit 2b0d11f5d1ef063a49455fb1e4ac77367ac6d521 diff --git a/services/runner/tasks/chain_state_monitor.py b/services/runner/tasks/chain_state_monitor.py index 2487c801..446fffd5 100644 --- a/services/runner/tasks/chain_state_monitor.py +++ b/services/runner/tasks/chain_state_monitor.py @@ -101,6 +101,18 @@ def _convert_to_chainhook_format( Returns: Dict formatted as a chainhook webhook payload """ + # Get detailed block information from API + try: + block_data = self.hiro_api.get_block_by_height(block_height) + logger.debug( + f"Retrieved block data for height {block_height}: {block_data}" + ) + except Exception as e: + logger.warning( + f"Could not fetch detailed block data for height {block_height}: {e}" + ) + block_data = {} + # Create block identifier block_identifier = BlockIdentifier(hash=block_hash, index=block_height) @@ -109,18 +121,99 @@ def _convert_to_chainhook_format( hash=parent_hash, index=block_height - 1 ) - # Create basic metadata + # Extract block time from block data or transaction data, fallback to current time + block_time = None + if isinstance(block_data, dict): + block_time = block_data.get("block_time") + elif hasattr(block_data, "block_time"): + block_time = block_data.block_time + + # If block_time not available from block data, try from first transaction + if not block_time and transactions.results: + tx = transactions.results[0] + if isinstance(tx, dict): + block_time = tx.get("block_time") + else: + block_time = getattr(tx, "block_time", None) + + # Fallback to current timestamp if still not found + if not block_time: + block_time = int(datetime.now().timestamp()) + logger.warning( + f"Using current timestamp for block {block_height} as block_time was not available" + ) + + # Create comprehensive metadata with all available fields metadata = BlockMetadata( - block_time=int(datetime.now().timestamp()), stacks_block_hash=block_hash + block_time=block_time, + stacks_block_hash=block_hash, ) - # Add bitcoin anchor block identifier if burn block height is available - if burn_block_height is not None: + # Extract additional metadata from block data if available + if isinstance(block_data, dict): + # Bitcoin anchor block identifier with proper hash + bitcoin_anchor_info = block_data.get("bitcoin_anchor_block_identifier", {}) + bitcoin_anchor_hash = ( + bitcoin_anchor_info.get("hash", "") + if isinstance(bitcoin_anchor_info, dict) + else "" + ) + if burn_block_height is not None: + metadata.bitcoin_anchor_block_identifier = BlockIdentifier( + hash=bitcoin_anchor_hash, index=burn_block_height + ) + + # PoX cycle information + pox_cycle_index = block_data.get("pox_cycle_index") + if pox_cycle_index is not None: + metadata.pox_cycle_index = pox_cycle_index + + pox_cycle_length = block_data.get("pox_cycle_length") + if pox_cycle_length is not None: + metadata.pox_cycle_length = pox_cycle_length + + pox_cycle_position = block_data.get("pox_cycle_position") + if pox_cycle_position is not None: + metadata.pox_cycle_position = pox_cycle_position + + cycle_number = block_data.get("cycle_number") + if cycle_number is not None: + metadata.cycle_number = cycle_number + + # Signer information + signer_bitvec = block_data.get("signer_bitvec") + if signer_bitvec is not None: + metadata.signer_bitvec = signer_bitvec + + signer_public_keys = block_data.get("signer_public_keys") + if signer_public_keys is not None: + metadata.signer_public_keys = signer_public_keys + + signer_signature = block_data.get("signer_signature") + if signer_signature is not None: + metadata.signer_signature = signer_signature + + # Other metadata + tenure_height = block_data.get("tenure_height") + if tenure_height is not None: + metadata.tenure_height = tenure_height + + confirm_microblock_identifier = block_data.get( + "confirm_microblock_identifier" + ) + if confirm_microblock_identifier is not None: + metadata.confirm_microblock_identifier = confirm_microblock_identifier + + reward_set = block_data.get("reward_set") + if reward_set is not None: + metadata.reward_set = reward_set + elif burn_block_height is not None: + # Fallback: create basic bitcoin anchor block identifier without hash metadata.bitcoin_anchor_block_identifier = BlockIdentifier( hash="", index=burn_block_height ) - # Convert transactions to chainhook format + # Convert transactions to chainhook format with enhanced data chainhook_transactions = [] for tx in transactions.results: # Handle tx as either dict or object @@ -144,6 +237,15 @@ def _convert_to_chainhook_format( if isinstance(tx.get("tx_result"), dict) else "" ) + # Extract events and additional transaction data + events = tx.get("events", []) + raw_tx = tx.get("raw_tx", "") + + # Create better description based on transaction type and data + description = self._create_transaction_description(tx) + + # Extract token transfer data if available + token_transfer = tx.get("token_transfer") else: tx_id = tx.tx_id exec_cost_read_count = tx.execution_cost_read_count @@ -162,13 +264,41 @@ def _convert_to_chainhook_format( tx_result_repr = ( tx.tx_result.repr if hasattr(tx.tx_result, "repr") else "" ) + events = getattr(tx, "events", []) + raw_tx = getattr(tx, "raw_tx", "") + + # Create better description + description = self._create_transaction_description(tx) + + # Extract token transfer data + token_transfer = getattr(tx, "token_transfer", None) # Create transaction identifier tx_identifier = TransactionIdentifier(hash=tx_id) - # Create transaction metadata + # Convert events to proper format + receipt_events = [] + for event in events: + if isinstance(event, dict): + receipt_events.append( + { + "data": event.get("data", {}), + "position": {"index": event.get("event_index", 0)}, + "type": event.get("event_type", ""), + } + ) + else: + receipt_events.append( + { + "data": getattr(event, "data", {}), + "position": {"index": getattr(event, "event_index", 0)}, + "type": getattr(event, "event_type", ""), + } + ) + + # Create transaction metadata with proper receipt tx_metadata = { - "description": f"Transaction {tx_id}", + "description": description, "execution_cost": { "read_count": exec_cost_read_count, "read_length": exec_cost_read_length, @@ -179,15 +309,15 @@ def _convert_to_chainhook_format( "fee": ( int(fee_rate) if isinstance(fee_rate, str) and fee_rate.isdigit() - else 0 + else int(fee_rate) if isinstance(fee_rate, (int, float)) else 0 ), "kind": {"type": tx_type}, "nonce": nonce, "position": {"index": tx_index}, - "raw_tx": "", # We don't have this from the v2 API + "raw_tx": raw_tx, "receipt": { "contract_calls_stack": [], - "events": [], + "events": receipt_events, "mutated_assets_radius": [], "mutated_contracts_radius": [], }, @@ -197,11 +327,14 @@ def _convert_to_chainhook_format( "success": tx_status == "success", } + # Generate operations based on transaction type and data + operations = self._create_transaction_operations(tx, token_transfer) + # Create transaction with receipt tx_with_receipt = TransactionWithReceipt( transaction_identifier=tx_identifier, metadata=tx_metadata, - operations=[], + operations=operations, ) chainhook_transactions.append(tx_with_receipt) @@ -211,7 +344,7 @@ def _convert_to_chainhook_format( block_identifier=block_identifier, parent_block_identifier=parent_block_identifier, metadata=metadata, - timestamp=int(datetime.now().timestamp()), + timestamp=block_time, transactions=chainhook_transactions, ) @@ -228,22 +361,38 @@ def _convert_to_chainhook_format( apply=[apply_block], chainhook=chainhook_info, events=[], rollback=[] ) - # Convert to dict for webhook processing + # Convert to dict for webhook processing with complete metadata metadata_dict = { "block_time": apply_block.metadata.block_time, "stacks_block_hash": apply_block.metadata.stacks_block_hash, } - # Add bitcoin anchor block identifier if present - if ( - hasattr(apply_block.metadata, "bitcoin_anchor_block_identifier") - and apply_block.metadata.bitcoin_anchor_block_identifier - ): + # Add all available metadata fields + if apply_block.metadata.bitcoin_anchor_block_identifier: metadata_dict["bitcoin_anchor_block_identifier"] = { "hash": apply_block.metadata.bitcoin_anchor_block_identifier.hash, "index": apply_block.metadata.bitcoin_anchor_block_identifier.index, } + # Add optional metadata fields if they exist + optional_fields = [ + "pox_cycle_index", + "pox_cycle_length", + "pox_cycle_position", + "cycle_number", + "signer_bitvec", + "signer_public_keys", + "signer_signature", + "tenure_height", + "confirm_microblock_identifier", + "reward_set", + ] + + for field in optional_fields: + value = getattr(apply_block.metadata, field, None) + if value is not None: + metadata_dict[field] = value + return { "apply": [ { @@ -263,7 +412,7 @@ def _convert_to_chainhook_format( "hash": tx.transaction_identifier.hash }, "metadata": tx.metadata, - "operations": [], + "operations": tx.operations, } for tx in apply_block.transactions ], @@ -281,6 +430,131 @@ def _convert_to_chainhook_format( "rollback": [], } + def _create_transaction_description(self, tx) -> str: + """Create a meaningful transaction description based on transaction data. + + Args: + tx: Transaction data (dict or object) + + Returns: + str: Human-readable transaction description + """ + if isinstance(tx, dict): + tx_type = tx.get("tx_type", "") + token_transfer = tx.get("token_transfer") + else: + tx_type = getattr(tx, "tx_type", "") + token_transfer = getattr(tx, "token_transfer", None) + + if ( + tx_type in ["token_transfer", "stx_transfer", "NativeTokenTransfer"] + and token_transfer + ): + if isinstance(token_transfer, dict): + amount = token_transfer.get("amount", "0") + recipient = token_transfer.get("recipient_address", "") + sender = ( + tx.get("sender_address", "") + if isinstance(tx, dict) + else getattr(tx, "sender_address", "") + ) + else: + amount = getattr(token_transfer, "amount", "0") + recipient = getattr(token_transfer, "recipient_address", "") + sender = ( + tx.get("sender_address", "") + if isinstance(tx, dict) + else getattr(tx, "sender_address", "") + ) + + return f"transfered: {amount} µSTX from {sender} to {recipient}" + elif tx_type == "coinbase": + return "coinbase transaction" + elif tx_type == "contract_call": + if isinstance(tx, dict): + contract_call = tx.get("contract_call", {}) + if isinstance(contract_call, dict): + contract_id = contract_call.get("contract_id", "") + function_name = contract_call.get("function_name", "") + return f"contract call: {contract_id}::{function_name}" + else: + contract_call = getattr(tx, "contract_call", None) + if contract_call: + contract_id = getattr(contract_call, "contract_id", "") + function_name = getattr(contract_call, "function_name", "") + return f"contract call: {contract_id}::{function_name}" + + # Fallback description + tx_id = ( + tx.get("tx_id", "") if isinstance(tx, dict) else getattr(tx, "tx_id", "") + ) + return f"Transaction {tx_id}" + + def _create_transaction_operations( + self, tx, token_transfer=None + ) -> List[Dict[str, Any]]: + """Create transaction operations based on transaction type and data. + + Args: + tx: Transaction data (dict or object) + token_transfer: Token transfer data if available + + Returns: + List[Dict[str, Any]]: List of operations for the transaction + """ + operations = [] + + if isinstance(tx, dict): + tx_type = tx.get("tx_type", "") + sender_address = tx.get("sender_address", "") + else: + tx_type = getattr(tx, "tx_type", "") + sender_address = getattr(tx, "sender_address", "") + + # Handle token transfers + if ( + tx_type in ["token_transfer", "stx_transfer", "NativeTokenTransfer"] + and token_transfer + ): + if isinstance(token_transfer, dict): + amount = int(token_transfer.get("amount", "0")) + recipient = token_transfer.get("recipient_address", "") + else: + amount = int(getattr(token_transfer, "amount", "0")) + recipient = getattr(token_transfer, "recipient_address", "") + + # Debit operation (sender) + operations.append( + { + "account": {"address": sender_address}, + "amount": { + "currency": {"decimals": 6, "symbol": "STX"}, + "value": amount, + }, + "operation_identifier": {"index": 0}, + "related_operations": [{"index": 1}], + "status": "SUCCESS", + "type": "DEBIT", + } + ) + + # Credit operation (recipient) + operations.append( + { + "account": {"address": recipient}, + "amount": { + "currency": {"decimals": 6, "symbol": "STX"}, + "value": amount, + }, + "operation_identifier": {"index": 1}, + "related_operations": [{"index": 0}], + "status": "SUCCESS", + "type": "CREDIT", + } + ) + + return operations + async def _execute_impl(self, context: JobContext) -> List[ChainStateMonitorResult]: """Run the chain state monitoring task.""" # Use the configured network