Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
73 changes: 73 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,79 @@ decides an outcome.**
`marketStatus.tag` is `Open | Resolved | Void`. `404 MARKET_NOT_FOUND`,
`409 MARKET_NOT_ON_CHAIN`, `502 CHAIN_READ_FAILED`.


### Treasury admin console (auth, admin)

The backend never holds the treasury admin key. It prepares unsigned,
simulated transactions for the on-chain admin/multisig, verifies the signed
XDR body against the prepared hash, and submits only when the collected
signature weight reaches the account's high threshold. Every state change
writes an `AgentLog` row (`ADMIN_OP_<STATE>`).

#### `GET /admin/treasury`
Returns treasury `RiskConfig`, available capital, USDC balance, daily usage
with headroom/reset time, treasury/market-core paused flags and admins, and
the agent's XLM fee balance. The on-chain admin is read from treasury instance
storage until the contract exposes `admin()`/`is_paused()`.

#### `GET /admin/treasury/operations?page&limit`
Paginated prepared/signed/submitted operations.

#### `POST /admin/treasury/operations`
Body: `{ "operation": { ... }, "force"?: boolean }`, where `operation` is one of:

- `{ "kind": "treasury.set_paused", "paused": true }`
- `{ "kind": "treasury.withdraw", "to": "G…|C…", "amountRaw": "12500000000" }`
- `{ "kind": "treasury.deposit", "from": "G…", "amountRaw": "100000000" }`
- `{ "kind": "treasury.collect_market_fees", "onChainMarketId": "7" }`
- `{ "kind": "market_core.set_paused", "paused": false }`

Returns `{ id, xdr, networkPassphrase, hash, expiresAt, requiredSigners,
thresholds, checks, humanSummary, status }`. Pre-flight failures return
`422 PREFLIGHT_FAILED` with `details.checks`. A `C…` admin returns
`501 UNSUPPORTED_ADMIN_KIND` (auth-entry signing is out of scope).

#### `POST /admin/treasury/operations/:id/signatures`
Body `{ "signedXdr": "…" }`. The envelope hash must equal the stored `txHash`;
new signatures are verified against the admin account's signers, duplicates are
ignored, and `weightCollected` is recomputed. `READY` means the high threshold
(and optional `TREASURY_WITHDRAW_MIN_SIGNERS`) is met.

#### `POST /admin/treasury/operations/:id/submit`
Allowed only in `READY` and before `expiresAt`, testnet only. Sends the
envelope, polls `getTransaction`, and records `SUCCESS`/`FAILED`, ledger and
result XDR. No server key is used.

#### `POST /admin/treasury/operations/:id/record`
Body `{ "txHash": "<64 hex>" }` for clients that submitted themselves.

#### `POST /admin/treasury/operations/:id/cancel`
Cancels a prepared/partially-signed operation.

### Transparency reports

Public, computed only from chain state and `chain_events`; see
`docs/transparency-reports.md`.

#### `GET /reports?page&limit`
Paginated index: `date`, `anchorLedger`, `cid`, `ipfsUri`, `hash`,
`onChainTraceId`, `mode` (`live`/`dry-run`), `consistency`
(`exact`/`best_effort`), the report document, and a verification recipe.

#### `GET /reports/latest`
Most recent report (404 if none yet).

#### `GET /reports/:date`
One report by `YYYY-MM-DD` (400 for a malformed date, 404 if absent).

`GET /portfolio` also includes `latestReport` with the same metadata.

### `POST /traces/verify` content source

The response now includes `contentSource: "cache" | "gateway"`: `cache` means
the bytes came from the local verified content store and both gateways could
be down. The on-chain `trace_hash` remains the authority.

## Realtime (socket.io)

Same origin as the API without `/api/v1`. The frontend listens to these
Expand Down
150 changes: 150 additions & 0 deletions docs/transparency-reports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Transparency reports v1

OracleDesk publishes a daily report computed **only from chain data** and
`chain_events`, anchors it to a ledger, pins the canonical JSON to IPFS, and
commits its sha256 through the reasoning registry. The public index serves the
CID, hash, on-chain trace id and anchor; `scripts/verify-report.ts` re-checks
them without database access.

## Document

```ts
interface TransparencyReportV1 {
kind: 'oracledesk.transparency-report';
schemaVersion: 1;
network: string; // Stellar passphrase
contracts: { treasury; marketCore; usdc; reasoningRegistry };
agent: string; // treasury RiskConfig.agent
period: { from: string; to: string }; // UTC day, ISO
anchor: { ledger: number; closedAt: string };
consistency: 'exact' | 'best_effort';
treasury: {
usdcBalanceRaw: string;
availableCapitalRaw: string;
riskConfig: { maxTradeRaw; maxMarketRaw; maxDailyRaw };
dailyUsage: { amountRaw: string; dayStart: number };
};
markets: Array<{
marketId: string;
status: 'Open' | 'Resolved:Yes' | 'Resolved:No' | 'Void';
createdByTreasury: boolean;
position: { yesRaw: string; noRaw: string };
reserves: { yesRaw: string; noRaw: string };
priceYesBps: number;
markValueRaw: string;
liquidationValueRaw: string;
settlementValueRaw: string | null;
lpRemainderRaw?: string; // final, unclaimed treasury-created pools
archived?: boolean;
}>;
totals: { positionsMarkRaw; positionsLiquidationRaw; navRaw };
period_flows: {
coverage: { fromLedger; toLedger; complete; gaps: Array<[number, number]> };
tradesBoughtRaw; tradesSoldRaw; feesPaidRaw; redeemedRaw;
feesCollectedRaw; seedDeployedRaw; tradeCount;
};
traces: { total: number; byAction: Record<string, number> };
method: { version: '1'; doc: string };
previous: { onChainTraceId: string; traceHash: string } | null;
}
```

All amounts are integer strings in 7-decimal USDC base units. No Prisma float
model is imported by the report service (CI asserts the import boundary).

## Integers and valuations

`BPS = 10_000`. Let `p = priceYesBps(reserve_yes, reserve_no)` from
`src/generated/fpmm.ts`.

- `markValueRaw = floor((yes × p + no × (BPS − p)) / BPS)`
- `liquidationValueRaw = collateralOutForShares(reserve_yes, reserve_no, yes, fee_bps)
+ collateralOutForShares(reserve_no, reserve_yes, no, fee_bps)`
- `settlementValueRaw = settleValue(kind, yes, no)` for final markets, where
`kind` is `yes`, `no` or `void`.
- `lpRemainderRaw = settleValue(kind, reserve_yes, reserve_no)` for final,
unclaimed (`lp_claimed === false`) pools created by the treasury.
- `totals.positionsMarkRaw = Σ markValueRaw`;
`positionsLiquidationRaw = Σ liquidationValueRaw`;
`navRaw = usdcBalanceRaw + positionsMarkRaw`.

## Snapshot

Simulations run against the RPC's latest state; Soroban cannot pin a
historical ledger. The builder therefore:

1. reads `getLatestLedger()` → `L0`;
2. enumerates `0..marketCount()` and reads each market + treasury position;
3. reads `getLatestLedger()` → `L1`;
4. if `L1 > L0`, retries up to 3 times; after the third attempt the report is
still published with `consistency: 'best_effort'` and `anchor.ledger = L1`.

The normal case is `consistency: 'exact'`. Entries that can no longer be read
are reported as `archived: true` rather than treated as zero.

## Period flows

Events are `chain_events` rows whose `ledgerClosedAt` falls in the UTC day:

- market-core `Trade` with `trader = treasury`: buys/sells (`is_buy`),
`fee`, and `tradeCount`;
- market-core `Redeemed` with `holder = treasury`;
- treasury `FeesCollected`;
- market-core `MarketCreated` with `creator = treasury` (`seed_amount`).

`coverage.complete` is false when the stored indexer cursor is older than the
last ledger in the period; the missing range is listed in `coverage.gaps`.
RPC retention means an indexer that did not run continuously cannot recover
those events.

## Trace counts

The report walks `traceCount()`/`get_trace(i)` and caches immutable trace
metadata in `registry_trace_cache`. Only traces whose `agent` equals the
treasury's `RiskConfig.agent` are counted in `traces.total` and
`traces.byAction`.

## Sentinel market id

`publish_trace` requires a `market_id`. Reports use
`REPORT_MARKET_ID = 18446744073709551615` (`u64::MAX`) and action `"report"`.
`market_trace_count(REPORT_MARKET_ID)` / `market_trace_at` therefore form an
on-chain index of reports. A dedicated `publish_report` entrypoint is requested
in `docs/contract-requests.md`.

## Publishing and idempotency

`REPORTS_ENABLED=true` schedules `REPORT_CRON` (default `10 0 * * *`,
`Etc/UTC`). The job:

1. builds the document for the UTC day;
2. canonicalises and pins it (`pinJson`);
3. publishes `trace_hash`/`ipfs_cid` via `publish_trace` (respecting
`CHAIN_EXECUTION_MODE`; `REPORT_MODE=dry-run` is also supported);
4. inserts or updates `transparency_reports`.

If a row exists without `onChainTraceId` (crash between pin and publish, or a
dry run being retried), the job re-publishes the **stored bytes** — it never
recomputes. Dry-run rows have `mode = 'dry-run'` and `onChainTraceId = null`.

## Verification

```bash
npm run verify:report -- --date 2026-10-14 [--rpc URL] [--gateway URL]
npm run verify:report -- --trace-id 42
```

The verifier uses only `@stellar/stellar-sdk` and the generated bindings:

1. walks `market_trace_count(u64::MAX)` / `market_trace_at` (or uses
`--trace-id`) and parses each CID until the report's `period.from` matches
the date;
2. fetches the CID from the gateway, computes sha256 and compares it with the
on-chain `trace_hash`;
3. checks the trace agent equals the report agent and the treasury
`RiskConfig.agent`;
4. recomputes `totals.positionsMarkRaw` and `navRaw`;
5. follows `previous` and checks the previous trace hash/action;
6. reports whether the state moved since the anchor ledger.

It exits non-zero on any failed check.
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
-- #100 local verified IPFS content store
CREATE TABLE "ipfs_content" (
"cid" TEXT NOT NULL,
"sha256" TEXT NOT NULL,
"size" INTEGER NOT NULL,
"bytes" BYTEA NOT NULL,
"origin" TEXT NOT NULL,
"verifiedBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastAccessAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"pinnedUntil" TIMESTAMP(3),

CONSTRAINT "ipfs_content_pkey" PRIMARY KEY ("cid")
);

CREATE INDEX "ipfs_content_sha256_idx" ON "ipfs_content"("sha256");
CREATE INDEX "ipfs_content_lastAccessAt_idx" ON "ipfs_content"("lastAccessAt");

-- Defence in depth: the application checks both, but a direct writer cannot
-- store a row whose declared size/hash disagree with the bytes. A CID may only
-- ever map to one byte string (enforced by the primary key + put()).
ALTER TABLE "ipfs_content"
ADD CONSTRAINT "ipfs_content_size_matches_bytes" CHECK ("size" = octet_length("bytes"));
ALTER TABLE "ipfs_content"
ADD CONSTRAINT "ipfs_content_sha256_format" CHECK ("sha256" ~ '^[0-9a-f]{64}$');

-- #156 treasury admin operations console
CREATE TABLE "admin_operations" (
"id" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"params" JSONB NOT NULL,
"contractId" TEXT NOT NULL,
"sourceAccount" TEXT NOT NULL,
"unsignedXdr" TEXT NOT NULL,
"txHash" TEXT NOT NULL,
"signedXdr" TEXT,
"signatures" JSONB NOT NULL DEFAULT '[]'::jsonb,
"weightCollected" INTEGER NOT NULL DEFAULT 0,
"status" TEXT NOT NULL,
"resultXdr" TEXT,
"ledger" INTEGER,
"error" JSONB,
"requestedBy" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "admin_operations_pkey" PRIMARY KEY ("id")
);

CREATE UNIQUE INDEX "admin_operations_txHash_key" ON "admin_operations"("txHash");
CREATE INDEX "admin_operations_status_createdAt_idx" ON "admin_operations"("status", "createdAt");

-- #159 daily transparency reports
CREATE TABLE "transparency_reports" (
"id" TEXT NOT NULL,
"date" TEXT NOT NULL,
"anchorLedger" INTEGER NOT NULL,
"cid" TEXT NOT NULL,
"hash" TEXT NOT NULL,
"onChainTraceId" BIGINT,
"mode" TEXT NOT NULL,
"consistency" TEXT NOT NULL,
"report" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "transparency_reports_pkey" PRIMARY KEY ("id")
);

CREATE UNIQUE INDEX "transparency_reports_date_key" ON "transparency_reports"("date");
CREATE INDEX "transparency_reports_anchorLedger_idx" ON "transparency_reports"("anchorLedger");

-- #159 immutable registry trace cache
CREATE TABLE "registry_trace_cache" (
"traceId" BIGINT NOT NULL,
"agent" TEXT NOT NULL,
"marketId" BIGINT NOT NULL,
"action" TEXT NOT NULL,
"traceHash" TEXT NOT NULL,
"ipfsCid" TEXT NOT NULL,
"publishedAt" BIGINT NOT NULL,
"fetchedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "registry_trace_cache_pkey" PRIMARY KEY ("traceId")
);

CREATE INDEX "registry_trace_cache_agent_action_idx" ON "registry_trace_cache"("agent", "action");
Loading