Skip to content

feat(treasury): accurate multi-currency balance conversion (#1827)#2379

Open
webguru-hypha wants to merge 7 commits into
mainfrom
feat/1827-currency-conversion-v2
Open

feat(treasury): accurate multi-currency balance conversion (#1827)#2379
webguru-hypha wants to merge 7 commits into
mainfrom
feat/1827-currency-conversion-v2

Conversation

@webguru-hypha

@webguru-hypha webguru-hypha commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add shared currency utilities and Chainlink-backed fiat conversion so treasury balances convert correctly across non-USD reference currencies
  • Let members choose a display currency in profile edit and return space/profile asset totals in that currency
  • Show token reference price on treasury asset cards and add the people.currency migration

Closes #1827

Test plan

  • Run migration 0064_people_currency.sql locally
  • Edit profile currency and confirm selection persists
  • Verify profile wallet and space treasury totals use the selected currency symbol
  • Confirm tokens priced in EUR/GBP/etc. sum correctly against USD-denominated tokens
  • Check token cards show reference price when configured in token settings

Made with Cursor

Summary by CodeRabbit

  • New Features
    • Added currency selection to profile settings, with support for multiple currencies.
    • Asset balances now display converted values using the selected currency.
    • Token cards can show reference prices with their corresponding currency.
  • Bug Fixes
    • Improved fallback pricing for tokens when live prices are unavailable.
    • Added validation and safe fallback to USD for unsupported currencies.
  • Localization
    • Added translated currency labels, placeholders, and token price text across supported languages.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds supported profile currencies, persisted currency preferences, fiat conversion utilities, converted asset balances, reference-price handling, currency-aware treasury formatting, and localized currency/token-price UI.

Changes

Currency conversion

Layer / File(s) Summary
Currency contracts and conversion service
packages/core/src/common/currency.ts, packages/core/src/common/server/*
Defines supported currencies, symbols, UI options, validation helpers, cached CoinGecko conversion rates, and USD conversion utilities.
Profile currency storage and editing
packages/storage-postgres/..., packages/core/src/people/..., packages/epics/src/people/..., packages/i18n/src/messages/*
Persists validated profile currencies, exposes them through domain queries, and adds a localized currency selector to profile editing.
Currency-aware asset API responses
apps/web/src/app/api/v1/people/..., apps/web/src/app/api/v1/spaces/..., packages/core/src/common/server/web3-rpc/get-token-meta.ts
Resolves request currencies, converts balances, carries reference-price currencies, and converts fallback token prices to USD.
Treasury data and display updates
packages/epics/src/treasury/..., packages/i18n/src/messages/*
Propagates currency through asset hooks, formats balances with currency symbols, and displays localized token reference prices.

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

Sequence Diagram(s)

sequenceDiagram
  participant ProfileForm
  participant PeopleAPI
  participant AssetRoute
  participant CoinGecko
  participant Treasury
  ProfileForm->>PeopleAPI: save selected currency
  Treasury->>AssetRoute: request assets with currency
  AssetRoute->>CoinGecko: resolve USD conversion rate
  CoinGecko-->>AssetRoute: return currency rate
  AssetRoute-->>Treasury: return converted assets and balance
  Treasury-->>Treasury: format balance and token price
Loading

Possibly related PRs

Suggested reviewers: plitzenberger, sergey3bv, DSanich, evgenibir

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title follows conventional commits format and accurately summarizes the multi-currency balance conversion work.
Linked Issues check ✅ Passed The PR implements the currency dropdown, persisted currency, converted balances, and token reference pricing required by #1827.
Out of Scope Changes check ✅ Passed The changes stay focused on currency selection, conversion, and treasury pricing, with no clear unrelated additions.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/1827-currency-conversion-v2

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: 9

🤖 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 `@apps/web/src/app/api/v1/people/`[personSlug]/assets/route.ts:
- Around line 257-259: The referencePrice fallback is used as USD even when
referenceCurrency is non-USD. Update the map in the asset pricing logic to
retain referenceCurrency alongside referencePrice, then convert the fallback
price to USD before it contributes to usdEqual and balance, reusing the existing
currency-conversion helper or rate source.

In `@apps/web/src/app/api/v1/spaces/`[spaceSlug]/assets/route.ts:
- Around line 84-86: Update the referencePrice conversion in the token mapping
to check `token.referencePrice != null` before calling `Number`, returning null
otherwise. Keep this consistent with the existing `t.referencePrice != null`
check to prevent undefined values from becoming NaN in `dbTokens` and the API
response.

In `@packages/core/src/common/currency.ts`:
- Around line 32-42: Update CURRENCY_SYMBOLS so every SupportedCurrency maps to
its proper Unicode currency symbol, including EUR, GBP, JPY, CNY, CAD, CHF, AUD,
and HKD, keeping USD as '$'. Ensure getCurrencySymbol consumers receive
consistent symbol values.

In `@packages/core/src/common/server/get-fiat-conversion-rate.ts`:
- Around line 33-40: Add an AbortSignal.timeout() option to the CoinGecko fetch
call in getUsdConversionRate, using an appropriate finite timeout alongside the
existing headers so stalled requests abort and reach the function’s catch
handling.

In `@packages/core/src/people/types.ts`:
- Line 12: Replace the bare string currency types in the Person and
EditPersonInput interfaces with the supported currency type exported from
`@hypha-platform/common`, such as CurrencyCode or SupportedCurrency, and add the
required import.

In `@packages/epics/src/people/components/edit-person-section.tsx`:
- Around line 449-485: Replace the hardcoded Select placeholder in the currency
FormField with tProfile('editForm.placeholders.currency'), adding that key to
all required i18n message files. Update the currency fallback in the Select
value to use imported DEFAULT_CURRENCY instead of the string literal 'USD'.

In `@packages/epics/src/treasury/components/assets/asset-card.tsx`:
- Around line 191-201: Replace the hardcoded “Token Price” text in the asset
card rendering with the localized TreasuryTab.assetCard.tokenPrice translation,
supplying formatted price and currency values. Pass the component’s lang
argument to formatCurrencyValue(referencePrice, lang), and add the tokenPrice
message key with matching placeholders to every locale.

In `@packages/epics/src/treasury/hooks/use-assets.ts`:
- Around line 48-49: Remove the duplicate referenceCurrency property from the
AssetItem type/interface, keeping the existing declaration and retaining
referencePrice unchanged.

In `@packages/i18n/src/messages/es.json`:
- Around line 1478-1479: Add the missing editForm.labels.currency translation to
every locale JSON file (de.json, en.json, fr.json, pt.json, and es.json),
placing it consistently alongside the existing editForm.labels.location entry
and using the appropriate localized “Currency” value.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: aab42109-28f5-4535-a6a2-27622bcb0bfb

📥 Commits

Reviewing files that changed from the base of the PR and between 4bb690a and b157432.

📒 Files selected for processing (25)
  • apps/web/src/app/api/v1/people/[personSlug]/assets/route.ts
  • apps/web/src/app/api/v1/spaces/[spaceSlug]/assets/route.ts
  • packages/core/src/common/currency.ts
  • packages/core/src/common/index.ts
  • packages/core/src/common/server/get-fiat-conversion-rate.ts
  • packages/core/src/common/server/index.ts
  • packages/core/src/common/server/web3-rpc/get-token-meta.ts
  • packages/core/src/governance/types.ts
  • packages/core/src/people/server/queries.ts
  • packages/core/src/people/types.ts
  • packages/core/src/people/validation.ts
  • packages/epics/src/people/components/edit-person-section.tsx
  • packages/epics/src/treasury/components/assets/asset-card.tsx
  • packages/epics/src/treasury/hooks/use-assets-section.ts
  • packages/epics/src/treasury/hooks/use-assets.ts
  • packages/epics/src/treasury/hooks/use-user-assets-section.ts
  • packages/epics/src/treasury/hooks/use-user-assets.ts
  • packages/i18n/src/messages/de.json
  • packages/i18n/src/messages/en.json
  • packages/i18n/src/messages/es.json
  • packages/i18n/src/messages/fr.json
  • packages/i18n/src/messages/pt.json
  • packages/storage-postgres/migrations/0064_people_currency.sql
  • packages/storage-postgres/migrations/meta/_journal.json
  • packages/storage-postgres/src/schema/people.ts

Comment thread apps/web/src/app/api/v1/people/[personSlug]/assets/route.ts Outdated
Comment thread apps/web/src/app/api/v1/spaces/[spaceSlug]/assets/route.ts Outdated
Comment thread packages/core/src/common/currency.ts
Comment thread packages/core/src/common/server/get-fiat-conversion-rate.ts
Comment thread packages/core/src/people/types.ts Outdated
Comment thread packages/epics/src/people/components/edit-person-section.tsx
Comment thread packages/epics/src/treasury/hooks/use-assets.ts Outdated
Comment thread packages/i18n/src/messages/es.json
Rebase onto main and resolve currency conflicts including NZD support.
Space treasury reads currency from the client query param instead of
getDb/findSelf, while profile assets keep the authenticated user connection.

Co-authored-by: Cursor <cursoragent@cursor.com>
@webguru-hypha
webguru-hypha force-pushed the feat/1827-currency-conversion-v2 branch from b157432 to d6f84b1 Compare July 10, 2026 12:01
Alex Prate and others added 2 commits July 10, 2026 14:11
Convert non-USD reference prices to USD before balance totals, align
currency symbols, add fiat fetch timeout, tighten types, and complete
i18n for profile currency and token price labels.

Co-authored-by: Cursor <cursoragent@cursor.com>
Validate DB currency strings with isSupportedCurrency before assigning
to Person.currency so check-types passes after the typed currency field.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

Neon logo Neon Schema Diff summary

Schema diff between the compare branch (preview/pr-2379-feat/1827-currency-conversion-v2) and the base branch (main).

Index: neondb-schema.sql
===================================================================
--- neondb-schema.sql	Branch main
+++ neondb-schema.sql	Branch preview/pr-2379-feat/1827-currency-conversion-v2
@@ -809,9 +809,10 @@
     nickname text,
     sub text DEFAULT auth.user_id(),
     lead_image_url text,
     web3_address text,
-    links jsonb DEFAULT '[]'::jsonb NOT NULL
+    links jsonb DEFAULT '[]'::jsonb NOT NULL,
+    currency text
 );
 
 
 ALTER TABLE public.people OWNER TO neondb_owner;

This comment was last updated at Fri, 10 Jul 2026 12:18:49 GMT

@github-actions

Copy link
Copy Markdown

🔗 Custom preview URL: https://pr-2379.preview-app.hypha.earth

Replace hardcoded USD fallback in the currency select with the shared
DEFAULT_CURRENCY constant to address the remaining CodeRabbit review.

Co-authored-by: Cursor <cursoragent@cursor.com>

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/core/src/common/server/web3-rpc/get-token-meta.ts (2)

140-142: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Avoid any in the catch block. Use unknown and guard with error instanceof Error before reading message.

🤖 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 `@packages/core/src/common/server/web3-rpc/get-token-meta.ts` around lines 140
- 142, Update the catch clause in the token metadata fetch logic to use unknown
instead of any, and only access error.message after an error instanceof Error
guard; provide a safe fallback message for non-Error values while preserving the
existing logging and rethrow behavior.

Source: Learnings


53-55: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Match stable-token metadata by address, not symbol. dbTokens?.find(...) can attach another row’s referencePrice/referenceCurrency (and iconUrl) when symbols collide. Fall back to symbol only for rows without address.

🤖 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 `@packages/core/src/common/server/web3-rpc/get-token-meta.ts` around lines 53 -
55, Update the token lookup in the metadata retrieval function to prefer
matching by normalized token address, preventing symbol collisions from
selecting another row’s metadata. Use symbol matching only as a fallback for
database rows whose address is absent, while preserving the existing behavior
for tokens without an address.
apps/web/src/app/api/v1/spaces/[spaceSlug]/assets/route.ts (1)

114-116: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace the untyped error and balance array. Use unknown for the caught RPC error and Awaited<ReturnType<typeof getTokenBalancesByAddress>> for externalTokens; any here skips type checking in the error and asset-parsing paths.

🤖 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 `@apps/web/src/app/api/v1/spaces/`[spaceSlug]/assets/route.ts around lines 114
- 116, The catch block uses an unsafe any type and the balance array lacks an
explicit inferred RPC result type. In the relevant asset route handler, change
the caught error in the catch block to unknown and type externalTokens as
Awaited<ReturnType<typeof getTokenBalancesByAddress>>; update error and
asset-parsing logic to narrow unknown safely before accessing properties.

Source: Learnings

🤖 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 `@apps/web/src/app/api/v1/people/`[personSlug]/assets/route.ts:
- Around line 412-417: Update the asset-list handler to accept
`PaginationParams` from `core/common`, paginate the sorted assets before
constructing the response, and return a `PaginatedResponse` containing the page
data and pagination metadata. Ensure the pagination is applied after sorting and
that the balance calculation remains consistent with the intended full or
paginated dataset.

In `@apps/web/src/app/api/v1/spaces/`[spaceSlug]/assets/route.ts:
- Around line 300-303: The asset-list endpoint currently returns all assets
without pagination. Update its handler to accept PaginationParams from
core/common, apply the requested page and limit after sorting the aggregated
assets, and return a PaginatedResponse<T> containing the paginated assets plus
total/count metadata while preserving balance and currency fields as
appropriate.

In `@packages/core/src/common/server/get-fiat-conversion-rate.ts`:
- Around line 1-8: Extract the CoinGecko request from the conversion logic into
a new server-only client module named coingecko-client.ts, exporting one
function such as fetchUsdPriceRates. Move the URL construction, timeout, JSON
parsing, and non-2xx error handling there; update get-fiat-conversion-rate.ts to
import it and retain only caching, validation, and fallback behavior.
- Around line 22-57: Update fetchUsdConversionRate to iterate over all rates in
the CoinGecko response, validate each positive finite numeric value, and store
every valid currency rate in conversionCache using getConversionCacheKey.
Preserve returning the requested currency’s rate and retain the existing error
when that requested rate is missing or invalid.

In `@packages/epics/src/treasury/components/assets/asset-card.tsx`:
- Around line 191-202: Separate the USD market-rate currency from the configured
reference-price currency in the asset-card data flow. Update the upstream
token-price computation and component props to provide a distinct
tokenPriceCurrency (USD or the selected display currency), use it for tokenPrice
on the existing market-rate row, and keep referenceCurrency only for formatting
referencePrice in the new row.

---

Outside diff comments:
In `@apps/web/src/app/api/v1/spaces/`[spaceSlug]/assets/route.ts:
- Around line 114-116: The catch block uses an unsafe any type and the balance
array lacks an explicit inferred RPC result type. In the relevant asset route
handler, change the caught error in the catch block to unknown and type
externalTokens as Awaited<ReturnType<typeof getTokenBalancesByAddress>>; update
error and asset-parsing logic to narrow unknown safely before accessing
properties.

In `@packages/core/src/common/server/web3-rpc/get-token-meta.ts`:
- Around line 140-142: Update the catch clause in the token metadata fetch logic
to use unknown instead of any, and only access error.message after an error
instanceof Error guard; provide a safe fallback message for non-Error values
while preserving the existing logging and rethrow behavior.
- Around line 53-55: Update the token lookup in the metadata retrieval function
to prefer matching by normalized token address, preventing symbol collisions
from selecting another row’s metadata. Use symbol matching only as a fallback
for database rows whose address is absent, while preserving the existing
behavior for tokens without an address.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 074f576e-84ef-4247-aec6-8f17b19a0df0

📥 Commits

Reviewing files that changed from the base of the PR and between b157432 and 1c649eb.

📒 Files selected for processing (24)
  • apps/web/src/app/api/v1/people/[personSlug]/assets/route.ts
  • apps/web/src/app/api/v1/spaces/[spaceSlug]/assets/route.ts
  • packages/core/src/common/currency.ts
  • packages/core/src/common/index.ts
  • packages/core/src/common/server/get-fiat-conversion-rate.ts
  • packages/core/src/common/server/index.ts
  • packages/core/src/common/server/web3-rpc/get-token-meta.ts
  • packages/core/src/people/server/queries.ts
  • packages/core/src/people/types.ts
  • packages/core/src/people/validation.ts
  • packages/epics/src/people/components/edit-person-section.tsx
  • packages/epics/src/treasury/components/assets/asset-card.tsx
  • packages/epics/src/treasury/hooks/use-assets-section.ts
  • packages/epics/src/treasury/hooks/use-assets.ts
  • packages/epics/src/treasury/hooks/use-user-assets-section.ts
  • packages/epics/src/treasury/hooks/use-user-assets.ts
  • packages/i18n/src/messages/de.json
  • packages/i18n/src/messages/en.json
  • packages/i18n/src/messages/es.json
  • packages/i18n/src/messages/fr.json
  • packages/i18n/src/messages/pt.json
  • packages/storage-postgres/migrations/0067_people_currency.sql
  • packages/storage-postgres/migrations/meta/_journal.json
  • packages/storage-postgres/src/schema/people.ts

Comment on lines +412 to +417
const usdBalance = sorted.reduce((sum, asset) => sum + asset.usdEqual, 0);

return NextResponse.json({
assets: sorted,
balance: sorted.reduce((sum, asset) => sum + asset.usdEqual, 0),
balance: usdBalance * usdConversionRate,
currency,

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.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Paginate this asset-list response.

This route returns every asset after per-token RPC work. Add PaginationParams input and return PaginatedResponse<T> to bound response size and request cost.

As per coding guidelines, “All list endpoints must use PaginatedResponse<T> and PaginationParams from core/common for pagination.”

🤖 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 `@apps/web/src/app/api/v1/people/`[personSlug]/assets/route.ts around lines 412
- 417, Update the asset-list handler to accept `PaginationParams` from
`core/common`, paginate the sorted assets before constructing the response, and
return a `PaginatedResponse` containing the page data and pagination metadata.
Ensure the pagination is applied after sorting and that the balance calculation
remains consistent with the intended full or paginated dataset.

Source: Coding guidelines

Comment on lines 300 to +303
return NextResponse.json({
assets: sorted,
balance: sorted.reduce((sum, asset) => sum + asset.usdEqual, 0),
balance: usdBalance * usdConversionRate,
currency,

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.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Paginate this asset-list response.

This endpoint aggregates every token and performs per-token chain reads before returning the full list. Accept PaginationParams and return PaginatedResponse<T>.

As per coding guidelines, “All list endpoints must use PaginatedResponse<T> and PaginationParams from core/common for pagination.”

🤖 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 `@apps/web/src/app/api/v1/spaces/`[spaceSlug]/assets/route.ts around lines 300
- 303, The asset-list endpoint currently returns all assets without pagination.
Update its handler to accept PaginationParams from core/common, apply the
requested page and limit after sorting the aggregated assets, and return a
PaginatedResponse<T> containing the paginated assets plus total/count metadata
while preserving balance and currency fields as appropriate.

Source: Coding guidelines

Comment on lines +1 to +8
import 'server-only';
import NodeCache from 'node-cache';
import {
DEFAULT_CURRENCY,
SupportedCurrency,
resolveSupportedCurrency,
SUPPORTED_CURRENCIES,
} from '../currency';

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.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

File naming doesn't follow the <api-name>-client.ts convention for external API clients.

As per coding guidelines for packages/core/src/**/server/**/*.ts: "External API clients should be plain fetch with a thin server-only wrapper; one exported function per API call, throws on non-2xx responses; implement as separate files under packages/core/src/common/server/<api-name>-client.ts following the Bridge or Alchemy client patterns."

Consider extracting the CoinGecko fetch call into packages/core/src/common/server/coingecko-client.ts (exporting a single function that throws on non-2xx), and keeping the caching/conversion logic in this file or a domain utility.

♻️ Proposed structure
// packages/core/src/common/server/coingecko-client.ts
import 'server-only';
import { SUPPORTED_CURRENCIES } from '../currency';

type FiatConversionResponse = { usd?: Record<string, number> };

const getSupportedQuoteCurrencies = (): string =>
  SUPPORTED_CURRENCIES.map((currency) => currency.toLowerCase()).join(',');

export async function fetchUsdPriceRates(): Promise<FiatConversionResponse> {
  const response = await fetch(
    `https://api.coingecko.com/api/v3/simple/price?ids=usd&vs_currencies=${getSupportedQuoteCurrencies()}`,
    {
      headers: { Accept: 'application/json' },
      signal: AbortSignal.timeout(10_000),
    },
  );
  if (!response.ok) {
    throw new Error(`Failed to fetch fiat rates: ${response.statusText}`);
  }
  return (await response.json()) as FiatConversionResponse;
}

Then get-fiat-conversion-rate.ts imports fetchUsdPriceRates and handles caching, validation, and fallback.

🤖 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 `@packages/core/src/common/server/get-fiat-conversion-rate.ts` around lines 1 -
8, Extract the CoinGecko request from the conversion logic into a new
server-only client module named coingecko-client.ts, exporting one function such
as fetchUsdPriceRates. Move the URL construction, timeout, JSON parsing, and
non-2xx error handling there; update get-fiat-conversion-rate.ts to import it
and retain only caching, validation, and fallback behavior.

Source: Coding guidelines

Comment on lines +22 to +57
async function fetchUsdConversionRate(currency: SupportedCurrency) {
if (currency === DEFAULT_CURRENCY) {
return 1;
}

const cacheKey = getConversionCacheKey(currency);
const cachedRate = conversionCache.get<number>(cacheKey);
if (cachedRate !== undefined) {
return cachedRate;
}

const response = await fetch(
`https://api.coingecko.com/api/v3/simple/price?ids=usd&vs_currencies=${getSupportedQuoteCurrencies()}`,
{
headers: {
Accept: 'application/json',
},
signal: AbortSignal.timeout(10_000),
},
);

if (!response.ok) {
throw new Error(`Failed to fetch fiat rates: ${response.statusText}`);
}

const data = (await response.json()) as FiatConversionResponse;
const normalizedCurrency = currency.toLowerCase();
const rate = data.usd?.[normalizedCurrency];

if (typeof rate !== 'number' || !Number.isFinite(rate) || rate <= 0) {
throw new Error(`Invalid conversion rate for currency "${currency}"`);
}

conversionCache.set(cacheKey, rate);
return rate;
}

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.

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Cache all rates from the single CoinGecko response to avoid redundant API calls.

fetchUsdConversionRate requests all supported currencies in one API call (getSupportedQuoteCurrencies() joins all of them), but only caches the one requested. If a different currency is requested next, it triggers another full API call to CoinGecko. Since the response already contains all rates, cache them all in one pass.

♻️ Proposed fix: cache all valid rates from a single response
   const data = (await response.json()) as FiatConversionResponse;
+
+   // Cache all valid rates from this single API response
+   if (data.usd) {
+     for (const supportedCurrency of SUPPORTED_CURRENCIES) {
+       if (supportedCurrency === DEFAULT_CURRENCY) continue;
+       const rate = data.usd[supportedCurrency.toLowerCase()];
+       if (typeof rate === 'number' && Number.isFinite(rate) && rate > 0) {
+         conversionCache.set(getConversionCacheKey(supportedCurrency), rate);
+       }
+     }
+   }
+
   const normalizedCurrency = currency.toLowerCase();
   const rate = data.usd?.[normalizedCurrency];
 
   if (typeof rate !== 'number' || !Number.isFinite(rate) || rate <= 0) {
     throw new Error(`Invalid conversion rate for currency "${currency}"`);
   }
 
-  conversionCache.set(cacheKey, rate);
   return rate;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function fetchUsdConversionRate(currency: SupportedCurrency) {
if (currency === DEFAULT_CURRENCY) {
return 1;
}
const cacheKey = getConversionCacheKey(currency);
const cachedRate = conversionCache.get<number>(cacheKey);
if (cachedRate !== undefined) {
return cachedRate;
}
const response = await fetch(
`https://api.coingecko.com/api/v3/simple/price?ids=usd&vs_currencies=${getSupportedQuoteCurrencies()}`,
{
headers: {
Accept: 'application/json',
},
signal: AbortSignal.timeout(10_000),
},
);
if (!response.ok) {
throw new Error(`Failed to fetch fiat rates: ${response.statusText}`);
}
const data = (await response.json()) as FiatConversionResponse;
const normalizedCurrency = currency.toLowerCase();
const rate = data.usd?.[normalizedCurrency];
if (typeof rate !== 'number' || !Number.isFinite(rate) || rate <= 0) {
throw new Error(`Invalid conversion rate for currency "${currency}"`);
}
conversionCache.set(cacheKey, rate);
return rate;
}
const data = (await response.json()) as FiatConversionResponse;
// Cache all valid rates from this single API response
if (data.usd) {
for (const supportedCurrency of SUPPORTED_CURRENCIES) {
if (supportedCurrency === DEFAULT_CURRENCY) continue;
const rate = data.usd[supportedCurrency.toLowerCase()];
if (typeof rate === 'number' && Number.isFinite(rate) && rate > 0) {
conversionCache.set(getConversionCacheKey(supportedCurrency), rate);
}
}
}
const normalizedCurrency = currency.toLowerCase();
const rate = data.usd?.[normalizedCurrency];
if (typeof rate !== 'number' || !Number.isFinite(rate) || rate <= 0) {
throw new Error(`Invalid conversion rate for currency "${currency}"`);
}
return rate;
🤖 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 `@packages/core/src/common/server/get-fiat-conversion-rate.ts` around lines 22
- 57, Update fetchUsdConversionRate to iterate over all rates in the CoinGecko
response, validate each positive finite numeric value, and store every valid
currency rate in conversionCache using getConversionCacheKey. Preserve returning
the requested currency’s rate and retain the existing error when that requested
rate is missing or invalid.

Comment on lines +191 to +202
{referencePrice !== null &&
referencePrice !== undefined &&
referenceCurrency && (
<div className="w-full flex flex-row gap-1">
<Text className="text-1 text-neutral-11">
{tTreasury('assetCard.tokenPrice', {
price: formatCurrencyValue(referencePrice, lang),
currency: referenceCurrency,
})}
</Text>
</div>
)}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep the USD market rate separate from the reference-price currency.

tokenPrice is used upstream to compute usdEqual, so it is USD. The existing row on Lines 141-146 labels it with referenceCurrency; this new row then shows the raw configured value, producing conflicting values for EUR/GBP-priced tokens. Return/render a distinct tokenPriceCurrency (USD or display currency), while reserving referenceCurrency for referencePrice.

🤖 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 `@packages/epics/src/treasury/components/assets/asset-card.tsx` around lines
191 - 202, Separate the USD market-rate currency from the configured
reference-price currency in the asset-card data flow. Update the upstream
token-price computation and component props to provide a distinct
tokenPriceCurrency (USD or the selected display currency), use it for tokenPrice
on the existing market-rate row, and keep referenceCurrency only for formatting
referencePrice in the new row.

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.

Currency conversion

1 participant