Skip to content

Path Payment Preview, Slippage Protection & Contributor Asset Intelligence #688

Description

@grantfox-oss

Overview

CrowdPay's path payment contribution flow submits a pathPaymentStrictReceive to Stellar but gives contributors zero information beforehand — they do not know the exchange rate, the number of hops, the estimated fee, or what slippage tolerance has been applied. A contributor sending XLM to a USDC campaign is flying blind. If the DEX liquidity shifts between the time they click "Contribute" and the time the transaction is submitted, they may receive a failed transaction with PATH_PAYMENT_OVER_SENDMAX and no explanation. This issue rebuilds the entire contribution flow around a preview-first architecture: the contributor sees the exact conversion details before signing, slippage tolerance is configurable, the best path is selected from multiple Horizon route options, and failed contributions are diagnosed and presented in plain English rather than a raw Stellar result code.

What needs to be built

backend/src/services/pathPaymentPreview.js — Preview engine

  • POST /api/campaigns/:id/contribution/preview — accepts { sourceAsset, sourceAmount?, destinationAmount?, slippageBps: number (default 100) }:

    • Calls Horizon GET /paths/strict-receive (if destinationAmount is specified) or GET /paths/strict-send (if sourceAmount is specified) with the campaign's target asset as the destination
    • Fetches all returned paths (up to 5); for each path computes:
      • Effective exchange rate: destinationAmount / sourceAmount
      • Total hop count
      • Estimated network fee (from Horizon fee_stats P90)
      • sendMax value applying the contributor's slippage tolerance: sourceAmount * (1 + slippageBps / 10000)
      • Liquidity confidence score: ratio of path's destination_amount to the requested amount — paths that can only partially fill get a lower score
    • Ranks paths by: 1) lowest sendMax, 2) fewest hops, 3) highest liquidity confidence score
    • Returns: { paths: [{ rank, hops: [{ asset, issuer }], effectiveRate, sendMax, estimatedFee, liquidityScore }], recommendedPath: number, expiresAt: timestamp (30 seconds) }
    • Stores the preview result in Redis with a 30-second TTL keyed by (campaignId, sourceAsset, destinationAmount, slippageBps) — used to validate the subsequent contribution submission
      backend/src/services/contribution.js — Submission with preview validation
  • POST /api/campaigns/:id/contributions — now requires { previewToken, selectedPathIndex, sourceAsset, sourceAmount }:

    • Fetches the preview from Redis by previewToken; if not found or expired, returns 422 PREVIEW_EXPIRED — contributor must re-preview before submitting
    • Validates selectedPathIndex is valid within the stored preview
    • Builds the pathPaymentStrictReceive using the preview's sendMax and exact intermediate path assets — not a fresh Horizon lookup (avoids race conditions)
    • If submission returns PATH_PAYMENT_OVER_SENDMAX: automatically retries once with sendMax * 1.05 (5% buffer); if still fails, returns SLIPPAGE_EXCEEDED with the actual rate at time of failure
    • On success: records the actual sourceAmount, destinationAmount, all intermediate hops, and the effective rate in the contributions table
      backend/src/services/contributionDiagnostics.js — Failure diagnosis
  • GET /api/contributions/:id/diagnosis — given a failed contribution ID:

    • Fetches the Stellar transaction result from Horizon by tx hash
    • Maps every possible pathPaymentStrictReceive result code to a plain-English explanation with a suggested action:
      • PATH_PAYMENT_OVER_SENDMAX → "The conversion rate changed while your transaction was being processed. Increase your slippage tolerance to 1.5% or try again."
      • PATH_PAYMENT_NO_ISSUER → "One of the intermediate assets in the payment path no longer has an active issuer. Choose a different source asset."
      • PATH_PAYMENT_TOO_FEW_OFFERS → "There is not enough liquidity in this trading pair right now. Try a smaller amount or wait for more market activity."
    • Returns: { resultCode, explanation, suggestedAction, retryable: boolean, alternativePathsAvailable: boolean }
      Frontend — Contribution modal rebuild
  • Step 1 — Asset & Amount:

    • Source asset selector: XLM native, or any asset (code + issuer input); contributor types either a source amount ("I want to send") or destination amount ("I want the campaign to receive")
    • Slippage tolerance selector: Low (0.5%), Medium (1%), High (2%), Custom (input in bps)
    • "Preview Conversion" button — calls POST /api/campaigns/:id/contribution/preview
  • Step 2 — Route Preview (before any signing):

    • Top recommended path displayed prominently:
      • Hop chain: XLM → USDC (Centre) rendered as asset bubbles connected by arrows
      • Effective rate: 1 XLM = 0.104 USDC
      • Max you'll send: 96.32 XLM (with slippage applied)
      • Campaign receives: 10.00 USDC (exact)
      • Network fee: 0.00001 XLM
      • Preview expires in: live countdown timer (30 seconds)
    • "Show all routes" expander — displays all returned paths with their rank, hop count, and rate for comparison
    • "Change route" — lets the contributor select a non-recommended path
    • "Confirm & Contribute" button — triggers signing and submission
  • Step 3 — Result:

    • Success: animated confetti, tx hash with Stellar Expert link, exact amounts received by campaign
    • Failure: plain-English error from the diagnosis endpoint, "Retry with higher slippage" button (pre-fills Step 1 with 1.5% tolerance), "Try a different asset" button
      Database migrations
  • Alter contributions: add source_asset, source_amount (numeric), path_hops (jsonb array of { asset, issuer }), effective_rate (numeric), slippage_bps (int), send_max (numeric), retry_count (int default 0), diagnosis (jsonb nullable for failed contributions)

Acceptance criteria

  • Preview correctly returns ranked paths for an XLM → USDC conversion — the recommended path has the lowest sendMax among all returned paths
  • A contribution submitted with an expired previewToken (> 30 seconds old) returns 422 PREVIEW_EXPIRED — no Stellar transaction is submitted
  • Automatic retry with sendMax * 1.05 on PATH_PAYMENT_OVER_SENDMAX succeeds when the rate has shifted by less than 5% between preview and submission
  • Diagnosis endpoint returns a non-empty explanation and suggestedAction for every pathPaymentStrictReceive result code documented in the Stellar protocol
  • path_hops in the contributions table exactly matches the intermediate assets used in the confirmed on-chain transaction — confirmed by decoding the Stellar transaction result
  • Preview countdown timer in the frontend reaches zero and disables the "Confirm & Contribute" button — user must re-preview before proceeding

Activity

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

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