PAR-158
ChestShop sales route through the "ChestShop System" clearing account (2 txns/sale) instead of clean buyer→seller transfers
Finding (prod data)
Every ChestShop sale is recorded as two ledger transactions bridged by a SYSTEM clearing account #4 "ChestShop System", not as one buyer→seller transfer. All 1.26M ChestShop ledger txns are 2-posting, and account #4 carries ~1.26M postings (it's the single busiest account in the DB — the "whale" behind the slow account pages).
Example — one sale "siven01 sold x8 Feather to GovInterior" is a pair:
| txn | account | amount |
|---|---|---|
| 1782332 | GovInterior (buyer) | −3.47 |
| 1782332 | ChestShop System | +3.47 |
| 1782333 | ChestShop System | −3.47 |
| 1782333 | siven01 (seller) | +3.47 |
i.e. buyer → ChestShop System then ChestShop System → seller.
Root cause (code)
ChestShop moves money as two single-sided operations (subtract from buyer, add to seller) via the legacy Vault economy model — com/Acrobot/ChestShop/Economy/Economy.java + Events/Economy/CurrencySubtractEvent etc., bridged by TreasuryListener. The Vault API has no atomic transfer(from,to), so Treasury's VaultEconomyAdapter books each single-sided op against the plugin's SYSTEM account:
treasury .../services/LedgerService.java— explicit:Direct value transfer (no plugin SYSTEM postings)(the clean path, used by rest-api/business) vsVault-style: withdraw … into plugin SYSTEM (burn)/deposit from plugin SYSTEM (mint).AccountServiceImpl.java:210— "Vault bridge SYSTEM accounts are faucets/sinks. credit_limit = -1". One per plugin (Exams,nightcore, … all owned by the sentinel UUID00…01); ChestShop's is just the highest-volume.
The system account nets ~0 over time — it's a pass-through, not money creation.
Impact
- Doubles ChestShop txn/posting volume (each sale = 2 txns).
- Account #4 = 1.26M postings → the perf hotspot behind slow
/accounts/[id], money-flow, etc. (mitigated but not removed by PAR-156/157). - Distorts analytics: the large
SYSTEM↔PERSONALflows in money-flow are mostly shop sales passing through the clearing account. - Non-atomic: withdraw-then-deposit can strand money in the system account if it fails between legs (vs a native transfer which is atomic).
Proposed fix (we own the ChestShop-3 fork)
For player shop sales, have ChestShop-3 call Treasury's native transfer (TreasuryApi.transfer(buyer→seller) / the "Direct value transfer, no SYSTEM postings" path) instead of the Vault subtract+deposit — collapsing each sale into one clean 2-leg buyer→seller transaction, no system account.
Caveats / scope
- Admin/server shops genuinely need the sink — infinite-stock server shops have no real counterparty (see
ServerAccountCorrector), so those keep a faucet/sink leg. The fix must distinguish player shops from server/admin shops. - ChestShop's event model is single-sided (
CurrencySubtractEvent/ hold / return-on-failure), so a transfer-based path is a real integration change in the fork, not a config flip — likely a new bridge route inTreasuryListener+ possibly atransfer-shaped path that still honours ChestShop's hold/refund semantics. - No historical backfill — leave the existing two-hop rows as legacy; document the cutover so analytics treat pre-fix ChestShop volume as doubled.
Acceptance
- A player-to-player ChestShop purchase produces one ledger transaction (
buyer −X,seller +X), no ChestShop System posting. - Admin/server-shop purchases still book against the system/server account (no real seller).
- Failure path leaves no money stranded (atomic).
- Verify post-cutover that ChestShop System (#4) stops accruing new postings for player sales.
Surfaced from an economy-data review; relates to the account-detail perf work (PAR-157) and the /pay attribution work (PAR-145).
Comments
No comments yet.
Activity
- tesks created the issue