Skip to content

Commit 89162f5

Browse files
Merge pull request #121 from ar-io/fix/loading-error
Fix/loading error
2 parents 3bae71a + 0eab918 commit 89162f5

12 files changed

Lines changed: 689 additions & 14 deletions
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Deploy to GitHub Pages (Preview)
2+
3+
on:
4+
workflow_dispatch: # Only manual trigger
5+
6+
permissions:
7+
contents: read
8+
pages: write
9+
id-token: write
10+
11+
concurrency:
12+
group: "pages"
13+
cancel-in-progress: false
14+
15+
jobs:
16+
build:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v6
21+
22+
- name: Setup Node.js
23+
uses: actions/setup-node@v6
24+
with:
25+
node-version: "22"
26+
cache: "yarn"
27+
28+
- name: Install dependencies
29+
run: yarn install --frozen-lockfile
30+
31+
- name: Clear Next.js build cache
32+
run: rm -rf .next out
33+
34+
- name: Build
35+
run: yarn build
36+
env:
37+
NODE_ENV: production
38+
BASE_PATH: /ar-io-docs
39+
NEXT_PUBLIC_BASE_PATH: /ar-io-docs
40+
NEXT_PUBLIC_SITE_URL: https://ar-io.github.io/ar-io-docs
41+
42+
- name: Setup Pages
43+
uses: actions/configure-pages@v6
44+
45+
- name: Upload Pages artifact
46+
uses: actions/upload-pages-artifact@v5
47+
with:
48+
path: ./out
49+
50+
deploy:
51+
environment:
52+
name: github-pages
53+
url: ${{ steps.deployment.outputs.page_url }}
54+
runs-on: ubuntu-latest
55+
needs: build
56+
steps:
57+
- name: Deploy to GitHub Pages
58+
id: deployment
59+
uses: actions/deploy-pages@v5

.github/workflows/pr-preview.yaml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
name: PR Preview Deployment
22

33
on:
4-
pull_request:
5-
types: [opened, synchronize, reopened, closed]
6-
paths:
7-
- "content/**"
8-
- "src/**"
4+
workflow_dispatch:
95

106
jobs:
117
lint:
128
runs-on: ubuntu-latest
13-
if: github.event.action != 'closed'
149

1510
steps:
1611
- name: Checkout repository
@@ -31,7 +26,6 @@ jobs:
3126
deploy-preview:
3227
runs-on: ubuntu-latest
3328
needs: lint
34-
if: github.event.pull_request.head.repo.full_name == github.repository && github.event.action != 'closed'
3529

3630
steps:
3731
- name: Checkout repository

content/build/run-a-gateway/manage/environment-variables.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,8 @@ The default public Solana RPC is rate-limited and may block `getProgramAccounts`
382382

383383
The observer uploads report bundles to Turbo. The upload signer is resolved from the first matching env in the [precedence chain](/build/run-a-gateway/manage/solana-migration#upload-signing-precedence). Setting envs from more than one chain group at once is rejected at startup.
384384

385+
If your observer logs warn that `TurboReportSink` is not configured, explicitly set a Solana upload signer. Most operators can use the same base58 secret for both `OBSERVER_PRIVATE_KEY` and `SOLANA_UPLOAD_PRIVATE_KEY`.
386+
385387
| Variable | Type | Default | Description |
386388
| --------------------------------- | ------ | ------- | --------------------------------------------------------------------------- |
387389
| `ARWEAVE_UPLOAD_KEY_FILE` | string | - | Path to an Arweave JWK file. Highest priority for upload signing |
@@ -391,7 +393,7 @@ The observer uploads report bundles to Turbo. The upload signer is resolved from
391393
| `SOLANA_UPLOAD_KEYPAIR_PATH` | string | - | Path to a separate Solana keypair JSON for uploads. Ignored when any `ARWEAVE_UPLOAD_*` or `ETHEREUM_UPLOAD_*` is set |
392394
| `SOLANA_UPLOAD_PRIVATE_KEY` | string | - | Alternative to above: base58 secret. Mutually exclusive with the file form |
393395

394-
When none of the above are set, uploads fall back to the observer key, then the operator key.
396+
When none of the above are set, uploads fall back to the observer key, then the operator key. For production observers, prefer setting `SOLANA_UPLOAD_KEYPAIR_PATH` or `SOLANA_UPLOAD_PRIVATE_KEY` explicitly so report uploads do not depend on fallback behavior.
395397

396398
### Offset Observation
397399

content/build/run-a-gateway/manage/solana-migration.mdx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ Complete these steps before the cutover date to ensure uninterrupted reward elig
120120
</Callout>
121121

122122
<Callout type="info" title="Phantom-export keys">
123-
If you already have a Phantom-exported base58 secret string for the observer or operator, you can skip the JSON keypair file entirely: set `OBSERVER_PRIVATE_KEY=<base58>` (or `SOLANA_PRIVATE_KEY=<base58>`) instead of the `*_KEYPAIR_PATH` env. Setting both forms for the same role is rejected at startup.
123+
If you already have a Phantom-exported base58 secret string for the observer or operator, you can skip the JSON keypair file entirely: set `OBSERVER_PRIVATE_KEY=<base58>` (or `SOLANA_PRIVATE_KEY=<base58>`) instead of the `*_KEYPAIR_PATH` env. When using an inline observer key, also set `SOLANA_UPLOAD_PRIVATE_KEY=<base58>` so observer report uploads to Turbo use the same signer. Setting both forms for the same role is rejected at startup.
124124
</Callout>
125125
</Step>
126126

@@ -223,7 +223,7 @@ The gateway uses up to four distinct wallet roles. Understanding these helps you
223223
|---|---|---|---|
224224
| **Operator** (+ cranker) | `join_network`, `update_gateway_settings`, permissionless cranker instructions | `SOLANA_KEYPAIR_PATH` or `SOLANA_PRIVATE_KEY` | — (required) |
225225
| **Observer** | `save_observations` transactions | `OBSERVER_KEYPAIR_PATH` or `OBSERVER_PRIVATE_KEY` | Falls back to operator key |
226-
| **Upload** | Observer report bundles sent to Turbo | See [upload precedence](#upload-signing-precedence) below | Falls back to observer → operator Solana key |
226+
| **Upload** | Observer report bundles sent to Turbo | See [upload precedence](#upload-signing-precedence) below | Falls back to observer → operator Solana key, but explicit upload env is recommended |
227227
| **HTTPSIG signer** | RFC 9421 response headers | Uses observer Solana key when set | Auto-generated standalone Ed25519 key |
228228

229229
<Callout type="info">
@@ -247,12 +247,20 @@ These are the five supported wallet setups. **Pattern 1 is the recommended defau
247247
```bash
248248
# One key for operator + observer + uploads
249249
SOLANA_KEYPAIR_PATH=/app/wallets/operator-keypair.json
250+
SOLANA_UPLOAD_KEYPAIR_PATH=/app/wallets/operator-keypair.json
250251
SOLANA_RPC_URL=<your dedicated RPC endpoint>
251252
AR_IO_WALLET=<your Solana pubkey>
252253
OBSERVER_WALLET=<your Solana pubkey>
253254
ENABLE_EPOCH_CRANKING=false # flip to true when ready
254255
```
255256

257+
If you use a base58 private key instead of a keypair file, explicitly set the upload key too:
258+
259+
```bash
260+
OBSERVER_PRIVATE_KEY=<your base58 private key>
261+
SOLANA_UPLOAD_PRIVATE_KEY=<your base58 private key>
262+
```
263+
256264
#### Pattern 2 — Keep existing Arweave JWK for uploads
257265

258266
The most common path for operators migrating from a pre-Solana setup. Your existing Arweave JWK continues signing report bundles while the Solana keypair handles protocol interactions.
@@ -275,14 +283,19 @@ The gateway picks the first matching upload signer from this list:
275283
2. ARWEAVE_UPLOAD_JWK (inline) → ArweaveSigner
276284
3. ETHEREUM_UPLOAD_PRIVATE_KEY_FILE (file) → EthereumSigner
277285
4. ETHEREUM_UPLOAD_PRIVATE_KEY (inline) → EthereumSigner
278-
5. SOLANA_UPLOAD_KEYPAIR_PATH (explicit) → SolanaSigner
279-
6. Fallback: OBSERVER_KEYPAIR_PATH ?? SOLANA_KEYPAIR_PATH → SolanaSigner
286+
5. SOLANA_UPLOAD_KEYPAIR_PATH (explicit file) → SolanaSigner
287+
6. SOLANA_UPLOAD_PRIVATE_KEY (explicit inline) → SolanaSigner
288+
7. Fallback: OBSERVER_KEYPAIR_PATH ?? SOLANA_KEYPAIR_PATH → SolanaSigner
280289
```
281290

282291
<Callout type="warn">
283292
Setting upload envs from more than one chain at once (e.g. `ARWEAVE_UPLOAD_KEY_FILE` **plus** `ETHEREUM_UPLOAD_PRIVATE_KEY`) raises a startup error listing every conflicting env. Pick exactly one upload chain.
284293
</Callout>
285294

295+
<Callout type="info" title="TurboReportSink not configured">
296+
If observer logs warn that `TurboReportSink` is not configured, the observer does not have an upload signer for report data. Add `SOLANA_UPLOAD_PRIVATE_KEY=<same base58 key as OBSERVER_PRIVATE_KEY>` or `SOLANA_UPLOAD_KEYPAIR_PATH=<same keypair path as OBSERVER_KEYPAIR_PATH>`, then restart the observer and check the next epoch logs.
297+
</Callout>
298+
286299
### Key Formats
287300

288301
Solana keypairs come in two common formats. Both encode the same 64-byte secret (`seed(32) || pubkey(32)`):

content/build/run-a-gateway/quick-start.mdx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ Ready to run a gateway with your own domain name and SSL certificates? Follow th
114114
ARNS_ROOT_HOST=<your-domain>
115115
AR_IO_WALLET=<your-solana-pubkey>
116116
OBSERVER_WALLET=<observer-solana-pubkey>
117+
OBSERVER_PRIVATE_KEY=<observer-base58-private-key>
118+
SOLANA_UPLOAD_PRIVATE_KEY=<observer-base58-private-key>
117119
```
118120

119121
<Callout type="info">
@@ -123,8 +125,15 @@ Ready to run a gateway with your own domain name and SSL certificates? Follow th
123125
**Supply Observer Wallet Keyfile:**
124126
Save your Solana keypair JSON file as `<Observer-Wallet-Address>.json` in the `wallets` directory.
125127

128+
If you use a keypair file instead of inline base58 keys, remove `OBSERVER_PRIVATE_KEY` and `SOLANA_UPLOAD_PRIVATE_KEY` from `.env`, then set both paths explicitly:
129+
130+
```bash
131+
OBSERVER_KEYPAIR_PATH=/app/wallets/<Observer-Wallet-Address>.json
132+
SOLANA_UPLOAD_KEYPAIR_PATH=/app/wallets/<Observer-Wallet-Address>.json
133+
```
134+
126135
<Callout type="info" title="Payment For Observer Report Uploads">
127-
By default, the Observer will use [Turbo Credits](https://docs.ardrive.io/docs/turbo/credits) to pay for uploading reports to Arweave. This allows reports under 100kb to be uploaded for free, but larger reports will fail if the Observer wallet does not contain Credits. Including `REPORT_DATA_SINK=arweave` in your `.env` file will configure the Observer to use AR tokens instead of Turbo Credits, without any free limit.
136+
By default, the Observer will use [Turbo Credits](https://docs.ardrive.io/docs/turbo/credits) to pay for uploading reports to Arweave. This allows reports under 100kb to be uploaded for free, but larger reports will fail if the Observer wallet does not contain Credits. Set `SOLANA_UPLOAD_PRIVATE_KEY` to the same base58 secret as `OBSERVER_PRIVATE_KEY` so Turbo has an explicit upload signer. If logs warn that `TurboReportSink` is not configured, this upload key is usually missing. Including `REPORT_DATA_SINK=arweave` in your `.env` file will configure the Observer to use AR tokens instead of Turbo Credits, without any free limit.
128137
</Callout>
129138

130139
**Start the Docker container:**
@@ -306,12 +315,21 @@ Ready to run a gateway with your own domain name and SSL certificates? Follow th
306315
ARNS_ROOT_HOST=<your-domain>
307316
AR_IO_WALLET=<your-public-wallet-address>
308317
OBSERVER_WALLET=<hot-wallet-public-address>
318+
OBSERVER_PRIVATE_KEY=<observer-base58-private-key>
319+
SOLANA_UPLOAD_PRIVATE_KEY=<observer-base58-private-key>
309320
```
310321

311322
**Save as `.env`** (select "All Files" as file type)
312323

313324
**Supply Observer Wallet Keyfile:**
314325
Save your wallet keyfile as `<Observer-Wallet-Address>.json` in the `wallets` directory.
326+
327+
If you use a keypair file instead of inline base58 keys, remove `OBSERVER_PRIVATE_KEY` and `SOLANA_UPLOAD_PRIVATE_KEY` from `.env`, then set both paths explicitly:
328+
329+
```bash
330+
OBSERVER_KEYPAIR_PATH=/app/wallets/<Observer-Wallet-Address>.json
331+
SOLANA_UPLOAD_KEYPAIR_PATH=/app/wallets/<Observer-Wallet-Address>.json
332+
```
315333
</Step>
316334

317335
<Step>

next.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const config = {
88
// Enable static export only for production builds
99
output: process.env.NODE_ENV === "production" ? "export" : "standalone",
1010
trailingSlash: process.env.NODE_ENV === "production" ? true : false,
11+
basePath: process.env.BASE_PATH || "",
1112
reactStrictMode: true,
1213
eslint: {
1314
// Warning: This allows production builds to successfully complete even if

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "0.0.0",
44
"private": true,
55
"scripts": {
6-
"build": "NODE_ENV=production next build",
6+
"build": "NODE_ENV=production next build && tsx scripts/inject-chunk-load-recovery.ts",
77
"dev": "next dev --turbo",
88
"start": "next start",
99
"lint": "eslint src/ content/ --ext .ts,.tsx,.mdx",
@@ -14,6 +14,7 @@
1414
"generate-sdk-llm-texts": "node scripts/generate-sdk-llm-texts.js",
1515
"generate-all-docs": "npm run generate-sdk-docs && npm run generate-llm-text && npm run generate-sdk-llm-texts",
1616
"check-links": "node scripts/check-links.mjs",
17+
"test:chunk-recovery": "node tests/chunk-load-recovery.test.mjs",
1718
"test-arns": "node scripts/test-arns-update.js",
1819
"test-signer": "node scripts/test-signer-only.js"
1920
},
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* Post-build step: inject the chunk-load recovery script as a real,
3+
* parser-blocking inline <script> at the very top of <head> in every exported
4+
* HTML file.
5+
*
6+
* Why a post-build step instead of rendering it in the React tree: under
7+
* `output: "export"` (Next.js static export + Turbopack + React 19) the RSC
8+
* renderer serializes any inline <script> — whether authored via next/script
9+
* or a plain <script> tag — into the React Flight payload (__next_f) rather
10+
* than emitting an executable tag. Such a script only runs after hydration,
11+
* which depends on the very app chunks it is meant to recover. Injecting into
12+
* the emitted HTML guarantees it executes before the async chunk scripts.
13+
*/
14+
import { promises as fs } from "node:fs";
15+
import path from "node:path";
16+
import { fileURLToPath } from "node:url";
17+
import { chunkLoadRecoveryScript } from "../src/lib/chunk-load-recovery-script";
18+
19+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
20+
const OUT_DIR = path.resolve(__dirname, "../out");
21+
const MARKER_ID = "chunk-load-recovery";
22+
const SCRIPT_TAG = `<script id="${MARKER_ID}">${chunkLoadRecoveryScript}</script>`;
23+
24+
async function collectHtmlFiles(dir: string): Promise<string[]> {
25+
const entries = await fs.readdir(dir, { withFileTypes: true });
26+
const files = await Promise.all(
27+
entries.map(async (entry) => {
28+
const full = path.join(dir, entry.name);
29+
if (entry.isDirectory()) return collectHtmlFiles(full);
30+
return entry.isFile() && entry.name.endsWith(".html") ? [full] : [];
31+
}),
32+
);
33+
return files.flat();
34+
}
35+
36+
async function main() {
37+
try {
38+
await fs.access(OUT_DIR);
39+
} catch {
40+
throw new Error(
41+
`Output directory not found: ${OUT_DIR}. Run "next build" first.`,
42+
);
43+
}
44+
45+
const htmlFiles = await collectHtmlFiles(OUT_DIR);
46+
if (htmlFiles.length === 0) {
47+
throw new Error(`No HTML files found under ${OUT_DIR}.`);
48+
}
49+
50+
let injected = 0;
51+
let skipped = 0;
52+
const missingHead: string[] = [];
53+
54+
for (const file of htmlFiles) {
55+
const html = await fs.readFile(file, "utf8");
56+
57+
if (html.includes(`id="${MARKER_ID}"`)) {
58+
skipped += 1;
59+
continue;
60+
}
61+
62+
const headIndex = html.indexOf("<head>");
63+
if (headIndex === -1) {
64+
missingHead.push(path.relative(OUT_DIR, file));
65+
continue;
66+
}
67+
68+
const insertAt = headIndex + "<head>".length;
69+
const next = html.slice(0, insertAt) + SCRIPT_TAG + html.slice(insertAt);
70+
await fs.writeFile(file, next);
71+
injected += 1;
72+
}
73+
74+
console.log(
75+
`[inject-chunk-load-recovery] injected into ${injected} file(s), ` +
76+
`skipped ${skipped} already-injected file(s).`,
77+
);
78+
79+
if (missingHead.length > 0) {
80+
throw new Error(
81+
`No <head> found in ${missingHead.length} HTML file(s): ` +
82+
missingHead.slice(0, 10).join(", ") +
83+
(missingHead.length > 10 ? ", …" : ""),
84+
);
85+
}
86+
}
87+
88+
main().catch((error) => {
89+
console.error("[inject-chunk-load-recovery]", error);
90+
process.exit(1);
91+
});

0 commit comments

Comments
 (0)