docs: add Solisting Explorer app design spec

This commit is contained in:
thesn10
2026-06-22 22:54:18 +02:00
parent 97403174b4
commit 7c91d2c52b

View File

@@ -0,0 +1,301 @@
# Solisting Explorer App — Design Spec
**Date:** 2026-06-22
**Status:** Approved
## Overview
Build a real Web3 app (`app/`) and a Codama-generated SDK (`sdk/`) for the Solisting protocol. The app is a full explorer + action interface that visually matches the existing prototype in `prototype-app/`. Write transactions are fully implemented — all 7 solisting instructions plus descro-side actions (complete, dispute, resolve via `@descro/sdk`).
---
## 1. Project Structure
```
solisting/
├── sdk/ # yarn workspace package: @solisting/sdk
│ ├── src/
│ │ ├── idl/
│ │ │ └── solisting.json # Anchor-generated IDL (anchor idl build -p solisting)
│ │ ├── generated/solisting/ # Codama output (accounts, instructions, pdas, types)
│ │ ├── pda.ts # findListingPda, findOrderPda
│ │ ├── listing.ts # fetchAllListings, fetchListingsBySeller
│ │ ├── order.ts # fetchOrdersForListing, fetchOrdersByBuyer
│ │ └── index.ts
│ ├── codama.solisting.json
│ └── package.json # name: "@solisting/sdk"
└── app/ # Next.js 16 App Router
├── src/
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── page.tsx # redirect → /listings
│ │ ├── listings/page.tsx
│ │ ├── listing/[pk]/page.tsx
│ │ ├── listing/create/page.tsx
│ │ ├── order/[pk]/page.tsx
│ │ ├── escrows/page.tsx
│ │ ├── resolvers/page.tsx
│ │ ├── resolver/[pk]/page.tsx
│ │ └── dashboard/page.tsx
│ ├── components/
│ │ ├── connector/ # Copied connectorkit base-ui components
│ │ │ ├── connect-button.tsx
│ │ │ ├── wallet-modal.tsx
│ │ │ └── wallet-dropdown-content.tsx
│ │ ├── ui/ # Shared primitives: Button, Badge, FieldRow, Toast
│ │ ├── Nav.tsx
│ │ ├── ListingsTable.tsx
│ │ ├── ListingDetail.tsx
│ │ ├── OrderDetail.tsx
│ │ ├── EscrowStateMachine.tsx
│ │ ├── EscrowsTable.tsx
│ │ ├── ResolverGrid.tsx
│ │ ├── ResolverDetail.tsx
│ │ ├── Dashboard.tsx
│ │ └── CreateListingForm.tsx
│ ├── hooks/
│ │ ├── useListings.ts
│ │ ├── useListing.ts
│ │ ├── useOrdersForListing.ts
│ │ ├── useOrder.ts
│ │ ├── useEscrows.ts
│ │ ├── useResolvers.ts
│ │ ├── useMyListings.ts
│ │ ├── useMyOrders.ts
│ │ └── useTx.ts # Generic send-tx + cache invalidation helper
│ ├── lib/
│ │ ├── rpc.ts # createRpc factory, QueryClient singleton
│ │ └── format.ts # fmtSol, fmtTok, relTime, abbrev, priceStr
│ ├── providers/
│ │ └── WalletProviders.tsx # AppProvider + QueryClientProvider
│ └── styles/
│ └── globals.css # CSS custom properties + font imports
└── package.json
```
---
## 2. SDK Design
### IDL Generation
```bash
anchor idl build -p solisting
# outputs: target/idl/solisting.json
# copy to: sdk/src/idl/solisting.json
```
### Codama Config (`codama.solisting.json`)
```json
{
"idl": "./src/idl/solisting.json",
"scripts": {
"js": [{ "from": "@codama/renderers-js", "args": ["./src/generated/solisting"] }]
}
}
```
### Codama-Generated Output
- `generated/solisting/accounts/``fetchListingAccount`, `fetchOrderAccount`, typed decoders
- `generated/solisting/instructions/``getCreateListingInstruction`, `getUpdateListingInstruction`, `getCloseListingInstruction`, `getCreateOrderInstruction`, `getAcceptOrderInstruction`, `getRejectOrderInstruction`, `getCancelOrderInstruction`, `getCloseStaleOrderInstruction`
- `generated/solisting/pdas/``findListingAccountPda`, `findOrderAccountPda`
- `generated/solisting/types/``Currency`, `AltCurrencyConfig`
### Hand-Written Helpers
```ts
// pda.ts
export function findListingPda(seller: Address, listingId: bigint): PdaResult
export function findOrderPda(listing: Address, buyer: Address, orderId: bigint): PdaResult
// listing.ts
export async function fetchAllListings(rpc: Rpc): Promise<ListingAccountWithPda[]>
export async function fetchListingsBySeller(rpc: Rpc, seller: Address): Promise<ListingAccountWithPda[]>
// order.ts
export async function fetchOrdersForListing(rpc: Rpc, listingPk: Address): Promise<OrderAccountWithPda[]>
export async function fetchOrdersByBuyer(rpc: Rpc, buyer: Address): Promise<OrderAccountWithPda[]>
export function deriveEscrowId(orderPda: Address): bigint // u64::from_le_bytes(pda.bytes[0..8])
```
### Local Dependency for @descro/sdk
Both `sdk/package.json` and `app/package.json` reference `@descro/sdk` via local path (not npm):
```json
"@descro/sdk": "file:../../descro/sdk"
```
---
## 3. App Architecture
### Technology Stack
| Concern | Choice |
|---|---|
| Framework | Next.js 16 App Router, all pages `'use client'` |
| Solana RPC | `@solana/kit` (`createSolanaRpc`) |
| Wallet | `@solana/connector` + `@solana/connector/react` |
| Wallet UI | connectorkit base-ui components (copied into project) |
| Data fetching | `@tanstack/react-query` — 30s poll for lists, 15s poll for detail views |
| Codama SDK | `@solisting/sdk` via `workspace:*` |
| Descro SDK | `@descro/sdk` via `file:../../descro/sdk` |
| UI components | Headless `@base-ui/react` for interactive primitives |
| Styling | CSS custom properties + inline styles (no Tailwind for own components) |
| Fonts | Space Grotesk (body) + JetBrains Mono (addresses/code) via Google Fonts |
### Routes
| Route | View | Auth |
|---|---|---|
| `/listings` | All ListingAccounts table, filter by status/seller | Public |
| `/listing/[pk]` | Listing detail: fields, qty bar, orders table, seller actions | Public; actions wallet-gated |
| `/listing/create` | Create listing form (`?edit=<pk>` for update flow) | Wallet required |
| `/order/[pk]` | OrderAccount + EscrowAccount side-by-side + state machine + actions | Public; actions wallet-gated |
| `/escrows` | All descro EscrowAccounts, filter by state | Public |
| `/resolvers` | ResolverEntry grid (2 columns) | Public |
| `/resolver/[pk]` | Resolver fields + dispute statistics bars | Public |
| `/dashboard` | Wallet-gated: My Listings tab + My Orders tab + Create button | Wallet required |
### React Query Hooks
All hooks take no arguments beyond the address/pk they fetch:
```ts
useListings() // polls 30s — all ListingAccounts via getProgramAccounts
useListing(pk) // polls 15s — single ListingAccount + orders
useOrdersForListing(pk) // polls 15s — filters orders by listing field
useOrder(pk) // polls 15s — OrderAccount + linked EscrowAccount from descro
useEscrows() // polls 30s — all descro EscrowAccounts
useResolvers() // polls 30s — all ResolverEntries from descro_ext_resolvers
useMyListings() // filters useListings() by connected wallet address
useMyOrders() // all orders where buyer === connected wallet
```
### Write Transactions
A `useTx` hook wraps the send → toast → invalidate cycle:
```ts
function useTx() {
const { sendTransaction } = useWallet()
const queryClient = useQueryClient()
return async (ix: IInstruction, invalidateKeys: string[][]) => {
// build + sign + send via @solana/kit
// show success toast
// queryClient.invalidateQueries for each key
}
}
```
Implemented instructions:
| Instruction | Triggered from | Cache keys invalidated |
|---|---|---|
| `create_listing` | Create form | `['listings']` |
| `update_listing` | Listing detail (seller) | `['listing', pk]`, `['listings']` |
| `close_listing` | Listing detail (seller) | `['listing', pk]`, `['listings']` |
| `create_order` | Listing detail (buyer) | `['listing', pk]`, `['orders', pk]` |
| `accept_order` | Order detail (seller) | `['order', pk]`, `['listing', listingPk]` |
| `reject_order` | Order detail (seller) | `['order', pk]`, `['listing', listingPk]` |
| `cancel_order` | Order detail (buyer) | `['order', pk]`, `['listing', listingPk]` |
| `close_stale_order` | Order detail (permissionless) | `['order', pk]`, `['listing', listingPk]` |
| descro `complete` | Order detail (buyer) | `['order', pk]` |
| descro `dispute` | Order detail (buyer/seller) | `['order', pk]` |
| descro `resolve` | Order detail (resolver) | `['order', pk]` |
---
## 4. Styling
### CSS Custom Properties
Single theme (Nebula dark), no theme toggle needed:
```css
:root {
--bg: oklch(0.165 0.022 282);
--bg2: oklch(0.205 0.026 282);
--bg3: oklch(0.25 0.03 283);
--bd: oklch(0.33 0.032 286);
--bdSoft: oklch(0.27 0.028 284);
--navbg: oklch(0.18 0.024 282 / .82);
--tx: oklch(0.96 0.008 285);
--mut: oklch(0.68 0.022 284);
--radius: 13px;
--acc: #9945FF;
--acc2: #14F195;
--acc2light: #7df5c4;
--accGlow: rgba(153,69,255,.55);
--accSoft: rgba(153,69,255,.13);
--accBd: rgba(153,69,255,.4);
--danger: #FF7A59;
--dangerSoft: rgba(255,122,89,.12);
}
```
### Component Styling Strategy
- **Own components**: inline styles + CSS variables, no Tailwind — mirrors the prototype exactly
- **Connectorkit components**: copied from `solana-foundation/connectorkit` `examples/next-js/components/connector/base-ui/`, Tailwind classes adapted to use `--acc`/`--acc2` variables
- **Base-UI usage**: `Menu` for wallet dropdown + network picker, `Dialog` for WalletModal — all styled with inline styles and CSS variables
### Fonts
```html
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
```
Applied via CSS variables:
- `--font-body: 'Space Grotesk', system-ui, sans-serif`
- `--font-mono: 'JetBrains Mono', monospace`
---
## 5. Wallet & Network Integration
```tsx
// providers/WalletProviders.tsx
<AppProvider connectorConfig={getDefaultConfig({
appName: 'Solisting Explorer',
appUrl: origin,
autoConnect: true,
clusters: [
{ id: 'solana:localnet', label: 'Localnet', url: 'http://localhost:8899' },
{ id: 'solana:devnet', label: 'Devnet', url: 'https://api.devnet.solana.com' },
{ id: 'solana:mainnet', label: 'Mainnet', url: 'https://api.mainnet-beta.solana.com' },
]
})} mobile={getDefaultMobileConfig(...)}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</AppProvider>
```
Navbar uses:
- `<ConnectButton />` — copied connectorkit component, shows wallet address when connected
- Network shown inline as a custom dropdown (using `useCluster()` from `@solana/connector/react`)
---
## 6. Global Search
The search bar (top nav) works client-side against cached React Query data. On Enter:
1. Check if input is a valid base58 pubkey → try to find it in listings, orders, escrows, resolvers caches
2. If found → navigate to the relevant detail page
3. If not found → navigate to a `/search?q=<input>` "not found" page
No separate search RPC call needed as long as list data is already cached.
---
## 7. Key Constraints
- `Currency::Spl` payments in `create_order` are rejected by the program (`SplNotImplemented`) — the UI shows SPL fields in the Create Listing form but hides the "Place Order" button when payment currency is SPL-only, showing a tooltip instead.
- `escrow_id` derivation: `u64::from_le_bytes(orderPda.toBytes().slice(0, 8))` — must match program exactly.
- Oracle accounts passed to `create_order` must match `listing.canonical_oracle` / `alt_cfg.usd_oracle` — the frontend reads these from the ListingAccount and passes them verbatim.
- The descro program key (`DjVR4EuYV6USMJFfsGZwhZ3y8rtWsmG8EvDY96GTqqi3`) is verified by solisting on every CPI — hardcoded in `@descro/sdk`'s `DESCRO_PROGRAM_ADDRESS`.