Skip to content

fix(#161): live USD-to-payment-asset conversion in verification - #169

Merged
DioChuks merged 2 commits into
BuidlZone-Labs:mainfrom
naninu123:fix/payment-verify-asset-conversion
Jul 29, 2026
Merged

fix(#161): live USD-to-payment-asset conversion in verification#169
DioChuks merged 2 commits into
BuidlZone-Labs:mainfrom
naninu123:fix/payment-verify-asset-conversion

Conversation

@naninu123

@naninu123 naninu123 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

PaymentVerificationService.verify treated expectedAmountUsd as if it were already a whole payment-asset amount (expectedAmountUsd * 1e18). Valid payments at real market rates were rejected (or absurd under/over-pays accepted).

This PR replaces that hardcoded assumption with an asset-agnostic pricing path:

  • Live (or cached) USD-per-asset rate via PRICE_API_URL / CoinGecko default
  • Configurable tolerance (PAYMENT_TOLERANCE_BPS) for quote→confirm drift
  • FALLBACK_USD_PER_ASSET when pricing is temporarily unavailable
  • Env for asset symbol + decimals (PAYMENT_ASSET, PAYMENT_ASSET_DECIMALS) so XLM/tokens work, not just ETH

Changes

  • New src/services/asset-pricing.service.ts — rate fetch, cache, conversion, tolerance
  • Update src/services/paymentVerification.service.ts — value check uses resolved base units + min acceptable
  • Update .env.example — pricing knobs documented
  • New tests/payment-conversion.service.test.ts — conversion + verify integration (14 tests)

Acceptance criteria

  • Payment verification uses current USD-to-payment-asset exchange rate
  • Hardcoded conversion assumptions removed
  • Fallback when pricing data unavailable
  • Configurable tolerance for payment variance
  • Unit tests cover conversion + verification
  • Checkout orchestration path unchanged (still calls Verifier.verify)

Test

npx jest tests/payment-conversion.service.test.ts --runInBand --forceExit
# 14 passed

Closes #161

Summary by CodeRabbit

  • New Features
    • Added configurable USD-based asset pricing for payment verification, including asset decimals and basis-point underpayment tolerance.
    • Introduced live pricing with caching and a configurable USD fallback when live pricing is unavailable.
    • Added accurate USD → on-chain base-unit conversion for supported payment assets.
  • Bug Fixes
    • Updated payment verification to compute expected and minimum acceptable payment amounts using resolved asset pricing (instead of a fixed conversion).
    • Improved pricing-related errors to clearly indicate configuration or fallback needs when pricing cannot be obtained.
  • Tests
    • Added unit tests covering USD-to-base-unit conversion, tolerance calculations, live vs fallback pricing behavior, and verification outcomes.

…cation

Replace hardcoded expectedAmountUsd * 1e18 (1 USD = 1 asset) with
asset-agnostic pricing: live/cached rate, configurable tolerance,
and FALLBACK_USD_PER_ASSET when the price API is unavailable.

Closes BuidlZone-Labs#161
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2d8f02c9-a36d-4c0b-b33b-a9c53139e50a

📥 Commits

Reviewing files that changed from the base of the PR and between 7e878d9 and 60daea4.

📒 Files selected for processing (2)
  • src/services/asset-pricing.service.ts
  • src/services/paymentVerification.service.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/services/paymentVerification.service.ts
  • src/services/asset-pricing.service.ts

📝 Walkthrough

Walkthrough

Payment verification now converts USD amounts using configured payment-asset pricing, applies tolerance to on-chain base-unit thresholds, supports cached live or fallback quotes, and distinguishes pricing configuration failures from unavailable pricing.

Changes

Payment pricing verification

Layer / File(s) Summary
Pricing configuration and quote resolution
.env.example, src/services/asset-pricing.service.ts, tests/payment-conversion.service.ts
Adds environment-based asset pricing settings, live API parsing, quote caching, fallback pricing, and tests for pricing sources and configuration.
Base-unit conversion and tolerance calculation
src/services/asset-pricing.service.ts
Validates USD pricing inputs, converts values using configured asset decimals, calculates expected base-unit amounts, and applies basis-point tolerance thresholds.
Payment verification integration and validation
src/services/paymentVerification.service.ts, tests/payment-conversion.service.ts
Uses resolved payment thresholds during verification, includes pricing metadata, classifies pricing failures, and tests exact, tolerated, rejected, and unavailable-pricing cases.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PaymentVerificationService
  participant asset-pricing.service
  participant PricingAPI
  participant BlockchainProvider

  PaymentVerificationService->>asset-pricing.service: resolve expected base units
  asset-pricing.service->>PricingAPI: fetch USD-per-asset quote
  PricingAPI-->>asset-pricing.service: return live quote
  asset-pricing.service-->>PaymentVerificationService: return expected and minimum amounts
  PaymentVerificationService->>BlockchainProvider: retrieve payment transaction
  BlockchainProvider-->>PaymentVerificationService: return on-chain value
  PaymentVerificationService-->>PaymentVerificationService: compare value with minimum threshold
Loading

Suggested reviewers: diochuks

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: fixing live USD-to-payment-asset conversion in verification.
Linked Issues check ✅ Passed The PR replaces hardcoded conversion with live/cached asset pricing, adds fallback and tolerance, and includes conversion tests as required.
Out of Scope Changes check ✅ Passed The changes stay focused on payment verification, pricing config, and tests, with no clear unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/services/asset-pricing.service.ts (3)

206-240: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Prefer the exact decimal path unconditionally instead of float math with an overflow-only fallback.

whole * factor uses IEEE-754 floats, which aren't exact for most rates/decimals combinations; the tolerance mechanism usually absorbs the resulting rounding noise, but this is fragile for tight tolerances or large decimals. The safe toFixed-based BigInt conversion already exists as a fallback for the !Number.isFinite(raw) case — using it unconditionally removes the float precision risk with little extra cost.

♻️ Proposed simplification
-  const whole = amountUsd / usdPerAsset;
-  if (!Number.isFinite(whole) || whole < 0) {
-    throw new Error(`USD→asset conversion produced non-finite value`);
-  }
-
-  // Split integer + fractional parts in base units
-  const factor = 10 ** decimals;
-  const raw = whole * factor;
-  if (!Number.isFinite(raw)) {
-    // Fallback: BigInt via string with fixed precision
-    const [i, f = ''] = whole.toFixed(decimals).split('.');
-    const frac = (f + '0'.repeat(decimals)).slice(0, decimals);
-    return BigInt(i) * BigInt(10 ** decimals) + BigInt(frac || '0');
-  }
-  return BigInt(Math.round(raw));
+  const whole = amountUsd / usdPerAsset;
+  if (!Number.isFinite(whole) || whole < 0) {
+    throw new Error(`USD→asset conversion produced non-finite value`);
+  }
+  const [i, f = ''] = whole.toFixed(decimals).split('.');
+  const frac = (f + '0'.repeat(decimals)).slice(0, decimals);
+  return BigInt(i) * BigInt(10 ** decimals) + BigInt(frac || '0');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/asset-pricing.service.ts` around lines 206 - 240, Update
usdToAssetBaseUnits to use the existing toFixed-based BigInt conversion
unconditionally after validating whole, removing the factor/raw float-math path
and its overflow fallback. Preserve the current decimal truncation/padding
behavior and return the exact base-unit BigInt result.

126-149: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No retry/backoff on the live price fetch before falling back.

A single transient network error immediately triggers the fallback path (or a hard failure if no fallback is configured), unlike fetchTransaction, which uses withRpcRetry. Consider a short retry/backoff here too, so a single blip doesn't unnecessarily degrade to (possibly stale) fallback pricing or reject a valid payment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/asset-pricing.service.ts` around lines 126 - 149, Update
fetchLiveUsdPerAsset to retry transient fetch or HTTP failures with a short
bounded backoff before propagating the error to the fallback path. Reuse the
existing retry/backoff utility used by fetchTransaction, if available, while
preserving response parsing and immediate handling of invalid price data.

155-198: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Cache stampede on TTL expiry.

When the cache expires, every concurrent getUsdPerAsset call independently calls fetchLiveUsdPerAsset instead of sharing one in-flight request. Under load this multiplies calls to the price API right at cache-expiry moments, increasing the risk of hitting provider rate limits.

♻️ Sketch: coalesce concurrent refreshes
 let cache: CacheEntry | null = null;
+let inFlight: Promise<AssetPriceQuote> | null = null;

 export async function getUsdPerAsset(
   assetOverride?: string,
 ): Promise<AssetPriceQuote> {
   ...
   if (cache && cache.quote.asset === asset && cache.expiresAt > now && cache.quote.usdPerAsset > 0) {
     return { ...cache.quote, source: 'cache' };
   }

+  if (inFlight) return inFlight;
+  inFlight = (async () => {
   try {
     const quote = await fetchLiveUsdPerAsset(asset, cfg.priceApiUrl);
     cache = { quote, expiresAt: now + cfg.cacheTtlMs };
     return quote;
   } catch (err) {
     ...
+  } finally {
+    inFlight = null;
   }
+  })();
+  return inFlight;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/asset-pricing.service.ts` around lines 155 - 198, Update
getUsdPerAsset to coalesce concurrent cache refreshes by maintaining a shared
in-flight fetch promise. After the cache-miss check, reuse that promise when
present; otherwise create and store the live fetch operation, update the cache
or apply the existing fallback behavior once it settles, and clear the in-flight
reference afterward so later expirations can refresh normally.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/services/asset-pricing.service.ts`:
- Around line 98-124: Update parseUsdFromBody to only inspect CoinGecko-shaped
nested prices under the requested asset’s mapped key, using a shared
COINGECKO_ID_MAP extracted from defaultPriceEndpoint. Remove the unrestricted
loop over all object keys, while preserving the direct usdPerAsset, price, and
flat symbol fallbacks.

In `@src/services/paymentVerification.service.ts`:
- Around line 138-163: The pricing-resolution catch in the payment verification
flow must distinguish transient pricing unavailability from permanent
configuration or input failures. Update resolveExpectedPaymentBaseUnits and/or
usdToAssetBaseUnits to expose a distinct error type or classification for
validation/configuration errors, then map those errors to non-retryable
PaymentVerificationError while retaining ServiceUnavailableError only for
genuinely unavailable pricing sources; preserve the existing retry guidance
solely for transient failures.

---

Nitpick comments:
In `@src/services/asset-pricing.service.ts`:
- Around line 206-240: Update usdToAssetBaseUnits to use the existing
toFixed-based BigInt conversion unconditionally after validating whole, removing
the factor/raw float-math path and its overflow fallback. Preserve the current
decimal truncation/padding behavior and return the exact base-unit BigInt
result.
- Around line 126-149: Update fetchLiveUsdPerAsset to retry transient fetch or
HTTP failures with a short bounded backoff before propagating the error to the
fallback path. Reuse the existing retry/backoff utility used by
fetchTransaction, if available, while preserving response parsing and immediate
handling of invalid price data.
- Around line 155-198: Update getUsdPerAsset to coalesce concurrent cache
refreshes by maintaining a shared in-flight fetch promise. After the cache-miss
check, reuse that promise when present; otherwise create and store the live
fetch operation, update the cache or apply the existing fallback behavior once
it settles, and clear the in-flight reference afterward so later expirations can
refresh normally.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: abd62d96-2d0e-4a49-b04b-b45fa0fe86ad

📥 Commits

Reviewing files that changed from the base of the PR and between 65aa5e5 and 7e878d9.

📒 Files selected for processing (4)
  • .env.example
  • src/services/asset-pricing.service.ts
  • src/services/paymentVerification.service.ts
  • tests/payment-conversion.service.test.ts

Comment thread src/services/asset-pricing.service.ts
Comment thread src/services/paymentVerification.service.ts
@naninu123

Copy link
Copy Markdown
Contributor Author

CI note

The red checks on this PR are pre-existing on main, not introduced by this change.

Check Cause Touched by #161?
Build src/controllers/media.controller.ts:40req.user?._id / req.user?.id not on Express User (TS2339) No
Lint Prettier drift on 17 files (e.g. ci.yml, login.controller.ts, media.controller.ts, captcha, zkpassport, …) No — new/edited files under this PR pass prettier --check
Tests 3 suites fail to compile because of the same media.controller TS error; 235 tests pass, including tests/payment-conversion.service.test.ts (14/14) for this fix No

main CI has been failing on the same class of issues (recent runs on main also report failure). Happy to land a separate PR for the media typing / prettier debt if maintainers want that cleaned up independently of #161.

@DioChuks
DioChuks self-requested a review July 27, 2026 16:25
@DioChuks

Copy link
Copy Markdown
Contributor

@naninu123 kindly ensure the right changes & resolve the coderabbit review.

…ath, config vs transient error separation

- parseUsdFromBody: only match CoinGecko entry for the requested asset
  (via shared COINGECKO_ID_MAP), not first object with .usd field.
- usdToAssetBaseUnits: always use toFixed→BigInt path (no float * 10**N),
  removing IEEE-754 precision risk unconditionally.
- PricingConfigError: new distinct error class for permanent config/input
  failures; paymentVerification catch maps it to 422 PaymentVerificationError,
  while transient network/API errors still get 503 ServiceUnavailableError.
- resolveExpectedPaymentBaseUnits: validates expectedAmountUsd + decimals
  upfront before network call (fail-fast, not per-request 503).

Closes BuidlZone-Labs#161
@naninu123

Copy link
Copy Markdown
Contributor Author

@DioChuks All 3 CodeRabbit review items addressed in 60daea4:

  1. parseUsdFromBody asset-key match — unrestricted loop replaced with shared COINGECKO_ID_MAP lookup; only the entry for the requested asset is trusted, not the first .usd found.
  2. Exact decimal path unconditionallyusdToAssetBaseUnits always uses toFixed → BigInt, no whole * factor float multiply. Removes IEEE-754 precision risk.
  3. Config vs transient error separation — new PricingConfigError for permanent input/config failures (mapped to 422 PaymentVerificationError); transient API/network errors still get 503 ServiceUnavailableError. resolveExpectedPaymentBaseUnits also validates expectedAmountUsd + decimals upfront (fail-fast, not per-request 503).

Pre-existing media.controller.ts TS2339 errors unchanged (out of scope). Ready for re-review.

@DioChuks

Copy link
Copy Markdown
Contributor

CI note

The red checks on this PR are pre-existing on main, not introduced by this change.

Check Cause Touched by #161?
Build src/controllers/media.controller.ts:40req.user?._id / req.user?.id not on Express User (TS2339) No
Lint Prettier drift on 17 files (e.g. ci.yml, login.controller.ts, media.controller.ts, captcha, zkpassport, …) No — new/edited files under this PR pass prettier --check
Tests 3 suites fail to compile because of the same media.controller TS error; 235 tests pass, including tests/payment-conversion.service.test.ts (14/14) for this fix No
main CI has been failing on the same class of issues (recent runs on main also report failure). Happy to land a separate PR for the media typing / prettier debt if maintainers want that cleaned up independently of #161.

Alright, thanks for your contribution ❤️
LGTM!

@DioChuks
DioChuks merged commit f204467 into BuidlZone-Labs:main Jul 29, 2026
3 of 5 checks passed
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.

Payment Verification Uses Incorrect USD-to-XLM/Asset Conversion Logic

2 participants