Skip to content

fix(campaigns): stop silent raisedAmount overwrite in getContractBalance (closes #1)#41

Open
GWorld57 wants to merge 1 commit into
OrbitChainLabs:mainfrom
GWorld57:fix/issue-1-getContractBalance-bug
Open

fix(campaigns): stop silent raisedAmount overwrite in getContractBalance (closes #1)#41
GWorld57 wants to merge 1 commit into
OrbitChainLabs:mainfrom
GWorld57:fix/issue-1-getContractBalance-bug

Conversation

@GWorld57

Copy link
Copy Markdown

Resolves OrbitChain-API critical issue #1: CampaignsService.getContractBalance was aggregating native XLM balances and issued-asset balances into a single numeric sum and silently overwriting Campaign.raisedAmount whenever a discrepancy was detected — with no audit log, no admin gate, and no notification.

Why this is critical

  • Frontend progress bars read Campaign.raisedAmount → progress is wrong for any multi-asset campaign.
  • Milestone unlock logic compares raised vs goal using the corrupt aggregate → milestones may unlock prematurely or never.
  • Platform analytics aggregate over raisedAmount → dashboards report ghost totals.

What this PR changes

  1. getContractBalance is now read-only with respect to Campaign.raisedAmount. The silent prisma.campaign.update is gone.
  2. The response exposes a deterministic per-asset breakdown (perAsset[]). It then computes netAvailableByAssetTotal by summing only native (XLM) entries so the figure is safe to compare against the XLM-denominated Campaign.raisedAmount. Non-XLM balances appear in perAsset but are never folded into the canonical total (the fix to the mixed-denomination sum).
  3. APPROVED/RELEASED FundRelease.amount values are aggregated and added back to the XLM net (released funds have already left the contract account).
  4. New admin-only endpoint POST /admin/campaigns/:id/reconcile-balance accepts { force: boolean, reason?: string }. It only mutates Campaign.raisedAmount when force === true && discrepancyDetected, and always writes an AuditLog row with action = ADMIN_ACTION, resourceType = campaign_balance_reconciliation, details.kind = BALANCE_RECONCILED, details.mode = WRITE|DRY_RUN, plus adminEmail, contractId, stored/new figures, and writtenAmount.
  5. AdminModule imports CampaignsModule so the new service wiring resolves cleanly.

Caveats documented in code

  • The FundRelease Prisma model lacks assetCode / assetIssuer columns. Per-asset release netting is conservatively treated as a single XLM bucket today. A follow-up migration adding those columns would unlock true per-asset netting without changing this endpoint contract.

Tests

  • 14 unit tests now live alongside the source in src/campaigns/campaigns.service.spec.ts:
    • NotFound when campaign missing
    • BadRequest when contractId is null
    • XLM-only no-discrepancy
    • Multi-asset no-mixed-denom (50 XLM + 1000 USDC → canonical total 50)
    • Post-release netted (10 XLM on-chain + 5 XLM released = 15 stored)
    • Drained-account + prior release (0 on-chain + 5 released → still flags discrepancy)
    • Silent-write refusal: when figures diverge, prisma.campaign.update is never called
  • Validation: npx tsc --noEmit is clean. npx jest src/campaigns/campaigns.service.spec.ts → 14 / 14 passing.

Closes #1

…nce (closes OrbitChainLabs#1)

- getContractBalance is now READ-ONLY; no longer mutates Campaign.raisedAmount
- Per-asset breakdown exposed via `perAsset`; multi-asset balances are never
  folded into a single mixed-denomination aggregate
- Canonical `netAvailableByAssetTotal` sums only native (XLM) entries so it
  is safe to compare against the XLM-denominated `Campaign.raisedAmount`
- APPROVED/RELEASED FundRelease amounts are netted into the XLM bucket via
  a new `sumApprovedReleasedAmount` helper (single XLM bucket today; per-asset
  netting requires a future migration to add assetCode/assetIssuer to
  FundRelease)
- New admin-only endpoint POST /admin/campaigns/:id/reconcile-balance with
  body { force: boolean, reason?: string }; only mutates raisedAmount when
  force === true AND discrepancyDetected; always writes an AuditLog row
  with kind=BALANCE_RECONCILED, mode=WRITE|DRY_RUN, adminEmail, contractId,
  and the projections
- Module wiring: AdminModule now imports CampaignsModule
- 14 unit tests cover NotFound, BadRequest, XLM-only no-discrepancy,
  multi-asset no-mixed-denom, post-release netted, drained-account, and
  silent-write refusal
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant