Skip to content

Use Decimal128 for exact monetary storage in MongoDB#6

Open
reubenyap wants to merge 2 commits intomasterfrom
claude/decimal128-monetary-storage-BbvlM
Open

Use Decimal128 for exact monetary storage in MongoDB#6
reubenyap wants to merge 2 commits intomasterfrom
claude/decimal128-monetary-storage-BbvlM

Conversation

@reubenyap
Copy link
Member

@reubenyap reubenyap commented Mar 9, 2026

Summary

  • Replace integer-based (groth) balance storage with MongoDB Decimal128 for exact decimal arithmetic
  • Add decimal_to_store() and store_to_decimal() helper functions
  • Eliminates floating-point rounding errors in balance calculations
  • Includes race condition fixes as prerequisite

Test plan

  • Verify balances are stored as Decimal128 in MongoDB
  • Verify tip amounts convert correctly between display and storage formats
  • Verify withdrawal fee calculations maintain precision
  • Verify deposit detection correctly converts and stores amounts

Note

High Risk
Touches balance/withdrawal logic and database monetary storage, so mistakes could mis-credit users or corrupt balances. Also introduces a one-time data migration that must be applied carefully in production.

Overview
Moves monetary storage from floats to exact decimals. tipbot.py now uses Python Decimal with a MongoDB Decimal128 codec, quantizes values to 8 decimal places, and stores fees/constants as decimals.

Hardens money-moving operations. Deposits, tips, withdrawals, and red envelope flows are rewritten to use atomic $inc/find_one_and_update with $gte guards and better idempotency (skip already-processed txIds) to reduce race conditions and double-crediting; withdrawals add rollback on wallet send failure.

Adds a one-time migration utility. New migrate_to_decimal128.py converts existing users (Balance, Locked), envelopes (amount, remains), and tip_logs (amount) fields to Decimal128, defaulting to dry-run unless --apply is provided.

Written by Cursor Bugbot for commit f9cd444. This will update automatically on new commits. Configure here.

claude added 2 commits March 9, 2026 05:47
- 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
@reubenyap reubenyap changed the title Claude/decimal128 monetary storage bbvl m Use Decimal128 for exact monetary storage in MongoDB Mar 9, 2026
@reubenyap
Copy link
Member Author

bugbot run

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

response = self.wallet_api.spendspark(
address,
float(amount),
comment
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Withdrawal fee no longer deducted from sent amount

High Severity

The old code sent amount - AV_FEE to spendspark and locked amount - AV_FEE, effectively collecting the 0.002 FIRO fixed withdrawal fee documented in CLAUDE.md. The new code sends float(amount) (the full amount) and locks the full amount, so the bot's withdrawal fee is no longer charged. The docstring incorrectly claims "The network receives amount - AV_FEE" but the code sends the unmodified amount. The PR test plan says "Verify withdrawal fee calculations maintain precision," implying the fee was meant to be preserved, not removed.

Additional Locations (1)

Fix in Cursor Fix in Web


def decimal_to_store(value):
"""Quantize a value to 8 decimal places for MongoDB storage as Decimal128."""
return to_decimal(value)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant decimal_to_store wrapper duplicates to_decimal exactly

Low Severity

decimal_to_store is a pass-through that just calls return to_decimal(value) with no additional logic. Its docstring says "for MongoDB storage as Decimal128" but it returns a Python Decimal, not Decimal128 (the codec handles conversion transparently). The PR description also mentions a companion store_to_decimal() helper that was never defined. This wrapper adds indirection without value.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants