feat(fee+burn+essence): 5% transparent fee, burn→close, Essence ledger + dashboard

Monetization (design Rev 4, §3.1) — transparent in-tx fee, non-custodial:
- @pyre/core: computeFeeBreakdown (single source of truth, BigInt) + FeeBreakdown
  threaded through close/burn previews; fee tests.
- @pyre/config: PYRE_TREASURY_WALLET / PYRE_FEE_BPS (500) / swap fee / max contribution.
- @pyre/solana: close-empty + burn→close now append ONE System transfer of exactly
  the disclosed fee to the treasury; rent/authority/feePayer pinned to wallet.
  buildBurnTx re-validates EVERY account on-chain and value-gates via the classifier
  (classic SPL + Token-2022) — never burns protected/valuable/NFT/unsupported;
  ignores client amount (burns real balance); whole-build rejection.
- @pyre/api: close-empty/burn endpoints carry the fee + bounded optional contribution;
  /api/receipt persists (cleanup_receipts) and records the on-chain treasury fee as
  Essence; GET /api/essence; startup migrate(). Best-effort DB (never fails receipts).
- @pyre/db: Postgres Essence ledger (rounds, cleanup_receipts, essence_contributions),
  idempotent migrations, parameterized + u64-safe.
- @pyre/web: fee preview ("reclaim · feeds the PYRE · you net" + treasury) + optional
  "feed more" slider; burn flow w/ destructive confirm; decode+match verifies the fee
  transfer (treasury + exact lamports) before signing; public "🔥 fed the PYRE" panel.

Built by agents (2 waves) + 2 audits. Security audit found a HIGH — buildBurnTx
didn't value-gate CLASSIC spl tokens (a direct API caller could burn USDC/an NFT);
FIXED (classify classic accounts too) + 2 regression tests. Integration: SHIP.
typecheck 8/8, core 91, solana 30, web build green. Live: burn preview on the dust
token shows 5% → treasury; non-empty/non-owned/valuable rejected. Nightly DB backup
cron enabled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 06:11:00 +00:00
parent f9c471ef71
commit b98b904896
22 changed files with 3115 additions and 182 deletions

View File

@@ -1,44 +1,121 @@
# @pyre/db
Database schema, migrations, and table definitions for PYRE (PostgreSQL).
Postgres-backed **Essence ledger** for PYRE. A small typed data layer over
`pg` (no ORM): a lazily-created connection pool, an idempotent migration
runner, and the round / receipt / contribution query surface.
## Purpose
## Trust rules
Per §13: the schema, migrations, and table definitions. Uses `pg` for
connectivity. Connection details come from `DATABASE_URL` via `@pyre/config`
**never** hardcode credentials.
- **Connection details come from the environment** (`DATABASE_URL`) or an
explicit argument — credentials are **never** hardcoded. The localhost dev URL
is only a last-resort fallback.
- **Recovered ATA rent is not Essence.** `cleanup_receipts` records rent
returned to the user; it never touches a round total. Only
`essence_contributions` (the protocol fee and explicit opt-in contributions)
feed `rounds.essence_lamports`.
- **Parameterized queries only** (`$1`, `$2`, …) — no string interpolation.
- Lamport amounts cross the API boundary as **decimal strings** (u64-safe) and
are cast to `::bigint` in SQL.
- **No network/DB access at import time.** The pool is created lazily;
`migrate()` is safe to call repeatedly.
## Tables (§15)
## Tables
### Initial MVP tables
Defined in `migrations/001_init.sql` (idempotent, `CREATE TABLE IF NOT EXISTS`).
- `wallet_scans` — id, wallet, status, created_at, completed_at, summary_json
- `token_accounts` — id, scan_id, wallet, ata, mint, token_program, raw_balance,
ui_balance, decimals, symbol, name, classification, warnings_json,
estimated_rent_lamports, created_at
- `cleanup_receipts` — id, wallet, scan_id, tx_signature, rent_returned_lamports,
closed_accounts_count, burned_tokens_count, status, created_at, receipt_json
- `prometheus_generations` — id, receipt_id, input_json, output_json, status,
risk_flags_json, created_at, approved_at, rejected_at
- `spawn_records` — id, generation_id, spawn_name, ticker, mint, metadata_uri,
pumpfun_url, launch_tx, status, created_at
### `rounds`
### Future tables
| column | type | notes |
| ------------------ | ------------- | --------------------------------------- |
| `id` | `BIGSERIAL` | primary key |
| `status` | `TEXT` | `'open' \| 'closed'`, default `'open'` |
| `essence_lamports` | `BIGINT` | running round total, default `0` |
| `started_at` | `TIMESTAMPTZ` | default `now()` |
| `closed_at` | `TIMESTAMPTZ` | nullable |
- `token_classifications`
- `burn_events`
- `close_account_events`
- `spawn_candidates`
- `system_events`
### `cleanup_receipts`
## Status
| column | type | notes |
| ------------------------ | ------------- | -------------------------------------- |
| `id` | `BIGSERIAL` | primary key |
| `wallet` | `TEXT` | |
| `tx_signature` | `TEXT` | **unique** (idempotency key) |
| `kind` | `TEXT` | `'close' \| 'burn'` |
| `rent_returned_lamports` | `BIGINT` | rent returned to the user |
| `fee_lamports` | `BIGINT` | protocol fee, default `0` |
| `closed_accounts` | `JSONB` | array of addresses, default `'[]'` |
| `created_at` | `TIMESTAMPTZ` | default `now()` |
**Skeleton.** Exports table-name constants and a connection-factory stub. No
queries, no schema DDL, no migrations yet.
Index: `cleanup_receipts(wallet)`.
## TODO
### `essence_contributions`
- Implement the `createPool()` connection factory (read `DATABASE_URL` via
`@pyre/config`).
- Add SQL migrations under `migrations/` and a migration runner.
- Add typed table definitions and a query layer.
| column | type | notes |
| -------------- | ------------- | -------------------------------------- |
| `id` | `BIGSERIAL` | primary key |
| `round_id` | `BIGINT` | FK → `rounds(id)` |
| `wallet` | `TEXT` | |
| `tx_signature` | `TEXT` | **unique** (idempotency key) |
| `lamports` | `BIGINT` | amount fed to the PYRE |
| `kind` | `TEXT` | `'fee' \| 'contribution'` |
| `created_at` | `TIMESTAMPTZ` | default `now()` |
Index: `essence_contributions(round_id)`.
## API
```ts
import {
getPool,
migrate,
ensureOpenRound,
recordReceipt,
recordEssence,
getEssenceSummary,
closePool,
} from "@pyre/db";
```
- `getPool(databaseUrl?): Pool` — lazily create and cache the singleton
`pg.Pool`. Connection string resolves to the explicit argument, then
`DATABASE_URL`, then the localhost dev default. No connection is opened until
first query.
- `migrate(): Promise<void>` — apply every `migrations/*.sql` in name order,
each in its own transaction. Idempotent; safe to call repeatedly.
- `ensureOpenRound(): Promise<{ id: string }>` — return the current open round,
creating one if none exists.
- `recordReceipt(r): Promise<void>` — insert a cleanup receipt
(`{ wallet, txSignature, kind: 'close'|'burn', rentReturnedLamports,
feeLamports, closedAccounts }`). Idempotent on `txSignature`. Does **not**
affect any round total.
- `recordEssence(e): Promise<{ recorded, roundId }>` — record a contribution
(`{ wallet, txSignature, lamports, kind: 'fee'|'contribution' }`) against the
open round. In one transaction: ensures a round, inserts (idempotent on
`txSignature`), and increments `rounds.essence_lamports` **only** when a new
row is inserted. Returns `recorded: false` for duplicate signatures.
- `getEssenceSummary(): Promise<EssenceSummary>` — open-round
`{ roundId, totalLamports, contributionCount, recent }`, where `recent` is the
last ~10 contributions (newest first).
- `closePool(): Promise<void>` — close and clear the pool for shutdown / test
teardown.
All lamport amounts are **strings** in and out.
## Usage
```ts
await migrate();
await recordEssence({
wallet: "Wallet…",
txSignature: "Sig…",
lamports: "1000000",
kind: "fee",
});
const summary = await getEssenceSummary();
```
## Migrations
SQL lives in `migrations/`, one forward migration per file in lexical order
(`001_init.sql`, `002_…sql`, …). Each file must be idempotent. The runner
(`migrate()`) applies them in name order against the resolved connection.