From 7c91d2c52b39c90b1bff9dc76c764bb548807ffc Mon Sep 17 00:00:00 2001 From: thesn10 <38666407+thesn10@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:54:18 +0200 Subject: [PATCH] docs: add Solisting Explorer app design spec --- ...026-06-22-solisting-explorer-app-design.md | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-22-solisting-explorer-app-design.md diff --git a/docs/superpowers/specs/2026-06-22-solisting-explorer-app-design.md b/docs/superpowers/specs/2026-06-22-solisting-explorer-app-design.md new file mode 100644 index 0000000..ce0c7a5 --- /dev/null +++ b/docs/superpowers/specs/2026-06-22-solisting-explorer-app-design.md @@ -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 +export async function fetchListingsBySeller(rpc: Rpc, seller: Address): Promise + +// order.ts +export async function fetchOrdersForListing(rpc: Rpc, listingPk: Address): Promise +export async function fetchOrdersByBuyer(rpc: Rpc, buyer: Address): Promise +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=` 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 + +``` + +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 + + + {children} + + +``` + +Navbar uses: +- `` — 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=` "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`.