|
1 | | -// One-shot fetcher that populates `test/fixtures/horizon-corpus/` with |
2 | | -// real envelope/operation/ledger XDR captured from horizon mainnet. |
| 1 | +// One-shot fetcher that populates `test/fixtures/horizon-corpus/` with real |
| 2 | +// envelope/result/fee-meta/ledger XDR captured from Horizon mainnet. |
3 | 3 | // |
4 | 4 | // Usage: |
5 | 5 | // pnpm tsx scripts/refresh-horizon-corpus.ts |
| 6 | +// HORIZON=... COUNT=100 pnpm tsx scripts/refresh-horizon-corpus.ts |
6 | 7 | // |
7 | 8 | // Refresh quarterly or when a new Stellar protocol version ships. The |
8 | 9 | // resulting JSON files are checked into the repo and consumed by |
9 | | -// `test/unit/base/xdr/corpus_round_trip.test.ts` — that test asserts every |
| 10 | +// `test/unit/xdr/corpus_round_trip.test.ts` — that test asserts every |
10 | 11 | // fixture round-trips losslessly through both the new and legacy XDR |
11 | 12 | // runtimes, so any encoding regression that affects shapes the network |
12 | 13 | // actually produces will fail loudly. |
13 | 14 | // |
14 | | -// We pull from a few endpoints to hit a broad surface: |
15 | | -// - /transactions → TransactionEnvelope (covers ops, memos, signers) |
16 | | -// - /ledgers/{seq} → LedgerHeader (covers nested ledger entry types) |
| 15 | +// We pull from two endpoints to hit a broad surface: |
| 16 | +// - /transactions → TransactionEnvelope (covers ops, memos, |
| 17 | +// signers), TransactionResult, and OperationMeta |
| 18 | +// via `fee_meta_xdr` |
| 19 | +// - /ledgers → LedgerHeader (covers nested ledger entry types) |
| 20 | +// |
| 21 | +// Note on transaction meta: SDF removed `result_meta_xdr` from the hosted |
| 22 | +// Horizon API and points callers at Stellar RPC instead, so there is no |
| 23 | +// `TransactionMeta` here. That coverage lives in the RPC corpus — see |
| 24 | +// `scripts/refresh-rpc-corpus.ts`. Don't try to reintroduce it from Horizon. |
17 | 25 | // |
18 | 26 | // We don't decode the bytes here — just snapshot the on-wire form. The |
19 | 27 | // test does the decode/re-encode validation. |
|
23 | 31 | // either sample a larger range, or hand-pick a mix of `envelopeTypeTx`, |
24 | 32 | // `envelopeTypeTxV0`, and `envelopeTypeTxFeeBump` records by querying |
25 | 33 | // historical ranges that included Soroban contract calls, classic |
26 | | -// payments, and fee-bumped batches. |
27 | | -import { writeFileSync, mkdirSync } from "node:fs"; |
| 34 | +// payments, and fee-bumped batches. The run prints the spread it got. |
28 | 35 | import { resolve } from "node:path"; |
29 | 36 |
|
| 37 | +import { |
| 38 | + readCount, |
| 39 | + requireNumber, |
| 40 | + requireString, |
| 41 | + reportCount, |
| 42 | + reportEnvelopeSpread, |
| 43 | + writeCorpus, |
| 44 | +} from "./corpus-fixtures.js"; |
| 45 | + |
30 | 46 | const HORIZON = process.env.HORIZON ?? "https://horizon.stellar.org"; |
31 | 47 | const OUT_DIR = resolve(import.meta.dirname, "../test/fixtures/horizon-corpus"); |
32 | | -const COUNT = Number(process.env.COUNT ?? 50); |
| 48 | +const COUNT = readCount(process.env.COUNT, 50); |
33 | 49 |
|
34 | | -interface CorpusFile<T> { |
35 | | - source: string; |
36 | | - fetchedAt: string; |
37 | | - records: T[]; |
| 50 | +// Horizon caps a single page at 200 records. |
| 51 | +const PAGE_LIMIT = Math.min(COUNT, 200); |
| 52 | + |
| 53 | +interface HorizonPage { |
| 54 | + _embedded?: { records?: Array<Record<string, unknown>> }; |
38 | 55 | } |
39 | 56 |
|
40 | | -async function getJson(url: string): Promise<unknown> { |
| 57 | +async function getJson(url: string): Promise<HorizonPage> { |
41 | 58 | const r = await fetch(url); |
42 | 59 | if (!r.ok) throw new Error(`${url} → ${r.status} ${r.statusText}`); |
43 | | - return r.json(); |
| 60 | + return (await r.json()) as HorizonPage; |
44 | 61 | } |
45 | 62 |
|
46 | 63 | async function snapshotTransactions(): Promise<void> { |
47 | | - const url = `${HORIZON}/transactions?limit=${Math.min(COUNT, 200)}&order=desc&include_failed=false`; |
48 | | - // eslint-disable-next-line @typescript-eslint/no-explicit-any |
49 | | - const json = (await getJson(url)) as any; |
50 | | - const records = json._embedded.records.map( |
51 | | - // eslint-disable-next-line @typescript-eslint/no-explicit-any |
52 | | - (r: any) => ({ |
53 | | - hash: r.hash, |
54 | | - envelope_xdr: r.envelope_xdr, |
55 | | - result_xdr: r.result_xdr, |
56 | | - result_meta_xdr: r.result_meta_xdr, |
57 | | - }), |
58 | | - ); |
59 | | - writeCorpus("transactions.json", { |
| 64 | + const url = `${HORIZON}/transactions?limit=${PAGE_LIMIT}&order=desc&include_failed=false`; |
| 65 | + const records = ((await getJson(url))._embedded?.records ?? []).map((r) => { |
| 66 | + const where = `transaction ${String(r.hash ?? "<no hash>")}`; |
| 67 | + return { |
| 68 | + hash: requireString(r.hash, "hash", where), |
| 69 | + envelope_xdr: requireString(r.envelope_xdr, "envelope_xdr", where), |
| 70 | + result_xdr: requireString(r.result_xdr, "result_xdr", where), |
| 71 | + fee_meta_xdr: requireString(r.fee_meta_xdr, "fee_meta_xdr", where), |
| 72 | + }; |
| 73 | + }); |
| 74 | + |
| 75 | + writeCorpus(OUT_DIR, "transactions.json", { |
60 | 76 | source: url, |
61 | 77 | fetchedAt: new Date().toISOString(), |
62 | 78 | records, |
63 | 79 | }); |
64 | | - console.log(`Wrote ${records.length} transaction records.`); |
| 80 | + reportCount(records.length, COUNT, "transaction"); |
| 81 | + reportEnvelopeSpread(records.map((r) => r.envelope_xdr)); |
65 | 82 | } |
66 | 83 |
|
67 | 84 | async function snapshotLedgers(): Promise<void> { |
68 | | - const url = `${HORIZON}/ledgers?limit=${Math.min(COUNT, 200)}&order=desc`; |
69 | | - // eslint-disable-next-line @typescript-eslint/no-explicit-any |
70 | | - const json = (await getJson(url)) as any; |
71 | | - const records = json._embedded.records.map( |
72 | | - // eslint-disable-next-line @typescript-eslint/no-explicit-any |
73 | | - (r: any) => ({ |
74 | | - sequence: r.sequence, |
75 | | - header_xdr: r.header_xdr, |
76 | | - }), |
77 | | - ); |
78 | | - writeCorpus("ledgers.json", { |
| 85 | + const url = `${HORIZON}/ledgers?limit=${PAGE_LIMIT}&order=desc`; |
| 86 | + const records = ((await getJson(url))._embedded?.records ?? []).map((r) => { |
| 87 | + const where = `ledger ${String(r.sequence ?? "<no sequence>")}`; |
| 88 | + return { |
| 89 | + sequence: requireNumber(r.sequence, "sequence", where), |
| 90 | + header_xdr: requireString(r.header_xdr, "header_xdr", where), |
| 91 | + }; |
| 92 | + }); |
| 93 | + |
| 94 | + writeCorpus(OUT_DIR, "ledgers.json", { |
79 | 95 | source: url, |
80 | 96 | fetchedAt: new Date().toISOString(), |
81 | 97 | records, |
82 | 98 | }); |
83 | | - console.log(`Wrote ${records.length} ledger records.`); |
84 | | -} |
85 | | - |
86 | | -function writeCorpus<T>(filename: string, payload: CorpusFile<T>): void { |
87 | | - mkdirSync(OUT_DIR, { recursive: true }); |
88 | | - writeFileSync( |
89 | | - resolve(OUT_DIR, filename), |
90 | | - JSON.stringify(payload, null, 2) + "\n", |
91 | | - ); |
| 99 | + reportCount(records.length, COUNT, "ledger"); |
92 | 100 | } |
93 | 101 |
|
94 | 102 | async function main(): Promise<void> { |
95 | 103 | console.log(`Fetching from ${HORIZON}, COUNT=${COUNT}`); |
96 | 104 | await snapshotTransactions(); |
97 | 105 | await snapshotLedgers(); |
98 | 106 | console.log(`\nCorpus written to ${OUT_DIR}`); |
99 | | - console.log(`Run \`pnpm exec vitest run test/unit/base/xdr/corpus_round_trip.test.ts\` to validate.`); |
| 107 | + console.log( |
| 108 | + `Run \`pnpm exec vitest run test/unit/xdr/corpus_round_trip.test.ts\` to validate.`, |
| 109 | + ); |
100 | 110 | } |
101 | 111 |
|
102 | 112 | main().catch((err) => { |
|
0 commit comments