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 decodersgenerated/solisting/instructions/—getCreateListingInstruction,getUpdateListingInstruction,getCloseListingInstruction,getCreateOrderInstruction,getAcceptOrderInstruction,getRejectOrderInstruction,getCancelOrderInstruction,getCloseStaleOrderInstructiongenerated/solisting/pdas/—findListingAccountPda,findOrderAccountPdagenerated/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/connectorkitexamples/next-js/components/connector/base-ui/, Tailwind classes adapted to use--acc/--acc2variables - Base-UI usage:
Menufor wallet dropdown + network picker,Dialogfor 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)
6. Global Search
The search bar (top nav) works client-side against cached React Query data. On Enter:
- Check if input is a valid base58 pubkey → try to find it in listings, orders, escrows, resolvers caches
- If found → navigate to the relevant detail page
- 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::Splpayments increate_orderare 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_idderivation:u64::from_le_bytes(orderPda.toBytes().slice(0, 8))— must match program exactly.- Oracle accounts passed to
create_ordermust matchlisting.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'sDESCRO_PROGRAM_ADDRESS.