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

Commit bc83ecb

Browse files
make discord messages more robust
1 parent 25e1eac commit bc83ecb

1 file changed

Lines changed: 158 additions & 76 deletions

File tree

app/services/integrations/webhooks/chainhook/handlers/dao_proposal_burn_height_handler.py

Lines changed: 158 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ def __init__(self):
3636
super().__init__()
3737
self.logger = configure_logger(self.__class__.__name__)
3838
self.chainhook_data: Optional[ChainHookData] = None
39+
self._processed_burn_heights: set = (
40+
set()
41+
) # Track processed burn heights in this session
3942

4043
def set_chainhook_data(self, data: ChainHookData) -> None:
4144
"""Set the chainhook data for this handler.
@@ -134,7 +137,7 @@ def _queue_message_exists(
134137
filters = QueueMessageFilter(
135138
type=message_type,
136139
dao_id=dao_id,
137-
is_processed=False,
140+
# Check both processed and unprocessed to prevent duplicates
138141
)
139142

140143
if wallet_id:
@@ -148,6 +151,44 @@ def _queue_message_exists(
148151
for msg in existing_messages
149152
)
150153

154+
def _discord_message_exists(
155+
self, proposal_id: UUID, dao_id: UUID, proposal_status: str
156+
) -> bool:
157+
"""Check if a Discord message already exists for this proposal and status.
158+
159+
Args:
160+
proposal_id: The proposal ID
161+
dao_id: The DAO ID
162+
proposal_status: The specific proposal status (e.g., 'veto_window_open')
163+
164+
Returns:
165+
bool: True if message exists, False otherwise
166+
"""
167+
filters = QueueMessageFilter(
168+
type=QueueMessageType.get_or_create("discord"),
169+
dao_id=dao_id,
170+
# Check both processed and unprocessed to prevent duplicates
171+
)
172+
173+
existing_messages = backend.list_queue_messages(filters=filters)
174+
175+
# Create a unique identifier for this specific message type
176+
unique_identifier = f"proposal-{proposal_id}-{proposal_status}"
177+
178+
# Check for existing messages with matching proposal status and proposal reference
179+
return any(
180+
msg.message
181+
and msg.message.get("proposal_status") == proposal_status
182+
and (
183+
# Check for unique identifier in content
184+
unique_identifier in str(msg.message.get("content", ""))
185+
or
186+
# Fallback: check for proposal ID in content
187+
str(proposal_id) in str(msg.message.get("content", ""))
188+
)
189+
for msg in existing_messages
190+
)
191+
151192
async def handle_transaction(self, transaction: TransactionWithReceipt) -> None:
152193
"""Handle burn height check transactions.
153194
@@ -158,100 +199,136 @@ async def handle_transaction(self, transaction: TransactionWithReceipt) -> None:
158199
Args:
159200
transaction: The transaction to handle
160201
"""
161-
tx_data = self.extract_transaction_data(transaction)
162-
burn_height = self._get_burn_height(tx_data)
202+
try:
203+
tx_data = self.extract_transaction_data(transaction)
204+
burn_height = self._get_burn_height(tx_data)
163205

164-
if burn_height is None:
165-
self.logger.warning("Could not determine burn height from transaction")
166-
return
206+
if burn_height is None:
207+
self.logger.warning("Could not determine burn height from transaction")
208+
return
167209

168-
self.logger.info(f"Processing burn height: {burn_height}")
169-
170-
# Find proposals that should start at this height
171-
proposals = backend.list_proposals(
172-
filters=ProposalFilter(
173-
status=ContractStatus.DEPLOYED,
174-
)
175-
)
210+
# Skip if we've already processed this burn height in this session
211+
burn_height_key = f"burn_{burn_height}"
212+
if burn_height_key in self._processed_burn_heights:
213+
self.logger.debug(
214+
f"Burn height {burn_height} already processed in this session, skipping"
215+
)
216+
return
176217

177-
# Filter proposals that should start or end at this burn height
178-
vote_proposals = [
179-
p
180-
for p in proposals
181-
if p.vote_start is not None
182-
and p.vote_end is not None
183-
and p.vote_start == burn_height
184-
and p.content is not None # Ensure content exists
185-
]
218+
self.logger.info(f"Processing burn height: {burn_height}")
186219

187-
end_proposals = [
188-
p
189-
for p in proposals
190-
if p.vote_start is not None
191-
and p.exec_start is not None
192-
and p.exec_start == burn_height
193-
and p.content is not None # Ensure content exists
194-
]
220+
# Mark this burn height as being processed
221+
self._processed_burn_heights.add(burn_height_key)
195222

196-
# Add veto window proposals
197-
veto_start_proposals = [
198-
p
199-
for p in proposals
200-
if p.vote_end is not None
201-
and p.vote_end == burn_height
202-
and p.content is not None
203-
]
223+
# Find proposals that should start at this height
224+
proposals = backend.list_proposals(
225+
filters=ProposalFilter(
226+
status=ContractStatus.DEPLOYED,
227+
)
228+
)
204229

205-
veto_end_proposals = [
206-
p
207-
for p in proposals
208-
if p.exec_start is not None
209-
and p.exec_start == burn_height
210-
and p.content is not None
211-
]
230+
# Filter proposals that should start or end at this burn height
231+
vote_proposals = [
232+
p
233+
for p in proposals
234+
if p.vote_start is not None
235+
and p.vote_end is not None
236+
and p.vote_start == burn_height
237+
and p.content is not None # Ensure content exists
238+
]
239+
240+
end_proposals = [
241+
p
242+
for p in proposals
243+
if p.vote_start is not None
244+
and p.exec_start is not None
245+
and p.exec_start == burn_height
246+
and p.content is not None # Ensure content exists
247+
]
248+
249+
# Add veto window proposals
250+
veto_start_proposals = [
251+
p
252+
for p in proposals
253+
if p.vote_end is not None
254+
and p.vote_end == burn_height
255+
and p.content is not None
256+
]
257+
258+
veto_end_proposals = [
259+
p
260+
for p in proposals
261+
if p.exec_start is not None
262+
and p.exec_start == burn_height
263+
and p.content is not None
264+
]
265+
266+
if not (
267+
vote_proposals
268+
or end_proposals
269+
or veto_start_proposals
270+
or veto_end_proposals
271+
):
272+
self.logger.debug(
273+
f"No eligible proposals found for burn height {burn_height}"
274+
)
275+
return
212276

213-
if not (
214-
vote_proposals
215-
or end_proposals
216-
or veto_start_proposals
217-
or veto_end_proposals
218-
):
219277
self.logger.info(
220-
f"No eligible proposals found for burn height {burn_height}"
278+
f"Found {len(vote_proposals)} proposals to vote, "
279+
f"{len(end_proposals)} proposals to conclude, "
280+
f"{len(veto_start_proposals)} proposals entering veto window, "
281+
f"{len(veto_end_proposals)} proposals ending veto window"
221282
)
222-
return
223283

224-
self.logger.info(
225-
f"Found {len(vote_proposals)} proposals to vote, "
226-
f"{len(end_proposals)} proposals to conclude, "
227-
f"{len(veto_start_proposals)} proposals entering veto window, "
228-
f"{len(veto_end_proposals)} proposals ending veto window"
229-
)
284+
# Process veto window start notifications
285+
self._process_veto_window_start_notifications(veto_start_proposals)
286+
287+
# Process veto window end notifications
288+
self._process_veto_window_end_notifications(veto_end_proposals)
289+
290+
# Process proposals that are ending
291+
self._process_ending_proposals(end_proposals)
292+
293+
# Process proposals that are ready for voting
294+
self._process_voting_proposals(vote_proposals)
295+
296+
except Exception as e:
297+
self.logger.error(
298+
f"Error processing transaction in DAOProposalBurnHeightHandler: {str(e)}",
299+
exc_info=True,
300+
)
301+
# Remove the burn height from processed set on error to allow retry
302+
if "burn_height_key" in locals():
303+
self._processed_burn_heights.discard(burn_height_key)
230304

231-
# Process veto window start notifications
305+
def _process_veto_window_start_notifications(self, veto_start_proposals):
306+
"""Process veto window start notifications."""
232307
for proposal in veto_start_proposals:
233308
dao = backend.get_dao(proposal.dao_id)
234309
if not dao:
235310
self.logger.warning(f"No DAO found for proposal {proposal.id}")
236311
continue
237312

238-
# Check if a veto notification message already exists
239-
if self._queue_message_exists(
240-
QueueMessageType.get_or_create("discord"), proposal.id, dao.id
241-
):
313+
# Check if a veto window start Discord message already exists
314+
if self._discord_message_exists(proposal.id, dao.id, "veto_window_open"):
242315
self.logger.debug(
243-
f"Veto notification Discord message already exists for proposal {proposal.id}, skipping"
316+
f"Veto window start Discord message already exists for proposal {proposal.id}, skipping"
244317
)
245318
continue
246319

320+
# Create unique identifier for this message
321+
unique_identifier = f"proposal-{proposal.id}-veto_window_open"
322+
247323
# Create veto window start Discord message
248324
message = (
249325
f"⚠️ **VETO WINDOW OPEN: Proposal #{proposal.proposal_id} of {dao.name}**\n\n"
250326
f"**Proposal:**\n{proposal.content[:100]}...\n\n"
251327
f"**Veto Window Details:**\n"
252328
f"• Opens at: Block {proposal.vote_end}\n"
253329
f"• Closes at: Block {proposal.exec_start}\n\n"
254-
f"View proposal details: {config.api.base_url}/proposals/{proposal.id}"
330+
f"View proposal details: {config.api.base_url}/proposals/{proposal.id}\n\n"
331+
f"<!-- {unique_identifier} -->"
255332
)
256333

257334
backend.create_queue_message(
@@ -265,30 +342,33 @@ async def handle_transaction(self, transaction: TransactionWithReceipt) -> None:
265342
f"Created veto window start Discord message for proposal {proposal.id}"
266343
)
267344

268-
# Process veto window end notifications
345+
def _process_veto_window_end_notifications(self, veto_end_proposals):
346+
"""Process veto window end notifications."""
269347
for proposal in veto_end_proposals:
270348
dao = backend.get_dao(proposal.dao_id)
271349
if not dao:
272350
self.logger.warning(f"No DAO found for proposal {proposal.id}")
273351
continue
274352

275-
# Check if a veto end notification message already exists
276-
if self._queue_message_exists(
277-
QueueMessageType.get_or_create("discord"), proposal.id, dao.id
278-
):
353+
# Check if a veto window end Discord message already exists
354+
if self._discord_message_exists(proposal.id, dao.id, "veto_window_closed"):
279355
self.logger.debug(
280-
f"Veto end notification Discord message already exists for proposal {proposal.id}, skipping"
356+
f"Veto window end Discord message already exists for proposal {proposal.id}, skipping"
281357
)
282358
continue
283359

360+
# Create unique identifier for this message
361+
unique_identifier = f"proposal-{proposal.id}-veto_window_closed"
362+
284363
# Create veto window end Discord message
285364
message = (
286365
f"🔒 **VETO WINDOW CLOSED: Proposal #{proposal.proposal_id} of {dao.name}**\n\n"
287366
f"**Proposal:**\n{proposal.content[:100]}...\n\n"
288367
f"**Status:**\n"
289368
f"• Veto window has now closed\n"
290369
f"• Proposal will be executed if it passed voting\n\n"
291-
f"View proposal details: {config.api.base_url}/proposals/{proposal.id}"
370+
f"View proposal details: {config.api.base_url}/proposals/{proposal.id}\n\n"
371+
f"<!-- {unique_identifier} -->"
292372
)
293373

294374
backend.create_queue_message(
@@ -305,7 +385,8 @@ async def handle_transaction(self, transaction: TransactionWithReceipt) -> None:
305385
f"Created veto window end Discord message for proposal {proposal.id}"
306386
)
307387

308-
# Process proposals that are ending
388+
def _process_ending_proposals(self, end_proposals):
389+
"""Process proposals that are ending."""
309390
for proposal in end_proposals:
310391
dao = backend.get_dao(proposal.dao_id)
311392
if not dao:
@@ -341,7 +422,8 @@ async def handle_transaction(self, transaction: TransactionWithReceipt) -> None:
341422
f"Created conclude queue message for proposal {proposal.id}"
342423
)
343424

344-
# Process proposals that are ready for voting
425+
def _process_voting_proposals(self, vote_proposals):
426+
"""Process proposals that are ready for voting."""
345427
for proposal in vote_proposals:
346428
# Get the DAO for this proposal
347429
dao = backend.get_dao(proposal.dao_id)

0 commit comments

Comments
 (0)