Skip to content

Per-Asset Goal Tracking, Real-Time USD Equivalents & Contributor Asset Preference #676

Description

@grantfox-oss

Overview

CrowdPay currently supports campaigns with a single target asset. A creator running a campaign in Nigeria might want to accept NGN-backed stablecoins, USDC, and XLM simultaneously — with a single funding goal expressed in USD equivalent. This requires: storing per-asset sub-totals, computing USD equivalents using live FX rates, letting contributors choose which asset they contribute in, and correctly handling path payments that convert the contributor's asset to any of the campaign's accepted assets.

What needs to be built

backend/src/services/multiCurrencyGoal.js

  • Campaign creation changes — POST /api/campaigns:

    • Add goalMode: 'single_asset' | 'multi_asset'
    • For multi_asset: acceptedAssets: [{ code, issuer, weight: number }] — weight determines how much of the goal can be filled by each asset (weights must sum to 100)
    • goalAmountUsd: the campaign goal expressed in USD equivalent
  • GoalTracker — on each contribution event from the Horizon stream:

    • Fetch live FX rate for the received asset; compute contribution_usd_equivalent = amount * rate
    • Update campaigns.total_raised_usd and campaign_asset_totals
    • If Fluxa FX rate is unavailable: use Horizon's order_book mid-price as fallback; log a warn event
  • POST /api/campaigns/:id/contribute — multi-asset contribution:

    • Accepts { asset, amount } — contributor chooses which accepted asset to send
    • Validates asset is in the campaign's acceptedAssets list
    • Builds a PathPaymentStrictReceive so the campaign wallet receives exactly in the chosen asset
    • Returns { estimatedRate, estimatedFee, pathHops } before submission for the contributor to review
      Frontend
  • Campaign creation form — goalMode toggle; for multi-asset: "Add accepted asset" button to build the asset list with per-asset weight sliders (constrained to sum to 100)

  • Campaign page — goal progress bar shows a single USD-equivalent progress; below the bar, a per-asset breakdown

  • Contribution modal — asset selector dropdown (only accepted assets); shows estimated USD equivalent as they type the amount; calls the Payment Simulator to show the conversion path

  • Currency toggle on the campaign page: switch the progress bar between USD equivalent and each accepted asset's native amount
    Database migrations

  • campaign_accepted_assets: id, campaign_id, asset_code, asset_issuer, weight, created_at

  • campaign_asset_totals: campaign_id, asset_code, asset_issuer, total_raised (numeric), total_raised_usd (numeric), last_updated

  • Alter campaigns: add goal_mode, goal_amount_usd, total_raised_usd

Acceptance criteria

  • A multi-asset campaign correctly accepts contributions in both USDC and XLM — both reflected in the per-asset breakdown and the USD-equivalent total
  • USD equivalent is computed correctly using the live FX rate — within 1% of the Horizon order book mid-price at the time of contribution
  • Fallback to Horizon order book mid-price when Fluxa FX rates are unavailable — no 500 error; a warn log entry is written
  • Contribution in an asset not in acceptedAssets returns 400 ASSET_NOT_ACCEPTED
  • Weight validation: acceptedAssets with weights not summing to 100 returns 400 INVALID_ASSET_WEIGHTS
  • Goal progress bar updates in real time via WebSocket within 1 second of a contribution being indexed

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions