docs: add Solisting Explorer app design spec
This commit is contained in:
@@ -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`.
|
||||
Reference in New Issue
Block a user