Add group tip notifications and restrict /atip to DMs#8
Add group tip notifications and restrict /atip to DMs#8
Conversation
- Use atomic find_one_and_update with $gte guards for tips, withdrawals, and red envelope creation to prevent spending more than available balance - Use $inc instead of $set for all balance mutations to prevent race conditions between concurrent operations - Use Decimal arithmetic for all FIRO calculations to prevent floating point precision errors (stored as float in MongoDB for backward compatibility with existing database) - Fix withdrawal fee logic: atomic deduct full amount, rollback exact amount on RPC failure - Fix catch_envelope to atomically deduct from envelope remains with $gte guard, preventing over-distribution - Fix update_balance withdrawal handler to properly handle locked amount reduction without leaving stale locked values - Store MongoClient as self.client for transaction session access - Replace print() with structured logging throughout - Replace deprecated insert() calls with insert_one() https://claude.ai/code/session_01K7FrkbpYpqJAoJfcx67osm
Replace float storage with BSON Decimal128 via a custom pymongo codec that transparently converts between Python Decimal and Decimal128. This eliminates floating-point drift from accumulated $inc operations. The codec is registered on the database connection so all collections automatically serialize Decimal values as Decimal128 and deserialize them back. The to_decimal() helper handles reading both legacy float values (pre-migration) and Decimal128 values (post-migration). Add migrate_to_decimal128.py script to batch-convert existing float Balance/Locked fields to Decimal128. Supports dry-run mode by default and logs every change. The bot works correctly both before and after migration (graceful degradation). https://claude.ai/code/session_01K7FrkbpYpqJAoJfcx67osm
Red envelopes now expire after 24 hours. Unclaimed funds are automatically returned to the creator's balance with a DM notification. The group message button is cleaned up on expiry. - Add expire_envelopes() background task running every 60 seconds - Add expiry guard in catch_envelope() to reject catches on expired envelopes - Add "expired" field to envelope documents for explicit state tracking - Atomic find_one_and_update prevents double-refund races https://claude.ai/code/session_01K7FrkbpYpqJAoJfcx67osm
Ensures envelopes that expired while the bot was offline are immediately processed before the polling loop begins, closing the 60-second gap before the first scheduled run. https://claude.ai/code/session_01K7FrkbpYpqJAoJfcx67osm
When a tip is made in a public channel, post a notification in that channel so other members can see it. Anonymous tips show "Anonymous" as the sender. DM notifications remain unchanged. https://claude.ai/code/session_01K7FrkbpYpqJAoJfcx67osm
/atip in a group chat defeats its purpose since the command message reveals the sender. Now the bot deletes the command from the group, replies privately telling the user to use DMs, and returns early. The unreachable anonymous branch in the group notification is removed. https://claude.ai/code/session_01K7FrkbpYpqJAoJfcx67osm
|
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Stale remains causes over-crediting in envelope expiry
- Changed ReturnDocument.AFTER to ReturnDocument.BEFORE and now derive the refund amount from the returned document's actual remains value at update time, preventing the race condition with catch_envelope.
Or push these changes by commenting:
@cursor push 700c0a9c0e
Preview (700c0a9c0e)
diff --git a/tipbot.py b/tipbot.py
--- a/tipbot.py
+++ b/tipbot.py
@@ -558,9 +558,8 @@
if remains <= 0:
continue
- store_remains = decimal_to_store(remains)
-
# Atomically zero out the envelope remains and mark as expired
+ # Use BEFORE to get the actual remains at update time (avoids race with catch_envelope)
updated = self.col_envelopes.find_one_and_update(
{
"_id": envelope['_id'],
@@ -574,11 +573,17 @@
"expired_at": int(datetime.datetime.now().timestamp())
}
},
- return_document=ReturnDocument.AFTER
+ return_document=ReturnDocument.BEFORE
)
if not updated:
continue
+ # Use the actual remains from the document at update time
+ actual_remains = to_decimal(updated['remains'])
+ if actual_remains <= 0:
+ continue
+ store_remains = decimal_to_store(actual_remains)
+
# Credit the creator
self.col_users.update_one(
{"_id": envelope['creator_id']},
@@ -586,7 +591,7 @@
)
logger.info("Envelope %s expired: refunded %s FIRO to user %s",
- envelope['_id'], remains, envelope['creator_id'])
+ envelope['_id'], actual_remains, envelope['creator_id'])
# Notify the creator
try:
@@ -594,7 +599,7 @@
envelope['creator_id'],
"<b>Your red envelope expired.</b>\n"
"<b>%s FIRO</b> unclaimed funds have been returned to your balance." %
- "{0:.8f}".format(remains),
+ "{0:.8f}".format(actual_remains),
parse_mode='HTML'
)
except Exception as exc:| self.col_users.update_one( | ||
| {"_id": envelope['creator_id']}, | ||
| {"$inc": {"Balance": store_remains}} | ||
| ) |
There was a problem hiding this comment.
Stale remains causes over-crediting in envelope expiry
High Severity
expire_envelopes runs in the background scheduler thread while catch_envelope runs in the main polling thread. The store_remains used to credit the creator is derived from the initial find cursor result, but between that read and the find_one_and_update, a concurrent catch_envelope call can reduce remains. The find_one_and_update uses ReturnDocument.AFTER (where remains is already zeroed), so the stale store_remains is used for the refund. This over-credits the creator, effectively creating funds out of thin air.



Summary
Test plan
Note
High Risk
High risk because it changes how balances are stored/updated (float→
Decimal128with new atomic$inc/find_one_and_updatepaths) and introduces a one-time database migration that affects core funds accounting.Overview
Monetary precision + safer accounting: Updates the bot to use
Decimal/MongoDBDecimal128for balances and amounts, adds a custom PyMongo codec, and replaces balance math with atomic$inc/find_one_and_updateguards to reduce race conditions in deposits, withdrawals, tips, and envelope claims.Behavior changes: Adds a one-time
migrate_to_decimal128.pyscript (dry-run by default) to convert existingusers,envelopes, andtip_logsmonetary fields; introduces automatic red-envelope expiry with refunds after 24h; posts a group notification when a tip is sent in a group; and restricts/atipto DMs by deleting group attempts and DM-ing the sender.Written by Cursor Bugbot for commit 68a597b. This will update automatically on new commits. Configure here.