Skip to content
This repository was archived by the owner on Mar 18, 2026. It is now read-only.

Commit d7d4a2c

Browse files
allow for trackkng of wallet stx from chainhook instead
1 parent 72ad65d commit d7d4a2c

3 files changed

Lines changed: 609 additions & 0 deletions

File tree

app/services/integrations/webhooks/chainhook/handler.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
from app.services.integrations.webhooks.chainhook.handlers.sell_event_handler import (
3838
SellEventHandler,
3939
)
40+
from app.services.integrations.webhooks.chainhook.handlers.stx_event_handler import (
41+
STXEventHandler,
42+
)
4043
from app.services.integrations.webhooks.chainhook.models import ChainHookData
4144

4245

@@ -65,6 +68,7 @@ def __init__(self):
6568
AirdropSTXHandler(),
6669
BuyEventHandler(),
6770
SellEventHandler(),
71+
STXEventHandler(),
6872
DAOProposalHandler(),
6973
DAOProposalBurnHeightHandler(),
7074
DAOVoteHandler(),
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
"""Handler for capturing STX transfer events and transaction fees."""
2+
3+
from datetime import datetime
4+
5+
from app.backend.factory import backend
6+
from app.backend.models import (
7+
WalletBase,
8+
WalletFilter,
9+
)
10+
from app.lib.logger import configure_logger
11+
from app.services.integrations.webhooks.chainhook.handlers.base import (
12+
ChainhookEventHandler,
13+
)
14+
from app.services.integrations.webhooks.chainhook.models import TransactionWithReceipt
15+
16+
17+
class STXEventHandler(ChainhookEventHandler):
18+
"""Handler for capturing and tracking STX balance changes in agent wallets.
19+
20+
This handler identifies:
21+
1. STXTransferEvent operations where our wallets are senders or recipients
22+
2. Transaction fees paid by our wallets
23+
3. Any operation that affects STX balances in our agent wallets
24+
25+
STX amounts are tracked in micro-STX (1 STX = 1,000,000 micro-STX).
26+
"""
27+
28+
def __init__(self):
29+
"""Initialize the handler with a logger."""
30+
super().__init__()
31+
self.logger = configure_logger(self.__class__.__name__)
32+
33+
def can_handle_transaction(self, transaction: TransactionWithReceipt) -> bool:
34+
"""Check if this handler can handle the given transaction.
35+
36+
This handler can handle any transaction that:
37+
1. Has STXTransferEvent operations involving our wallets
38+
2. Has transaction fees paid by our wallets
39+
3. Contains STX operations with our wallet addresses
40+
41+
Args:
42+
transaction: The transaction to check
43+
44+
Returns:
45+
bool: True if this handler can handle the transaction, False otherwise
46+
"""
47+
tx_data = self.extract_transaction_data(transaction)
48+
tx_metadata = tx_data["tx_metadata"]
49+
50+
# Check if transaction sender is one of our wallets (for fee tracking)
51+
sender = tx_metadata.sender
52+
if self._is_our_wallet_address(sender):
53+
self.logger.debug(f"Found transaction from our wallet: {sender}")
54+
return True
55+
56+
# Check if any operations involve our wallets
57+
operations = transaction.operations
58+
if not operations:
59+
return False
60+
61+
for operation in operations:
62+
# Convert operation to dict if it's not already
63+
if hasattr(operation, "__dict__"):
64+
op_dict = operation.__dict__
65+
else:
66+
op_dict = operation
67+
68+
# Check if this is an STX operation
69+
if not self._is_stx_operation(op_dict):
70+
continue
71+
72+
# Check if the operation involves our wallet
73+
account_info = op_dict.get("account", {})
74+
account_address = account_info.get("address") if account_info else None
75+
76+
if account_address and self._is_our_wallet_address(account_address):
77+
self.logger.debug(
78+
f"Found STX operation involving our wallet: {account_address}"
79+
)
80+
return True
81+
82+
return False
83+
84+
def _is_stx_operation(self, operation_dict: dict) -> bool:
85+
"""Check if the operation is an STX-related operation."""
86+
# Check if operation type suggests STX movement
87+
operation_type = operation_dict.get("type", "").upper()
88+
if operation_type not in ["CREDIT", "DEBIT"]:
89+
return False
90+
91+
# Check if the currency is STX
92+
amount_info = operation_dict.get("amount", {})
93+
currency_info = amount_info.get("currency", {}) if amount_info else {}
94+
currency_symbol = currency_info.get("symbol", "") if currency_info else ""
95+
96+
return currency_symbol == "STX"
97+
98+
def _is_our_wallet_address(self, address: str) -> bool:
99+
"""Check if the given address belongs to one of our wallets."""
100+
if not address:
101+
return False
102+
103+
# Check mainnet addresses
104+
wallets = backend.list_wallets(WalletFilter(mainnet_address=address))
105+
if wallets:
106+
return True
107+
108+
# Check testnet addresses
109+
wallets = backend.list_wallets(WalletFilter(testnet_address=address))
110+
return bool(wallets)
111+
112+
def _get_wallet_by_address(self, address: str):
113+
"""Get wallet by address (mainnet or testnet)."""
114+
if not address:
115+
return None
116+
117+
# Check mainnet addresses first
118+
wallets = backend.list_wallets(WalletFilter(mainnet_address=address))
119+
if wallets:
120+
return wallets[0]
121+
122+
# Check testnet addresses
123+
wallets = backend.list_wallets(WalletFilter(testnet_address=address))
124+
if wallets:
125+
return wallets[0]
126+
127+
return None
128+
129+
async def handle_transaction(self, transaction: TransactionWithReceipt) -> None:
130+
"""Handle STX transfer transactions and fee payments involving our wallets."""
131+
tx_data = self.extract_transaction_data(transaction)
132+
tx_id = tx_data["tx_id"]
133+
tx_metadata = tx_data["tx_metadata"]
134+
135+
# Handle transaction fees paid by our wallets
136+
sender = tx_metadata.sender
137+
transaction_fee = tx_metadata.fee
138+
139+
if self._is_our_wallet_address(sender) and transaction_fee > 0:
140+
await self._handle_transaction_fee(sender, transaction_fee, tx_id)
141+
142+
# Handle STX transfer operations
143+
operations = transaction.operations
144+
if operations:
145+
for operation in operations:
146+
await self._process_stx_operation(operation, tx_id)
147+
148+
async def _handle_transaction_fee(
149+
self, sender_address: str, fee_amount: int, tx_id: str
150+
) -> None:
151+
"""Handle transaction fees paid by our wallets."""
152+
wallet = self._get_wallet_by_address(sender_address)
153+
if not wallet:
154+
return
155+
156+
self.logger.info(
157+
f"Processing transaction fee: {fee_amount} micro-STX paid by wallet {wallet.id} "
158+
f"({sender_address}) in transaction {tx_id}"
159+
)
160+
161+
# Deduct fee from wallet balance
162+
await self._update_wallet_balance(wallet, -fee_amount, "transaction_fee", tx_id)
163+
164+
async def _process_stx_operation(self, operation, tx_id: str) -> None:
165+
"""Process a single STX operation."""
166+
# Convert operation to dict if it's not already
167+
if hasattr(operation, "__dict__"):
168+
op_dict = operation.__dict__
169+
else:
170+
op_dict = operation
171+
172+
# Check if this is an STX operation
173+
if not self._is_stx_operation(op_dict):
174+
return
175+
176+
# Extract operation details
177+
account_info = op_dict.get("account", {})
178+
account_address = account_info.get("address") if account_info else None
179+
180+
if not account_address or not self._is_our_wallet_address(account_address):
181+
return
182+
183+
amount_info = op_dict.get("amount", {})
184+
amount_value = amount_info.get("value", 0) if amount_info else 0
185+
operation_type = op_dict.get("type", "").upper()
186+
operation_status = op_dict.get("status", "").upper()
187+
188+
# Only process successful operations
189+
if operation_status != "SUCCESS":
190+
self.logger.debug(f"Skipping non-successful operation: {operation_status}")
191+
return
192+
193+
wallet = self._get_wallet_by_address(account_address)
194+
if not wallet:
195+
return
196+
197+
self.logger.info(
198+
f"Processing STX operation: {operation_type} {amount_value} micro-STX "
199+
f"for wallet {wallet.id} ({account_address}) in transaction {tx_id}"
200+
)
201+
202+
# Calculate balance change based on operation type
203+
balance_change = 0
204+
operation_description = ""
205+
206+
if operation_type == "CREDIT":
207+
balance_change = amount_value
208+
operation_description = "stx_transfer_received"
209+
elif operation_type == "DEBIT":
210+
balance_change = -amount_value
211+
operation_description = "stx_transfer_sent"
212+
213+
if balance_change != 0:
214+
await self._update_wallet_balance(
215+
wallet, balance_change, operation_description, tx_id
216+
)
217+
218+
async def _update_wallet_balance(
219+
self, wallet, balance_change: int, operation_type: str, tx_id: str
220+
) -> None:
221+
"""Update the STX balance for a wallet."""
222+
# Get current balance (default to 0 if None)
223+
current_balance_str = wallet.stx_balance or "0"
224+
current_balance = int(current_balance_str)
225+
226+
# Calculate new balance
227+
new_balance = current_balance + balance_change
228+
229+
# Ensure balance doesn't go negative (shouldn't happen in practice, but safety check)
230+
if new_balance < 0:
231+
self.logger.warning(
232+
f"Wallet {wallet.id} balance would go negative: {current_balance} + {balance_change} = {new_balance}. "
233+
f"Setting to 0. Transaction: {tx_id}"
234+
)
235+
new_balance = 0
236+
237+
# Update only the balance fields
238+
update_data = WalletBase(
239+
stx_balance=str(new_balance),
240+
balance_updated_at=datetime.now(),
241+
)
242+
243+
backend.update_wallet(wallet.id, update_data)
244+
245+
self.logger.info(
246+
f"Updated STX balance for wallet {wallet.id}: "
247+
f"{current_balance} -> {new_balance} micro-STX "
248+
f"(change: {balance_change:+d}, operation: {operation_type}, tx: {tx_id})"
249+
)
250+
251+
# Convert to STX for logging (1 STX = 1,000,000 micro-STX)
252+
current_stx = current_balance / 1_000_000
253+
new_stx = new_balance / 1_000_000
254+
change_stx = balance_change / 1_000_000
255+
256+
self.logger.info(
257+
f"STX balance in human-readable format: "
258+
f"{current_stx:.6f} -> {new_stx:.6f} STX "
259+
f"(change: {change_stx:+.6f} STX)"
260+
)

0 commit comments

Comments
 (0)