|
| 1 | +# Ownership Read Model |
| 2 | + |
| 3 | +The ownership read model is the authoritative source of truth for wallet-level key holdings and per-creator holder lists. It is maintained by the indexer and consumed by API endpoints that return holder counts, balances, and holder lists. |
| 4 | + |
| 5 | +## Table Schema |
| 6 | + |
| 7 | +The ownership read model is stored in the `KeyOwnership` table. |
| 8 | + |
| 9 | +| Field | Type | Description | |
| 10 | +|----------------|------------|-----------------------------------------------------------------------------| |
| 11 | +| `id` | `String` | Unique record identifier (cuid). | |
| 12 | +| `ownerAddress` | `String` | Stellar wallet address of the key holder. | |
| 13 | +| `creatorId` | `String` | ID of the creator whose keys are held. | |
| 14 | +| `balance` | `Decimal` | Number of keys currently held. Defaults to `0`. Never goes below `0`. | |
| 15 | +| `createdAt` | `DateTime` | Timestamp when this ownership record was first created. | |
| 16 | +| `updatedAt` | `DateTime` | Timestamp of the most recent balance update (auto-managed by Prisma). | |
| 17 | + |
| 18 | +**Uniqueness constraint:** `(ownerAddress, creatorId)` — one record per wallet per creator. |
| 19 | + |
| 20 | +**Indexes:** `ownerAddress`, `creatorId` — both indexed for efficient lookups by wallet or by creator. |
| 21 | + |
| 22 | +## Update Triggers |
| 23 | + |
| 24 | +The indexer updates the ownership read model in response to three on-chain trade event types: |
| 25 | + |
| 26 | +### Buy |
| 27 | + |
| 28 | +When a wallet purchases keys from a creator: |
| 29 | + |
| 30 | +1. An `upsert` is performed on `(ownerAddress, creatorId)`. |
| 31 | +2. `balance` is incremented by the purchased amount. |
| 32 | +3. If no record exists, one is created with `balance = purchased amount`. |
| 33 | + |
| 34 | +### Sell |
| 35 | + |
| 36 | +When a wallet sells keys back to a creator: |
| 37 | + |
| 38 | +1. The existing `KeyOwnership` record for `(ownerAddress, creatorId)` is located. |
| 39 | +2. `balance` is decremented by the sold amount. |
| 40 | +3. If `balance` reaches `0`, the record is retained at `0` (not deleted) to preserve audit history and simplify replay logic. |
| 41 | + |
| 42 | +### Peer-to-Peer Transfer |
| 43 | + |
| 44 | +When a wallet transfers keys directly to another wallet (without going through the bonding curve): |
| 45 | + |
| 46 | +1. The sender's `KeyOwnership` record is decremented by the transferred amount. |
| 47 | +2. The recipient's `KeyOwnership` record is incremented by the same amount (upserted if it does not exist). |
| 48 | +3. Both updates are applied atomically where possible to prevent intermediate inconsistent states. |
| 49 | + |
| 50 | +## Balance Conservation Invariant |
| 51 | + |
| 52 | +At any point in time, the sum of all `balance` values across every `KeyOwnership` record for a given `creatorId` must equal that creator's total key supply as recorded on-chain: |
| 53 | + |
| 54 | +``` |
| 55 | +∑ balance(ownerAddress, creatorId) = creatorTotalSupply(creatorId) |
| 56 | +``` |
| 57 | + |
| 58 | +This invariant must hold after every trade event is processed. Any discrepancy indicates a missed or double-processed event and should trigger a reconciliation replay. |
| 59 | + |
| 60 | +## Replay and Consistency Recovery |
| 61 | + |
| 62 | +If the indexer misses one or more on-chain events (due to a crash, network gap, or RPC timeout), the ownership read model can fall out of sync with the chain state. |
| 63 | + |
| 64 | +**Replay procedure:** |
| 65 | + |
| 66 | +1. The admin replay endpoint (`POST /api/v1/admin/replay`) re-fetches the affected ledger range from the Stellar RPC and re-emits all trade events in order. |
| 67 | +2. Each event is processed with idempotency guards: an event with a ledger sequence already recorded is skipped without modifying the read model. |
| 68 | +3. After replay completes, the balance conservation invariant is re-validated. If the sum of balances still does not match the on-chain supply, the replay window is widened and the process repeats. |
| 69 | +4. Replay is safe to run at any time because all write paths are idempotent — re-processing a seen event produces no side effects. |
| 70 | + |
| 71 | +Gaps detected by the ledger gap detection service (`LedgerGapDetectionService`) are automatically flagged and can trigger a targeted replay without requiring a full historical re-index. |
0 commit comments