# 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`.