Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3851c22
Add minerals economy v2 foundation
Aug 20, 2026
9659352
Require fixed anchors for mineral settlement
Aug 20, 2026
0b06dd2
feat: add minerals economy mutations and wallet API
Aug 20, 2026
8c3b94a
fix: close minerals economy cutover and concurrency gaps
Aug 20, 2026
3fbbe23
fix: preserve pre-cutover generation idempotency
Aug 20, 2026
bff68bc
fix: make mineral reads and upgrades consistent
Aug 20, 2026
9d71207
Merge remote-tracking branch 'origin/main' into feat/minerals-economy-v2
Aug 20, 2026
914d4b7
Fix approvals and polish Megastera UI
AstralEX1 Aug 20, 2026
2960045
feat: add mineral account backfill command
AstralEX1 Aug 20, 2026
873b603
fix: use authoritative mineral cutover clock
AstralEX1 Aug 20, 2026
ec5bb43
fix: harden mineral economy rollout
AstralEX1 Aug 20, 2026
a16cacd
Fix reusable approvals and purchase flow
AstralEX1 Aug 20, 2026
17e55b4
fix: audit minerals economy stage b
AstralEX1 Aug 20, 2026
c8bc2de
fix: close pre-cutover planet repair race
AstralEX1 Aug 20, 2026
792d070
fix: close planet fallback update race
AstralEX1 Aug 20, 2026
e70601e
feat: rank leaderboard by spendable minerals
AstralEX1 Aug 20, 2026
0547776
Keep purchase errors inline
AstralEX1 Aug 20, 2026
8ff2401
feat: harden minerals economy rollout
AstralEX1 Aug 20, 2026
9d57b5e
fix: build generator before postgres tests
AstralEX1 Aug 20, 2026
a16cc07
fix: timestamp generation after account lock
AstralEX1 Aug 20, 2026
da12341
fix: close minerals economy integrity gaps
AstralEX1 Aug 21, 2026
6fac6ba
test: make minerals concurrency checks deterministic
AstralEX1 Aug 21, 2026
d7b33a2
merge: include current production fixes
AstralEX1 Aug 21, 2026
da472aa
fix(db): lock trigger function search paths
AstralEX1 Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ BASE_RPC_URL=https://mainnet.base.org
# Optional comma-separated fallback RPCs for receipt verification.
# BASE_RPC_FALLBACK_URLS=https://base-rpc.publicnode.com
DATABASE_URL=postgresql://user:password@localhost:5432/megastera
# Optional direct PostgreSQL URL for Prisma migrations and mineral cutover preparation.
# DIRECT_URL=
# Optional server-only key for the same-origin Megapot Data API proxy.
# MEGAPOT_API_KEY=
# Optional V2 wallet-mineral cutover. Empty keeps Economy v2 disabled.
# Must be an exact UTC midnight, for example 2026-08-21T00:00:00.000Z.
# MINERAL_ECONOMY_CUTOVER_AT=
# Keep disabled until the upgrade route has owner-bound authentication.
# MINERAL_UPGRADES_ENABLED=false
# Receipt proofs are accepted only after this many newer canonical blocks.
MEGAPLANETS_CONFIRMATIONS=6

Expand Down
49 changes: 49 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,52 @@ jobs:

- name: Build production bundle
run: pnpm build

postgres:
name: PostgreSQL economy concurrency
runs-on: ubuntu-latest
timeout-minutes: 20
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: megaplanets
POSTGRES_PASSWORD: megaplanets
POSTGRES_DB: megaplanets
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U megaplanets -d megaplanets"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
DATABASE_URL: postgresql://megaplanets:megaplanets@127.0.0.1:5432/megaplanets
DIRECT_URL: postgresql://megaplanets:megaplanets@127.0.0.1:5432/megaplanets
MINERAL_ECONOMY_TEST_DATABASE_URL: postgresql://megaplanets:megaplanets@127.0.0.1:5432/megaplanets
MINERAL_ECONOMY_REQUIRE_POSTGRES: '1'

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up pnpm
uses: pnpm/action-setup@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Generate Prisma client
run: pnpm db:generate

- name: Apply full Prisma migration chain
run: pnpm db:migrate:deploy

- name: Run real PostgreSQL concurrency suite
run: pnpm test:postgres
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ dist-ssr
.data/
api/generated/
server/api/generated/
.codegraph/

# editor
.vscode/*
Expand Down
16 changes: 16 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!-- CODEGRAPH_START -->
## CodeGraph

In repositories indexed by CodeGraph (a `.codegraph/` directory exists at the repo root), reach for it BEFORE grep/find or reading files when you need to understand or locate code:

- **MCP tool** (when available): `codegraph_explore` answers most code questions in one call — the relevant symbols' verbatim source plus the call paths between them, including dynamic-dispatch hops grep can't follow. Name a file or symbol in the query to read its current line-numbered source. If it's listed but deferred, load it by name via tool search.
- **Shell** (always works): `codegraph explore "<symbol names or question>"` prints the same output.

If there is no `.codegraph/` directory, skip CodeGraph entirely — indexing is the user's decision.
<!-- CODEGRAPH_END -->

## Local environment files

- Keep `.env.local` named exactly `.env.local` so Vite loads it; on macOS, run `chflags nohidden .env.local` after creating it so Finder displays the file. Never print, commit, or expose its contents.

For Megapot contract work, fetch [https://llms.megapot.io](https://llms.megapot.io)
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,18 +190,21 @@ The current product includes:
- wallet connection and Megapot ticket purchasing;
- Base mainnet ticket flow;
- coordinate selection and direct checkout;
- keeper-assisted bulk purchases for larger all-random expeditions;
- keeper-assisted bulk purchases for 11–50 ticket expeditions, including up to ten selected-coordinate tickets;
- canonical `TicketPurchased` receipt recovery and verification;
- deterministic planet generation from immutable ticket provenance;
- deterministic planet traits and GIF rendering;
- persistent planet and ticket records in PostgreSQL;
- retry-safe pending states while receipts finalize or generation completes;
- `My Planets` collection and ticket status;
- continuous mineral production;
- wallet leaderboard;
- live mineral production with same-type collection bonuses;
- current wallet leaderboard;
- opt-in mineral-economy-v2 spendable balances and persisted per-Planet upgrade primitives;
- Megapot drawing state, ticket history and winnings;
- onchain winnings claim flow after simulation;
- Season 1 leaderboard competition and manual reward settlement.
- Season 1 leaderboard countdown/prize presentation; manual reward settlement is outside the runtime/API in this repository.

This worktree also contains an opt-in mineral-economy-v2 path. Before the prepared cutover, wallet mining and the current leaderboard use the V1 live calculation. After cutover, the backend reconstructs spendable mineral scores from opening balances, historical production, collection/upgrade rate changes, and persisted upgrade costs. The unauthenticated upgrade endpoint remains server-disabled until owner-bound authorization is implemented.

Planet generation is idempotent and keyed by ticket provenance, so the same valid ticket cannot create duplicate planets through repeated generation requests.

Expand Down Expand Up @@ -231,6 +234,7 @@ The browser handles interaction, but it is not the authority for planet creation
- Planet generation is deterministic and idempotent.
- Conflicting persisted provenance fails closed.
- Mining is derived from persisted planet data rather than mutable browser state.
- After the mineral-economy cutover, wallet balances and current leaderboard scores are derived from persisted integer-micro account and upgrade history; V1 remains the pre-cutover fallback.
- Generated GIF bytes are stored with an immutable content hash.
- Server credentials and database secrets never enter the Vite client environment.

Expand All @@ -242,7 +246,7 @@ For deeper implementation details, see [`docs/ARCHITECTURE.md`](docs/ARCHITECTUR

Season 1 establishes the core **Megapot ticket → planet → mining → competition** loop.

Future seasons can expand that foundation with planet upgrades, colonies, stars, ships, trading, and deeper player-to-player interaction. Existing planets are intended to remain relevant as the universe grows rather than becoming obsolete after a season.
Future seasons can expand that foundation with colonies, stars, ships, trading, and deeper player-to-player interaction. This worktree's mineral upgrade path is feature-gated and operator-controlled; it is not a completed reward-system rollout. Existing planets are intended to remain relevant as the universe grows rather than becoming obsolete after a season.

## Repository

Expand Down
18 changes: 12 additions & 6 deletions api/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Megastera API

The active API verifies finalized Megapot `TicketPurchased` receipt logs, persists the canonical ticket row, generates one deterministic Planet GIF per receipt log, and serves the database-backed collection, mining snapshots, and leaderboard.
The active API verifies finalized Megapot `TicketPurchased` receipt logs, persists the canonical ticket row, generates one deterministic Planet GIF per receipt log, and serves the database-backed collection, live mining snapshots, and current leaderboard. It also exposes the same-origin Megapot Data API proxy.

## HTTP surface

Expand All @@ -9,27 +9,33 @@ The active API verifies finalized Megapot `TicketPurchased` receipt logs, persis
- `POST /api/planets/generate`
- `POST /api/planets/generate/batch`
- `GET /api/planets?owner=...`
- `GET /api/planets/collection?owner=...`
- `GET /api/planets/:planetId`
- `GET /api/planets/:planetId/gif`
- `GET /api/planets/:planetId/mining`
- `POST /api/planets/:planetId/upgrade`
- `GET /api/wallets/:address/mining`
- `GET /api/leaderboard/current`, `/current/:address`, `/history`, `/days/:periodId`
- `GET /api/leaderboard/current?offset=...&limit=...`
- `GET /api/leaderboard/current/:address`
- `GET /api/megapot/*` (proxy to the Base mainnet Megapot Data API)

Planet vouchers, Pinata/IPFS artifacts, Planet contract reads/writes, direct holdings, and continuous indexers are intentionally not exposed.
The batch generation body accepts 1–50 receipt references. Archived leaderboard periods are written by the separate `pnpm api:leaderboard-worker` process but are not exposed by the current HTTP surface. The unauthenticated upgrade endpoint always returns `404`; its persistence primitives remain available for authenticated server-side integration later.

Planet vouchers, Pinata/IPFS artifacts, Planet contract reads/writes, direct holdings, continuous indexers, and leaderboard history/finalization routes are intentionally not exposed.

## Local process

```sh
pnpm api:server
```

The HTTP server defaults to `127.0.0.1:8787`; set `MEGAPLANETS_API_HOST` and `MEGAPLANETS_API_PORT` to override. Backend generation requires `DATABASE_URL` and `BASE_RPC_URL`; optional RPC failover is configured with `BASE_RPC_FALLBACK_URLS`.
The HTTP server defaults to `127.0.0.1:8787`; set `MEGAPLANETS_API_HOST` and `MEGAPLANETS_API_PORT` to override. Backend generation requires `DATABASE_URL` and `BASE_RPC_URL`; optional RPC failover is configured with `BASE_RPC_FALLBACK_URLS`. Set server-only `MEGAPOT_API_KEY` if the proxy should authenticate upstream requests.

## Safety boundary

- `MEGAPLANETS_ALLOWED_ORIGINS` remains an exact CORS allowlist.
- Request bodies are bounded to 16 KiB.
- Batch generation is bounded to 50 receipt references.
- Receipt verification checks Base mainnet, canonical jackpot/source, event fields, confirmation depth, block hash, and optional wallet recipient.
- Generation is idempotent on `originTxHash:logIndex`; conflicting persisted provenance is rejected.
- GIF bytes are stored in PostgreSQL and served with an immutable content hash.
- Leaderboard reads are derived from ready persisted Planet records.
- Leaderboard reads are derived from ready persisted Planet records, collection milestones, and—after cutover—mineral account and upgrade history.
30 changes: 19 additions & 11 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Megastera architecture

This document describes the active Megastera architecture on Base mainnet. Historical NFT/indexer experiments remain in git history only; they are not runtime requirements.
This document describes the active Megastera architecture on Base mainnet and the opt-in mineral-economy-v2 path in this worktree. Legacy NFT/indexer models and migrations remain for database compatibility; they are not runtime requirements or active write paths.

## Sources of truth

Expand All @@ -10,9 +10,11 @@ This document describes the active Megastera architecture on Base mainnet. Histo
| Ticket eligibility | Finalized receipt containing canonical `MEGASTERA` `TicketPurchased` | `server/api/receiptVerification.ts` |
| Historical ticket status and winnings | Base mainnet Megapot Data API wallet ticket feed | `src/lib/api.ts` / `useWalletTickets` |
| Planet identity and media | Shared deterministic generator plus `BackendPlanet` row | API and My Planets |
| Planet ownership | `BackendPlanet.ownerAddress`, copied from the verified receipt recipient | API list/mining routes |
| Planet mining | `baseMineralsPerDay` and `generatedAt` | `server/api/miningStore.ts` |
| Leaderboard | Ready `BackendPlanet.generatedAt` + `baseMineralsPerDay` rows | Live read route with an approximately 60-second backend cache |
| Planet ownership | `BackendPlanet.ownerAddress`, copied from the verified receipt recipient | API list/wallet mining route |
| Planet mining | V1 `baseMineralsPerDay`/`generatedAt` calculation before cutover; V2 `MineralAccount`, collection events, and upgrade history after cutover | `server/api/miningStore.ts` / `mineralEconomy.ts` |
| Current leaderboard | V1 live rows before cutover; after cutover, opening balance + historical production − persisted upgrade costs | `server/api/leaderboardStore.ts` / `leaderboardRoutes.ts` |
| Mineral economy cutover | Exact UTC-midnight configuration reconciled with the persisted `MineralEconomyCutover` row and PostgreSQL clock | `server/api/backendConfig.ts` / `mineralAccounts.ts` |
| Archived leaderboard periods | `LeaderboardPeriod` and `LeaderboardEntry` rows written by the standalone finalization worker | Database/worker only; no archived-period HTTP route is mounted |

## Runtime flow

Expand All @@ -28,29 +30,33 @@ Megapot receipt

If generation fails after the receipt proof is saved, `GET /api/planets/collection` still returns a pending row. If the receipt is not final yet, the frontend keeps its canonical receipt in local storage and renders the same ticket as pending until the backend catch-up pass can persist and generate it.

Direct purchases use one to ten tickets. Eleven to fifty all-random tickets use the Megapot keeper facilitator. For bulk orders, each execution receipt is processed; the order-creation transaction is never used as Planet provenance. Every ticket remains tied to its immutable `originTxHash:logIndex` key.
Direct purchases use one to ten tickets. Eleven to fifty ticket expeditions use the Megapot keeper facilitator; up to ten tickets may carry selected coordinates and the rest are dynamic. For bulk orders, each execution receipt is processed; the order-creation transaction is never used as Planet provenance. Every ticket remains tied to its immutable `originTxHash:logIndex` key.

When `MINERAL_ECONOMY_CUTOVER_AT` is absent or still in the future, wallet mining and current leaderboard reads use the V1 live calculation. Once the cutover is active, current leaderboard reads reconstruct each wallet's spendable score as opening balance plus historical production minus upgrade purchase costs, with collection and upgrade events changing the effective rate at their event times. The post-cutover read is computed from a repeatable-read database transaction.

The source tag is always `MEGASTERA`. It is a Megapot attribution value and does not refer to a Planet NFT deployment.

## Backend API

The Vercel entrypoint `api/index.ts` delegates to `server/api/index.ts`, which mounts only:

- backend Planet generation, collection, GIF, and mining routes;
- live leaderboard routes; and
- backend Planet generation, collection, GIF, and wallet mining routes;
- the server-disabled Planet upgrade endpoint;
- live leaderboard routes;
- the same-origin `/api/megapot/*` proxy for the Megapot Data API; and
- liveness/metrics routes.

`server/api/receiptVerification.ts` performs the complete verification sequence against bounded RPC fallbacks: Base mainnet chain ID, receipt event fields, optional recipient, finalized block depth, canonical block hash, and block timestamp.

`server/api/prismaTicketPurchase.ts` persists only the immutable ticket row required by backend generation. `server/api/backendPlanet.ts` derives the deterministic traits and GIF and upserts one row per ticket purchase. Existing ready rows are returned unchanged; conflicting proof fields fail closed. Collection queries filter by Base mainnet, the active jackpot, and the `MEGASTERA` source tag.

No active module signs vouchers, pins media, reads a Planet contract, projects Planet events, scans all tickets continuously, or writes transfer/accrual ledgers.
No active module signs vouchers, pins media, reads a Planet contract, projects Planet events, scans all tickets continuously, or writes transfer/accrual ledgers. The upgrade endpoint remains server-disabled because it does not yet verify a request-wallet signature.

## Frontend boundaries

`src/pages/Play.tsx` owns checkout and, after canonical receipt recovery in the purchase hooks, requests backend generation with bounded finality retries. The active UI has two stages: Buy tickets and Explore planets. On success it shows the generated cards full screen with `Explore again` and `My planets` actions.

`src/pages/Planets.tsx` merges the backend collection with locally confirmed site receipts and the complete paginated wallet ticket feed. Site tickets are always represented as a Planet or pending card; unmatched wallet tickets are plain ticket cards. It does not derive inventory from chain holdings or expose mint/reveal controls. `src/hooks/useWalletMining.ts` reads the backend wallet snapshot and locally interpolates no persistent state. The `/tickets` surface uses `useJackpotState` for current-drawing status/countdown and the Base mainnet Data API for wallet ticket/win rows; claims call `Jackpot.claimWinnings` directly after simulation.
`src/pages/Planets.tsx` merges the backend collection with locally confirmed site receipts and the complete paginated wallet ticket feed. Site tickets are always represented as a Planet or pending card; unmatched wallet tickets are plain ticket cards. It does not derive inventory from chain holdings or expose mint/reveal controls. `src/hooks/useWalletMining.ts` reads and validates the backend wallet snapshot, including current balance and upgrade state, and locally interpolates no persistent state. Upgrade controls remain hidden because the backend reports `upgradesEnabled: false`. The `/tickets` surface uses `useJackpotState` for current-drawing status/countdown and the Base mainnet Data API for wallet ticket/win rows; claims call `Jackpot.claimWinnings` directly after simulation.

The landing experience may render deterministic planet previews for presentation. Those previews are not the authority for a purchased Planet; verified receipt provenance and the persisted backend record remain authoritative.

Expand All @@ -64,18 +70,20 @@ The `BackendPlanet` model is linked one-to-one to `TicketPurchase` and stores:
- GIF bytes and content hash; and
- ready/failed status plus bounded generation error text.

This worktree additionally stores `upgradeLevel` and `upgradeBonusBps` on `BackendPlanet`, opening/current balances in `MineralAccount`, the authoritative cutover in `MineralEconomyCutover`, and idempotent `(planetId, targetLevel)` charges in `PlanetUpgradePurchase`. V2 calculations use integer micros and keep the V1 opening-balance calculation as the cutover anchor.

Legacy Prisma tables and migrations are retained only for database compatibility. No active code writes the legacy Planet, voucher, artifact, projector, or accrual paths.

## Verification map

Run the repository quality gate from the root:

```text
pnpm db:generate
pnpm db:validate
pnpm lint
pnpm typecheck
pnpm test
pnpm --filter @megaplanets/planet-generator golden
pnpm build
pnpm db:generate
pnpm db:validate
```
Loading
Loading