Files
solisting/docs/superpowers/specs/2026-06-22-solisting-explorer-app-design.md
2026-06-22 22:54:18 +02:00

12 KiB

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

anchor idl build -p solisting
# outputs: target/idl/solisting.json
# copy to: sdk/src/idl/solisting.json

Codama Config (codama.solisting.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

// 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):

"@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:

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:

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:

: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

<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

// 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)

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.