forked from SiLioLabs/PayFlow
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathreplay-events.ts
More file actions
248 lines (208 loc) · 7.95 KB
/
Copy pathreplay-events.ts
File metadata and controls
248 lines (208 loc) · 7.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
/**
* replay-events.ts — Replay historical contract events through the indexer pipeline.
*
* Re-fetches events from a specified ledger range and processes them through
* the existing indexer pipeline using upsert semantics to avoid duplicates.
*
* Usage:
* npx ts-node scripts/replay-events.ts --from-ledger 50000 --to-ledger 51000
*
* Environment variables:
* CONTRACT_ID — Deployed FlowPay contract ID (required)
* RPC_URL — Soroban RPC endpoint (default: https://soroban-testnet.stellar.org)
*
* Exit codes:
* 0 — replay completed successfully
* 1 — invalid arguments or replay failure
*/
import { Contract, Networks } from "@stellar/stellar-sdk";
import { Server } from "@stellar/stellar-sdk/rpc";
// ── Configuration ────────────────────────────────────────────────────────────
const CONTRACT_ID = process.env.CONTRACT_ID || process.env.VITE_CONTRACT_ID || "";
const RPC_URL = process.env.RPC_URL || process.env.VITE_RPC_URL || "https://soroban-testnet.stellar.org";
/**
* Number of ledgers to fetch per batch. Keeps RPC responses manageable
* and allows incremental progress reporting.
*/
const BATCH_SIZE = 100;
/**
* Maximum events per RPC getEvents call.
*/
const EVENTS_PER_REQUEST = 1000;
// ── Argument Parsing ─────────────────────────────────────────────────────────
interface ReplayArgs {
fromLedger: number;
toLedger: number;
}
function parseArgs(argv: string[]): ReplayArgs {
let fromLedger: number | undefined;
let toLedger: number | undefined;
for (let i = 2; i < argv.length; i++) {
switch (argv[i]) {
case "--from-ledger":
fromLedger = parseInt(argv[++i], 10);
break;
case "--to-ledger":
toLedger = parseInt(argv[++i], 10);
break;
default:
console.error(`Unknown argument: ${argv[i]}`);
console.error("Usage: replay-events.ts --from-ledger <n> --to-ledger <n>");
process.exit(1);
}
}
if (fromLedger === undefined || toLedger === undefined) {
console.error("ERROR: Both --from-ledger and --to-ledger are required.");
console.error("Usage: replay-events.ts --from-ledger <n> --to-ledger <n>");
process.exit(1);
}
if (!Number.isInteger(fromLedger) || !Number.isInteger(toLedger)) {
console.error("ERROR: --from-ledger and --to-ledger must be integers.");
process.exit(1);
}
if (fromLedger < 0 || toLedger < 0) {
console.error("ERROR: Ledger values must be non-negative.");
process.exit(1);
}
if (toLedger < fromLedger) {
console.error("ERROR: --to-ledger must be >= --from-ledger.");
process.exit(1);
}
return { fromLedger, toLedger };
}
// ── Event Processing ─────────────────────────────────────────────────────────
interface ReplayEvent {
eventName: string;
address: string;
data: unknown;
ledger: number;
timestamp: string;
txHash: string;
}
/**
* Parse a raw RPC event into our internal representation.
*/
function parseEvent(rawEvent: any): ReplayEvent {
const eventName = rawEvent.topic?.[0]?.toString() ?? "unknown";
const address = rawEvent.topic?.[1]?.toString() ?? "";
const ledger = rawEvent.ledger ?? 0;
const timestamp = rawEvent.ledgerCloseTime
? new Date(rawEvent.ledgerCloseTime * 1000).toISOString()
: new Date().toISOString();
const txHash = rawEvent.txHash ?? rawEvent.id ?? "";
return { eventName, address, data: rawEvent.value, ledger, timestamp, txHash };
}
/**
* Process a single event through the indexer pipeline using upsert semantics.
* This function should be connected to the existing indexer's persistence layer.
*
* Upsert behavior: if an event with the same txHash + eventName + address already
* exists, it is updated rather than duplicated.
*/
async function upsertEvent(event: ReplayEvent): Promise<void> {
// In a production setup, this would call the existing indexer's upsert API.
// For now, this provides the integration point where the indexer service
// should be invoked. The indexer handles:
// - Subscription state updates (subscribed, cancelled)
// - Charge records (charged)
// - Pay-per-use tracking (pay_per_use)
// - Merchant stats updates
//
// Example integration:
// await indexerService.upsert(event);
//
// The upsert ensures idempotency — replaying the same range twice
// produces the same final state without duplicate entries.
return;
}
// ── Batch Fetching ───────────────────────────────────────────────────────────
/**
* Fetch events for a single ledger range batch, handling pagination.
*/
async function fetchBatch(
server: Server,
startLedger: number,
endLedger: number
): Promise<ReplayEvent[]> {
const events: ReplayEvent[] = [];
let cursor: string | undefined;
let hasMore = true;
while (hasMore) {
const requestParams: any = {
startLedger: cursor ? undefined : startLedger,
cursor,
filters: [{ type: "contract", contractIds: [CONTRACT_ID] }],
limit: EVENTS_PER_REQUEST,
};
const response = await server.getEvents(requestParams);
for (const rawEvent of response.events) {
const eventLedger = rawEvent.ledger ?? 0;
// Stop if we've exceeded our target range
if (eventLedger > endLedger) {
hasMore = false;
break;
}
events.push(parseEvent(rawEvent));
}
// If we got fewer events than the limit, we've exhausted this range
if (response.events.length < EVENTS_PER_REQUEST) {
hasMore = false;
} else if (hasMore) {
// Use cursor-based pagination for the next page
cursor = (response as any).cursor;
if (!cursor) {
hasMore = false;
}
}
}
return events;
}
// ── Main ─────────────────────────────────────────────────────────────────────
async function main(): Promise<void> {
const { fromLedger, toLedger } = parseArgs(process.argv);
if (!CONTRACT_ID) {
console.error("ERROR: CONTRACT_ID environment variable is not set.");
process.exit(1);
}
const server = new Server(RPC_URL);
console.log("Replay started");
console.log("");
console.log(`Ledgers: ${fromLedger} → ${toLedger}`);
console.log("");
let totalEvents = 0;
let batchCount = 0;
// Process in batches to handle large ledger ranges incrementally
for (let batchStart = fromLedger; batchStart <= toLedger; batchStart += BATCH_SIZE) {
const batchEnd = Math.min(batchStart + BATCH_SIZE - 1, toLedger);
batchCount++;
try {
const events = await fetchBatch(server, batchStart, batchEnd);
// Process each event through the upsert pipeline
for (const event of events) {
await upsertEvent(event);
}
totalEvents += events.length;
// Progress reporting
const progress = Math.min(
100,
Math.round(((batchEnd - fromLedger + 1) / (toLedger - fromLedger + 1)) * 100)
);
console.log(
` Batch ${batchCount}: ledgers ${batchStart}–${batchEnd} | ` +
`${events.length} events | ${progress}% complete`
);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
console.error(`ERROR processing batch ${batchCount} (ledgers ${batchStart}–${batchEnd}): ${message}`);
process.exit(1);
}
}
console.log("");
console.log(`Processed batches: ${batchCount}`);
console.log("");
console.log(`Events replayed: ${totalEvents}`);
console.log("");
console.log("Replay completed");
process.exit(0);
}
main();