Skip to content

refactor(fx): consume shared backend rate policy - #2607

Draft
0xkkonrad wants to merge 7 commits into
mainfrom
fix/shared-fx-api
Draft

refactor(fx): consume shared backend rate policy#2607
0xkkonrad wants to merge 7 commits into
mainfrom
fix/shared-fx-api

Conversation

@0xkkonrad

@0xkkonrad 0xkkonrad commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replace the UI's duplicate pair-conversion implementation with Peanut API GET /fx/rate.
  • Keep exact same-currency identity local and preserve the visible provider-both-else-reference-both selection policy.
  • Validate direction, atomic selection/provenance, decimal bounds, generation/effective timestamps, and freshness before using a response.
  • Handle 400/404/429/503 deliberately, honor Retry-After, avoid retrying deterministic/rate-limit outcomes, and keep expected outcomes out of Sentry.
  • Refresh the checked-in OpenAPI snapshot/types from the backend contract.

Why the UI should consume the same API

Split needs the backend contract, but moving the UI too is what makes that contract trustworthy: the production UI dogfoods the exact pair endpoint and validation rules that Split depends on. It removes a second implementation of provider/reference fallback, so a future provider, cache, or policy change cannot silently diverge between products.

This is not intended to change the rate a user sees. The API preserves the UI's existing atomic policy: use provider rates only when both legs are covered; otherwise use reference rates for both legs. PLN→EUR therefore remains reference/reference, with no hybrid source.

The actual operational change is that refresh/cache ownership moves to Peanut API. First-party clients call the public endpoint without cookies or bearer credentials; the legacy /api/exchange-rate wrapper remains for compatibility with a bounded five-minute success cache and no cached failures.

Rollout / review focus

  • Deploy and smoke API #1277 first.
  • Confirm same-currency identity works during an API outage and PLN→EUR validates as reference_pair.
  • Then deploy this PR and Split #4. Content facts are in mono #67.

QA

  • Final focused FX/client/hook/compatibility/Sentry regression: 4 suites, 93 tests passed, including success→failed-refetch fail-closed behavior.
  • Final full Jest: 180 suites, 2,399 passed, 3 skipped, 0 failed. The focused suite and typecheck were also rerun after the final hook change, OpenAPI sync, and latest-main merge.
  • Typecheck, changed production-file ESLint, Prettier, and git diff --check pass. Repository-wide ESLint remains advisory/red at its pre-existing 978-problem baseline; the only touched files it names are unchanged any/CommonJS lines in the existing Sentry utility/test.
  • Backend and UI OpenAPI JSON are byte-identical.
  • The branch contains current main; the final merge was conflict-free.
  • CodeRabbit previously hit its review limit, so this remains draft for human review.

Screenshots: N/A (no intended visible UI change).

@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview Aug 5, 2026 3:36pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The change moves display-rate fetching to the backend /fx/rate contract. It adds response validation, public unauthenticated transport, route-specific caching and error handling, client integration, observability rules, rollout documentation, and unrelated OpenAPI contract additions.

FX rate rollout

Layer / File(s) Summary
FX contract and rate validation
src/types/api.openapi.json, src/utils/fx.utils.ts, src/utils/__tests__/fx.utils.test.ts
The API defines FX snapshot and pair-rate responses. fetchDisplayRate validates rates, timestamps, identity pairs, sources, and HTTP errors.
Public API transport
src/utils/api-fetch.ts, src/utils/demo-api.ts, src/utils/__tests__/api-fetch.test.ts, src/utils/__tests__/demo-api.test.ts
Unauthenticated requests omit bearer authentication. The /fx/rate passthrough forwards query parameters and applies a 10-second timeout.
Client and route integration
src/hooks/useExchangeRate.ts, src/app/api/exchange-rate/route.ts, src/proxy.ts, src/__tests__/proxy.test.ts, src/app/**/page.tsx, src/app/m/[slug]/MerchantLandingPage.tsx, src/app/api/exchange-rate/__tests__/route.test.ts
The hook uses the shared backend fetch for all clients. The compatibility route maps expected 400/404 errors. The proxy preserves route-owned caching for the exact exchange-rate path.
Observability and rollout support
src/utils/sentry.utils.ts, src/utils/__tests__/sentry.utils.test.ts, docs/api-types.md, next.config.js
Expected public FX 400/404 responses bypass Sentry reporting. Documentation records rollout sequencing and provider precedence. The Frankfurter CSP entry is removed.

API contract expansion

Layer / File(s) Summary
ENS, consent, and legal-document contracts
src/types/api.openapi.json
The OpenAPI specification adds ENS reverse lookup, optional ENS chainId, consent endpoints, and accepted-document fields for passkey and Rain card requests.
Badge code contract
src/types/api.openapi.json
The grant-badge code enum adds NITA.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant useExchangeRate
  participant fetchDisplayRate
  participant PeanutAPI
  Client->>useExchangeRate: Request display rate
  useExchangeRate->>fetchDisplayRate: Fetch normalized currency pair
  fetchDisplayRate->>PeanutAPI: GET /fx/rate without authentication
  PeanutAPI-->>fetchDisplayRate: Validated rate or 400/404/503 response
  fetchDisplayRate-->>useExchangeRate: Rate or typed error
  useExchangeRate-->>Client: Display rate result
Loading

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: hugo0, abalinda

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: refactoring FX handling to consume the shared backend rate policy.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/shared-fx-api

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.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6346.7 → 6346.27 (-0.43)
Findings: +13 net (+375 new, -362 resolved)

🆕 New findings (375)

  • critical complexity — src/utils/demo-api.ts — CC 108, MI 59.47, SLOC 926
  • high structural-dup — types/api.generated.ts:383 — 83 duplicate lines / 388 tokens with types/api.generated.ts:1096
  • high structural-dup — types/api.generated.ts:393 — 74 duplicate lines / 348 tokens with types/api.generated.ts:1106
  • high structural-dup — types/api.generated.ts:139 — 72 duplicate lines / 269 tokens with types/api.generated.ts:9696
  • high structural-dup — types/api.generated.ts:139 — 70 duplicate lines / 264 tokens with types/api.generated.ts:9782
  • high structural-dup — types/api.generated.ts:139 — 67 duplicate lines / 249 tokens with types/api.generated.ts:9373
  • high structural-dup — types/api.generated.ts:10785 — 61 duplicate lines / 188 tokens with types/api.generated.ts:10887
  • high hotspot — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — 55 commits, +505/-454 lines since 6 months ago
  • high structural-dup — types/api.generated.ts:9318 — 55 duplicate lines / 162 tokens with types/api.generated.ts:9470
  • high structural-dup — types/api.generated.ts:9318 — 55 duplicate lines / 162 tokens with types/api.generated.ts:9641
  • high structural-dup — types/api.generated.ts:9318 — 55 duplicate lines / 165 tokens with types/api.generated.ts:9899
  • medium high-mdd — src/hooks/useExchangeRate.ts:27 — useExchangeRate: MDD 57.2 (uses across many lines from declarations)
  • medium structural-dup — types/api.generated.ts:3379 — 49 duplicate lines / 139 tokens with types/api.generated.ts:6763
  • medium structural-dup — types/api.generated.ts:3379 — 49 duplicate lines / 137 tokens with types/api.generated.ts:7338
  • medium high-mdd — src/utils/sentry.utils.ts:385 — fetchWithSentry: MDD 42.2 (uses across many lines from declarations)
  • medium structural-dup — types/api.generated.ts:704 — 42 duplicate lines / 152 tokens with types/api.generated.ts:2415
  • medium structural-dup — types/api.generated.ts:7415 — 40 duplicate lines / 108 tokens with types/api.generated.ts:7487
  • medium structural-dup — types/api.generated.ts:9333 — 40 duplicate lines / 120 tokens with types/api.generated.ts:9570
  • medium structural-dup — types/api.generated.ts:893 — 39 duplicate lines / 137 tokens with types/api.generated.ts:2801
  • medium structural-dup — types/api.generated.ts:7415 — 39 duplicate lines / 109 tokens with types/api.generated.ts:7561

…and 355 more.

✅ Resolved (362)

  • src/utils/demo-api.ts — CC 107, MI 59.52, SLOC 917
  • types/api.generated.ts:346 — 83 duplicate lines / 384 tokens with types/api.generated.ts:1059
  • types/api.generated.ts:356 — 74 duplicate lines / 344 tokens with types/api.generated.ts:1069
  • types/api.generated.ts:139 — 72 duplicate lines / 269 tokens with types/api.generated.ts:9519
  • types/api.generated.ts:139 — 70 duplicate lines / 264 tokens with types/api.generated.ts:9605
  • types/api.generated.ts:139 — 67 duplicate lines / 249 tokens with types/api.generated.ts:9196
  • types/api.generated.ts:9141 — 55 duplicate lines / 162 tokens with types/api.generated.ts:9293
  • types/api.generated.ts:9141 — 55 duplicate lines / 162 tokens with types/api.generated.ts:9464
  • types/api.generated.ts:9141 — 55 duplicate lines / 165 tokens with types/api.generated.ts:9722
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — 54 commits, +504/-453 lines since 6 months ago
  • src/hooks/useExchangeRate.ts:28 — useExchangeRate: MDD 49.1 (uses across many lines from declarations)
  • types/api.generated.ts:3207 — 49 duplicate lines / 139 tokens with types/api.generated.ts:6586
  • types/api.generated.ts:3207 — 49 duplicate lines / 137 tokens with types/api.generated.ts:7161
  • src/utils/sentry.utils.ts:382 — fetchWithSentry: MDD 42.2 (uses across many lines from declarations)
  • types/api.generated.ts:667 — 42 duplicate lines / 152 tokens with types/api.generated.ts:2243
  • types/api.generated.ts:7238 — 40 duplicate lines / 108 tokens with types/api.generated.ts:7310
  • types/api.generated.ts:9156 — 40 duplicate lines / 120 tokens with types/api.generated.ts:9393
  • types/api.generated.ts:856 — 39 duplicate lines / 137 tokens with types/api.generated.ts:2629
  • types/api.generated.ts:7238 — 39 duplicate lines / 109 tokens with types/api.generated.ts:7384
  • types/api.generated.ts:8783 — 39 duplicate lines / 116 tokens with types/api.generated.ts:8933

…and 342 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/app/api/exchange-rate/route.ts 8.0 10.0 +1.9
src/utils/fx.utils.ts 6.0 2.2 -3.9

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2402 ran, 0 failed, 0 skipped, 41.3s

📊 Coverage (unit)

metric %
statements 63.1%
branches 47.3%
functions 53.0%
lines 63.6%
⏱ 10 slowest test cases
time test
3.8s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.2s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.4s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.4s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.2s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › keeps stickers off the username pill (final pass respects the keep-out)
0.2s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Manteca PIX form ready shows merchant card + amount input + pay button
0.2s src/utils/__tests__/demo-balance.test.ts › keeps a spent-down balance across a cold start within the TTL
0.2s src/utils/__tests__/url.utils.test.ts › uses the public BASE_URL in Capacitor, not the localhost WebView origin
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@0xkkonrad

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 1

🧹 Nitpick comments (2)
src/utils/__tests__/fx.utils.test.ts (1)

74-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The implausibly small rate fixture does not isolate MIN_DISPLAY_RATE.

'0.0000000000000000001' has 19 decimal places. PLAIN_DECIMAL allows at most 18, so the payload is rejected by the precision rule before the numeric bound is reached. The lower bound stays untested.

Use a value with 18 decimals that is still below 1e-18.

♻️ Proposed fixture change
-        ['implausibly small rate', { ...validResponse, rate: '0.0000000000000000001' }],
+        // 18 decimals, so PLAIN_DECIMAL passes and MIN_DISPLAY_RATE is the rule under test.
+        ['implausibly small rate', { ...validResponse, rate: '0.000000000000000000' }],

Note: '0.000000000000000000' parses to 0, which is below MIN_DISPLAY_RATE. Pick any 18-decimal string under 1e-18 that suits the intent.

🤖 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/utils/__tests__/fx.utils.test.ts` at line 74, Update the “implausibly
small rate” fixture in the rate validation tests to use a numeric string with
exactly 18 decimal places that remains below MIN_DISPLAY_RATE, so it passes
PLAIN_DECIMAL precision validation and specifically exercises the lower-bound
check.
src/utils/fx.utils.ts (1)

76-80: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a timeout to the FX request.

fetchDisplayRate runs on the Next.js server for /api/exchange-rate and in native clients. apiFetch supports timeoutMs, but this call omits it. If api.peanut.me stalls, the route handler holds the request until the platform limit.

src/utils/demo-api.ts already uses a 10-second timeout for the native FX passthrough. Use the same bound here.

♻️ Proposed fix to bound the outbound call
     const response = await apiFetch(`/fx/rate?${query.toString()}`, {
         method: 'GET',
         includeAuth: false,
         credentials: 'omit',
+        timeoutMs: 10_000,
     })
🤖 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/utils/fx.utils.ts` around lines 76 - 80, Update the apiFetch call in
fetchDisplayRate to pass the timeoutMs option using the existing 10-second bound
from src/utils/demo-api.ts, while preserving the current GET method,
authentication, and credential settings.
🤖 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/proxy.ts`:
- Around line 57-60: Update the exchange-rate route handler in
src/app/api/exchange-rate/route.ts so every 400, 404, and 500 error response
explicitly sets Cache-Control to no-store, while preserving the existing
s-maxage/stale-while-revalidate policy on successful responses.

---

Nitpick comments:
In `@src/utils/__tests__/fx.utils.test.ts`:
- Line 74: Update the “implausibly small rate” fixture in the rate validation
tests to use a numeric string with exactly 18 decimal places that remains below
MIN_DISPLAY_RATE, so it passes PLAIN_DECIMAL precision validation and
specifically exercises the lower-bound check.

In `@src/utils/fx.utils.ts`:
- Around line 76-80: Update the apiFetch call in fetchDisplayRate to pass the
timeoutMs option using the existing 10-second bound from src/utils/demo-api.ts,
while preserving the current GET method, authentication, and credential
settings.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b23aed96-a4b0-4245-93bd-4d9dcb5a89a6

📥 Commits

Reviewing files that changed from the base of the PR and between 8c5cd1e and 5f95fad.

⛔ Files ignored due to path filters (1)
  • src/types/api.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (18)
  • docs/api-types.md
  • next.config.js
  • src/__tests__/proxy.test.ts
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
  • src/app/api/exchange-rate/__tests__/route.test.ts
  • src/app/api/exchange-rate/route.ts
  • src/app/m/[slug]/MerchantLandingPage.tsx
  • src/hooks/useExchangeRate.ts
  • src/proxy.ts
  • src/types/api.openapi.json
  • src/utils/__tests__/api-fetch.test.ts
  • src/utils/__tests__/demo-api.test.ts
  • src/utils/__tests__/fx.utils.test.ts
  • src/utils/__tests__/sentry.utils.test.ts
  • src/utils/api-fetch.ts
  • src/utils/demo-api.ts
  • src/utils/fx.utils.ts
  • src/utils/sentry.utils.ts
💤 Files with no reviewable changes (1)
  • next.config.js

Comment thread src/proxy.ts
@0xkkonrad

Copy link
Copy Markdown
Contributor Author

Addressed all CodeRabbit findings in 5ade3d4: error responses are explicitly no-store, FX calls have a 10s timeout, and the lower-bound fixture now passes decimal-shape validation before exercising the numeric bound. Focused 47 tests, typecheck, changed ESLint, and formatting pass.

@0xkkonrad

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

1 participant