diff --git a/app/services/integrations/webhooks/chainhook/handlers/dao_charter_update_handler.py b/app/services/integrations/webhooks/chainhook/handlers/dao_charter_update_handler.py new file mode 100644 index 00000000..63a6922b --- /dev/null +++ b/app/services/integrations/webhooks/chainhook/handlers/dao_charter_update_handler.py @@ -0,0 +1,110 @@ +"""Handler for DAO charter update transactions.""" + +from app.backend.factory import backend +from app.backend.models import DAOBase, ExtensionFilter, ContractStatus +from app.services.integrations.webhooks.chainhook.handlers.base import ( + ChainhookEventHandler, +) +from app.services.integrations.webhooks.chainhook.models import TransactionWithReceipt +from app.services.integrations.webhooks.dao.models import ( + ContractType, + ExtensionsSubtype, +) +from app.services.processing.stacks_chainhook_adapter.parsers.clarity import ( + ClarityParser, +) + + +class DAOCharterUpdateHandler(ChainhookEventHandler): + """Handler for set-dao-charter contract calls. + + This handler detects transactions that update a DAO's charter and processes + the event to update the backend database. + """ + + def __init__(self): + super().__init__() + self.parser = ClarityParser(logger=self.logger) + + def can_handle_transaction(self, transaction: TransactionWithReceipt) -> bool: + """Check if this is a set-dao-charter transaction.""" + tx_data = self.extract_transaction_data(transaction) + tx_metadata = tx_data["tx_metadata"] + if not hasattr(tx_metadata, "kind") or tx_metadata.kind.type != "ContractCall": + return False + + if not tx_metadata.success: + return False + + contract_data = tx_metadata.kind.data + method = getattr(contract_data, "method", "") + contract_id = getattr(contract_data, "contract_identifier", "") + + return method == "set-dao-charter" and "-dao-charter" in contract_id + + async def handle_transaction(self, transaction: TransactionWithReceipt) -> None: + """Handle the charter update transaction.""" + tx_data = self.extract_transaction_data(transaction) + + if not tx_data["tx_metadata"].success: + self.logger.warning("Transaction failed, skipping charter update") + return + + # Find the print event (smart_contract_log) + print_events = [ + event + for event in tx_data["tx_metadata"].receipt.events + if event.type == "SmartContractEvent" + and "print" in event.data.get("topic", "") + ] + if not print_events: + self.logger.warning("No print event found in set-dao-charter transaction") + return + + # Parse the Clarity repr from the event + for event in print_events: + parsed_data = self.parser.parse(event.data) + if isinstance(parsed_data, dict) and "payload" in parsed_data: + payload = parsed_data["payload"] + dao_principal = payload.get("dao", "") + new_charter = payload.get("charter", "") + previous_charter = payload.get("previousCharter", "") + + self.logger.info( + f"Detected DAO charter update for DAO {dao_principal}: " + f"New charter length: {len(new_charter)}" + ) + + # Query for DAO ID via extensions + ext_filter = ExtensionFilter( + contract_principal=dao_principal, + type=ContractType.EXTENSIONS.value, + subtype=ExtensionsSubtype.DAO_CHARTER.value, + status=ContractStatus.DEPLOYED, + ) + extensions = backend.list_extensions(ext_filter) + if not extensions or not extensions[0].dao_id: + self.logger.error( + f"No matching DAO found for principal {dao_principal}" + ) + return + + dao_id = extensions[0].dao_id + + # Optional: Validate previous_charter + current_dao = backend.get_dao(dao_id) + if current_dao and current_dao.charter != previous_charter: + self.logger.warning( + "Charter mismatch, possible race condition - skipping" + ) + return + + # Update the DAO + update_data = DAOBase(charter=new_charter) + updated_dao = backend.update_dao(dao_id, update_data) + if updated_dao: + self.logger.info( + f"Successfully updated DAO {dao_id} with new charter" + ) + else: + self.logger.error(f"Failed to update DAO {dao_id}") diff --git a/app/services/processing/stacks_chainhook_adapter/filters/transaction.py b/app/services/processing/stacks_chainhook_adapter/filters/transaction.py index 6031a1ad..1a183c34 100644 --- a/app/services/processing/stacks_chainhook_adapter/filters/transaction.py +++ b/app/services/processing/stacks_chainhook_adapter/filters/transaction.py @@ -320,3 +320,23 @@ def create_dao_proposal_filter(success_only: bool = True) -> CompositeFilter: ) return CompositeFilter([propose_filter, conclude_filter, vote_filter], logic="OR") + + +def create_dao_charter_update_filter( + contract_pattern: Optional[str] = None, + success_only: bool = True, +) -> ContractCallFilter: + """Create a filter for set-dao-charter transactions. + + Args: + contract_pattern: Optional pattern to match contract identifier + success_only: Only match successful transactions + + Returns: + ContractCallFilter: Configured filter for set-dao-charter + """ + return ContractCallFilter( + method="set-dao-charter", + contract_pattern=contract_pattern or r".*-dao-charter", + success_only=success_only, + ) diff --git a/app/services/processing/stacks_chainhook_adapter/utils/template_manager.py b/app/services/processing/stacks_chainhook_adapter/utils/template_manager.py index 4a289588..9387b9eb 100644 --- a/app/services/processing/stacks_chainhook_adapter/utils/template_manager.py +++ b/app/services/processing/stacks_chainhook_adapter/utils/template_manager.py @@ -83,6 +83,7 @@ def load_templates(self): "send-many-stx-airdrop": "send-many-stx-airdrop.json", "vote-on-action-proposal": "vote-on-action-proposal.json", "coinbase": "coinbase-block.json", + "set-dao-charter": "set-dao-charter.json", } for template_name, filename in template_files.items(): @@ -121,16 +122,13 @@ def get_template_for_transaction_type( "coinbase": "coinbase", # Use coinbase template for tenure change + coinbase blocks "multi-vote-on-action-proposal": "vote-on-action-proposal", # Use single vote template "governance-and-airdrop-multi-tx": "send-many-governance-airdrop", # Use airdrop template + "set-dao-charter": "set-dao-charter", } template_name = type_mapping.get(transaction_type) if template_name and template_name in self.templates: return deepcopy(self.templates[template_name]) - # Fallback to a generic template (use conclude-action-proposal as base) - if "conclude-action-proposal" in self.templates: - return deepcopy(self.templates["conclude-action-proposal"]) - return None def populate_template( diff --git a/chainhook-data/set-dao-charter.json b/chainhook-data/set-dao-charter.json new file mode 100644 index 00000000..242d43fa --- /dev/null +++ b/chainhook-data/set-dao-charter.json @@ -0,0 +1,119 @@ +{ + "apply": [ + { + "block_identifier": { + "hash": "0xbd3d9e5acea30fdedeea3a7ed6730c8aa774b42e0cc61700f883a8754ad46df0", + "index": 3608542 + }, + "metadata": { + "bitcoin_anchor_block_identifier": { + "hash": "0x1cd55ccc77a67c9e146a2011e59e0809f0f5f6aa2b0f48e0b2024f15a805a1dd", + "index": 102928 + }, + "block_time": 1760769017, + "confirm_microblock_identifier": null, + "cycle_number": null, + "pox_cycle_index": 4751, + "pox_cycle_length": 20, + "pox_cycle_position": 8, + "reward_set": null, + "signer_bitvec": "013500000027ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1f", + "signer_public_keys": [ + "0x02e8620935d58ebffa23c260f6917cbd0915ea17d7a46df17e131540237d335504", + "0x036a44f61d85efa844b42475f107b106dc8fb209ae27813893c3269c59821e0333" + ], + "signer_signature": [ + "00108b3efa3a9f72631f5a9e1f7de9f9995f8b00117385bcd54e931fa9875ab78d599f33c1300fe49199e3cbc854130d68fe10ec26748e92c7bc10fe8d7d3de631", + "0015c9a9fab6329b2c4f407c490a3c7ba02f2db8d418608e3e268482a5cf332b2e031f02dd881f3891e6ae941ad78b76cc8163cc484e8426868001cc433d1698d8" + ], + "stacks_block_hash": "0xbd3d9e5acea30fdedeea3a7ed6730c8aa774b42e0cc61700f883a8754ad46df0", + "tenure_height": 92146 + }, + "parent_block_identifier": { + "hash": "0x51ecee55299a282deef7351659ea37ce28b5975710f54b1c153b2f8f9595d758", + "index": 3608541 + }, + "timestamp": 1760769017, + "transactions": [ + { + "metadata": { + "description": "invoked: ST2Q77H5HHT79JK4932JCFDX4VY6XA3Y1F61A25CD.aitest4-dao-charter::set-dao-charter(u\"aibtc is technocapital acceleration\")", + "execution_cost": { + "read_count": 18, + "read_length": 13091, + "runtime": 481904, + "write_count": 2, + "write_length": 203 + }, + "fee": 217, + "kind": { + "data": { + "args": [ + "u\"aibtc is technocapital acceleration\"" + ], + "contract_identifier": "ST2Q77H5HHT79JK4932JCFDX4VY6XA3Y1F61A25CD.aitest4-dao-charter", + "method": "set-dao-charter" + }, + "type": "ContractCall" + }, + "nonce": 780, + "position": { + "index": 0 + }, + "raw_tx": "0x80800000000400ae73c4b18e8e994c8918a4c7b7a4df8dd50fc1790000000000000212000000000000031f00016a5ab57ed3a3ea199fcc2e56cd7a86298da86243b109183be6aca465d0961f8d2fceea826875ac56d2906388efa39135833b1268ab3f745a83f0a2728aa81d66030100000000021aa372c8d8b2239bb9339c38c8f136d273b7134d821d6661636573332d616374696f6e2d70726f706f73616c2d766f74696e6718636f6e636c7564652d616374696f6e2d70726f706f73616c000000020100000000000000000000000000000021061aa372c8d8b2239bb9339c38c8f136d273b7134d821a6661636573332d616374696f6e2d73656e642d6d657373616765", + "receipt": { + "contract_calls_stack": [], + "events": [ + { + "data": { + "contract_identifier": "ST2Q77H5HHT79JK4932JCFDX4VY6XA3Y1F61A25CD.aitest4-dao-charter", + "topic": "print", + "value": { + "notification": "aitest4-dao-charter/set-dao-charter", + "payload": { + "burnHeight": 102928, + "charter": "aibtc is technocapital acceleration", + "contractCaller": "ST2Q77H5HHT79JK4932JCFDX4VY6XA3Y1F61A25CD", + "createdAt": 3608542, + "dao": "ST2Q77H5HHT79JK4932JCFDX4VY6XA3Y1F61A25CD.aitest4-dao-charter", + "previousCharter": "# CHARTER\\n\\n## MISSION\\n\\nAccelerate technocapitalism for human prosperity.\\n\\n## VALUES\\n\\nOur network believes that:\\n\\n- Technocapitalism leads to abundance.\\n- Deceleration leads to impoverishment.\\n- It is morally sound to accelerate technocapital.\\n\\n## COLLECTIVE ACTION\\n\\nThe AIBTC network collectively works towards a Current Task; a single, measurable directive. All contributions, evaluations, and payouts must advance the Current Task.\\n\\n## THE CURRENT TASK\\n\\nCreate engaging technocapital acceleration memes.\\n\\nQuote post the canonical Current Task from @aibtcdev on X with original technocapital acceleration memes that tastefully include an AIBTC watermark.\\n\\nAIBTC watermarks: https://github.com/aibtcdev/branding/tree/main/watermarks\\n\\n## GUIDELINES\\n\\nIn pursuit of our Mission and the Current Task, we adhere to the following Guidelines:\\n\\n- **Presidential Rule:** A President may be appointed and holds sole authority to issue a new Current Task and apply changes to this Charter.\\n- **Canonical Task Post:** All contributions must directly quote post or reply to the canonical X post that established the Current Task and clearly advance its directive.\\n- **Eligibility:** Only verified members holding and blue-check verified on X.com may submit contributions or receive rewards.\\n- **Completed Work:** Only finished, publicly verifiable work is eligible; drafts or promises are rejected.\\n- **Approval & Reward:** Agent approval is required; approved work earns BTC with onchain payouts and receipts.\\n- **Anti-Spam:** A small submission bond is required; failed entries forfeit it.\\n- **Block Rhythm:** Maximum one contribution approval per Bitcoin block.\\n- **Composability:** Smart contracts / extensions, if any, must execute via contribution approvals or rejections.\\n- **Safety:** No plagiarism, doxxing, or illegal content.\\n\\n## BENCHMARKS\\n\\nWe measure ongoing network health with the following benchmarks:\\n\\n- **Adoption:** >= 75% of circulating tokens cast votes per 12,000 BTC blocks.\\n- **Growth:** + >=10% unique contribution earners per 12,000 BTC blocks.\\n- **Retention:** >= 40% of monthly contribution earners return every 4,000 BTC block period.\\n- **Throughput:** 30-90 per contribution approvals per every 144 contributions submitted.\\n- **Credibility:** >= 99% of contribution approvals committed within 3 BTC blocks.", + "txSender": "ST2Q77H5HHT79JK4932JCFDX4VY6XA3Y1F61A25CD", + "version": 2 + } + } + }, + "position": { + "index": 0 + }, + "type": "SmartContractEvent" + } + ], + "mutated_assets_radius": [], + "mutated_contracts_radius": [ + "ST2Q77H5HHT79JK4932JCFDX4VY6XA3Y1F61A25CD.aitest4-dao-charter" + ] + }, + "result": "(ok true)", + "sender": "ST2Q77H5HHT79JK4932JCFDX4VY6XA3Y1F61A25CD", + "sponsor": null, + "success": true + }, + "operations": [], + "transaction_identifier": { + "hash": "0xd34eacbd93c614c1ee6e5dc1f1331ea731d90e9113a441bc3e00e9b51ae11fb7" + } + } + ] + } + ], + "chainhook": { + "is_streaming_blocks": false, + "predicate": { + "equals": 3608542, + "scope": "block_height" + }, + "uuid": "778568f2-2a7f-4b58-a52c-9b1810abeb0a" + }, + "events": [], + "rollback": [] +}