13 KiB
Solisting SDK Implementation Plan (Part 1 of 4)
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Generate the Anchor IDL, run Codama to produce TypeScript bindings, and write hand-crafted SDK helpers for the @solisting/sdk yarn workspace package.
Architecture: The SDK mirrors the @descro/sdk pattern: Codama renders the IDL into typed account decoders and instruction builders under src/generated/, while hand-written files in src/ add PDA helpers, filtering utilities, and the critical deriveEscrowId function. @descro/sdk is referenced as a local file: path (not npm).
Tech Stack: Anchor IDL build, codama + @codama/renderers-js, @solana/kit ^6, @solana/program-client-core, vitest for unit tests.
File Map
| Path | Purpose |
|---|---|
package.json (root) |
Add "workspaces": ["sdk","app"] |
sdk/package.json |
Workspace package @solisting/sdk |
sdk/tsconfig.json |
TypeScript config |
sdk/codama.solisting.json |
Codama render config |
sdk/src/idl/solisting.json |
Anchor-generated IDL (copied from target/idl/) |
sdk/src/generated/solisting/ |
Codama output — do NOT edit manually |
sdk/src/pda.ts |
findListingPda, findOrderPda, deriveEscrowId |
sdk/src/listing.ts |
fetchAllListings, fetchListingsBySeller |
sdk/src/order.ts |
fetchOrdersForListing, fetchOrdersByBuyer |
sdk/src/index.ts |
Public re-exports |
sdk/src/__tests__/pda.test.ts |
Unit tests |
Task 1: Generate the Anchor IDL
Files:
-
Read:
programs/solisting/Cargo.toml(confirmidl-buildfeature exists) -
Output:
target/idl/solisting.json -
Step 1: Verify anchor-cli is installed
anchor --version
Expected: anchor-cli 0.30.x or 1.x
- Step 2: Generate the IDL (host-target compilation, not SBF)
anchor idl build --program-name solisting
If this fails, use the full build (slower):
anchor build
Expected: target/idl/solisting.json created
- Step 3: Confirm IDL structure
cat target/idl/solisting.json | python3 -m json.tool | head -60
Expected: JSON with "name": "solisting", "accounts", "instructions", "types" keys. Confirm accounts named listingAccount and orderAccount, and 8 instructions.
- Step 4: Commit
git add target/idl/solisting.json
git commit -m "chore: generate solisting IDL"
Task 2: SDK Package Setup
Files:
-
Modify:
package.json(root) -
Create:
sdk/package.json -
Create:
sdk/tsconfig.json -
Create:
sdk/codama.solisting.json -
Create:
sdk/src/idl/solisting.json -
Step 1: Update root package.json to add workspaces
Replace package.json at repo root with:
{
"name": "solisting",
"license": "ISC",
"private": true,
"workspaces": ["sdk", "app"],
"scripts": {
"lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
},
"devDependencies": {
"prettier": "^3.8.3"
}
}
- Step 2: Create
sdk/package.json
{
"name": "@solisting/sdk",
"version": "0.1.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"generate": "codama run js --config codama.solisting.json",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@descro/sdk": "file:../../descro/sdk",
"@solana/kit": "^6.0.0",
"@solana/program-client-core": "^6.4.0"
},
"devDependencies": {
"@codama/nodes-from-anchor": "^1.4.1",
"@codama/renderers-js": "^2.2.0",
"codama": "^1.6.0",
"typescript": "^6.0.3",
"vitest": "^3.0.0"
}
}
- Step 3: Create
sdk/tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"skipLibCheck": true,
"outDir": "./dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
- Step 4: Create
sdk/codama.solisting.json
{
"idl": "./src/idl/solisting.json",
"scripts": {
"js": [
{
"from": "@codama/renderers-js",
"args": ["./src/generated/solisting"]
}
]
}
}
- Step 5: Copy the IDL into the SDK
mkdir -p sdk/src/idl
cp target/idl/solisting.json sdk/src/idl/solisting.json
- Step 6: Install dependencies
yarn install
Expected: sdk/node_modules/ populated, @codama/renderers-js available.
- Step 7: Commit
git add sdk/package.json sdk/tsconfig.json sdk/codama.solisting.json sdk/src/idl/solisting.json package.json
git commit -m "feat: add @solisting/sdk package scaffold"
Task 3: Run Codama
Files:
-
Create:
sdk/src/generated/solisting/(entire directory, auto-generated) -
Step 1: Run codama
cd sdk && yarn generate
Expected: sdk/src/generated/solisting/ created with subdirectories:
src/generated/solisting/
├── accounts/
│ ├── index.ts
│ ├── listingAccount.ts
│ └── orderAccount.ts
├── instructions/
│ ├── index.ts
│ ├── createListing.ts
│ ├── updateListing.ts
│ ├── closeListing.ts
│ ├── createOrder.ts
│ ├── acceptOrder.ts
│ ├── rejectOrder.ts
│ ├── cancelOrder.ts
│ └── closeStaleOrder.ts
├── pdas/
│ ├── index.ts
│ ├── listingAccount.ts
│ └── orderAccount.ts
├── types/
│ ├── index.ts
│ ├── currency.ts
│ └── altCurrencyConfig.ts
├── errors/
│ └── solisting.ts
├── programs/
│ └── solisting.ts
└── index.ts
- Step 2: Inspect the generated account types
cat sdk/src/generated/solisting/accounts/listingAccount.ts | head -40
Note the exact field names (camelCase in TS, e.g. quantityReserved, isActive, canonicalCurrency, metadataUri). These must match what you use in the hand-written helpers and components.
- Step 3: Inspect the generated PDA functions
cat sdk/src/generated/solisting/pdas/listingAccount.ts
cat sdk/src/generated/solisting/pdas/orderAccount.ts
Note the exact seed parameter names (e.g. { seller, listingId } or { seller, listingIdLeBytes }). Required in Task 4.
- Step 4: Inspect the generated instruction builders
cat sdk/src/generated/solisting/instructions/createListing.ts | head -50
Note all required and optional account + data fields. These will be used in Plan 4 (write transactions).
- Step 5: Add generated directory to git
git add sdk/src/generated/
git commit -m "feat: generate solisting codama bindings"
Task 4: SDK Hand-Written Helpers
Files:
-
Create:
sdk/src/pda.ts -
Create:
sdk/src/listing.ts -
Create:
sdk/src/order.ts -
Create:
sdk/src/__tests__/pda.test.ts -
Step 1: Write
sdk/src/pda.ts
import { getAddressEncoder, type Address, type ProgramDerivedAddress } from '@solana/kit'
import { findListingAccountPda, findOrderAccountPda } from './generated/solisting'
// Convenience wrappers so callers don't need to pass the program address.
export async function findListingPda(
seller: Address,
listingId: bigint,
): Promise<ProgramDerivedAddress> {
return findListingAccountPda({ seller, listingId })
}
export async function findOrderPda(
listingAccount: Address,
buyer: Address,
orderId: bigint,
): Promise<ProgramDerivedAddress> {
return findOrderAccountPda({ listingAccount, buyer, orderId })
}
/**
* Replicates the program's escrow_id derivation:
* u64::from_le_bytes(order_pda.to_bytes()[0..8])
* Must match exactly — used when building create_order instructions.
*/
export function deriveEscrowId(orderPda: Address): bigint {
const bytes = getAddressEncoder().encode(orderPda) // 32-byte Uint8Array
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
return view.getBigUint64(0, true) // little-endian, first 8 bytes
}
Note: If codama's findListingAccountPda seed parameters differ (inspect in Task 3 Step 3), adjust accordingly. The seeds from CLAUDE.md are [b"listing", seller, listing_id_le].
- Step 2: Write
sdk/src/listing.ts
import type { Account, Address } from '@solana/kit'
import {
fetchAllListingAccounts,
fetchListingAccount,
type ListingAccount,
} from './generated/solisting'
export type ListingAccountWithPda = Account<ListingAccount>
type Rpc = Parameters<typeof fetchAllListingAccounts>[0]
export async function fetchAllListings(rpc: Rpc): Promise<ListingAccountWithPda[]> {
return fetchAllListingAccounts(rpc)
}
export async function fetchListingsBySeller(
rpc: Rpc,
seller: Address,
): Promise<ListingAccountWithPda[]> {
const all = await fetchAllListingAccounts(rpc)
return all.filter((l) => l.data.seller === seller)
}
export async function fetchListing(
rpc: Rpc,
address: Address,
): Promise<ListingAccountWithPda | null> {
return fetchListingAccount(rpc, address).catch(() => null)
}
- Step 3: Write
sdk/src/order.ts
import type { Account, Address } from '@solana/kit'
import {
fetchAllOrderAccounts,
fetchOrderAccount,
type OrderAccount,
} from './generated/solisting'
import { deriveEscrowId } from './pda'
export type OrderAccountWithPda = Account<OrderAccount>
type Rpc = Parameters<typeof fetchAllOrderAccounts>[0]
export async function fetchOrdersForListing(
rpc: Rpc,
listingPk: Address,
): Promise<OrderAccountWithPda[]> {
const all = await fetchAllOrderAccounts(rpc)
return all.filter((o) => o.data.listingAccount === listingPk)
}
export async function fetchOrdersByBuyer(
rpc: Rpc,
buyer: Address,
): Promise<OrderAccountWithPda[]> {
const all = await fetchAllOrderAccounts(rpc)
return all.filter((o) => o.data.buyer === buyer)
}
export async function fetchOrder(
rpc: Rpc,
address: Address,
): Promise<OrderAccountWithPda | null> {
return fetchOrderAccount(rpc, address).catch(() => null)
}
export { deriveEscrowId }
- Step 4: Write unit tests
sdk/src/__tests__/pda.test.ts
import { describe, it, expect } from 'vitest'
import { address } from '@solana/kit'
import { deriveEscrowId, findListingPda } from '../pda'
const SELLER = address('11111111111111111111111111111112')
describe('deriveEscrowId', () => {
it('returns a bigint from the first 8 bytes of the order PDA', () => {
// We test the determinism: same input → same output
const fakeOrderPda = address('So11111111111111111111111111111111111111112')
const id1 = deriveEscrowId(fakeOrderPda)
const id2 = deriveEscrowId(fakeOrderPda)
expect(id1).toBe(id2)
expect(typeof id1).toBe('bigint')
})
it('returns different ids for different PDAs', () => {
const pda1 = address('So11111111111111111111111111111111111111112')
const pda2 = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA')
expect(deriveEscrowId(pda1)).not.toBe(deriveEscrowId(pda2))
})
})
describe('findListingPda', () => {
it('derives a deterministic PDA for a given seller + listingId', async () => {
const [pda1] = await findListingPda(SELLER, 1001n)
const [pda2] = await findListingPda(SELLER, 1001n)
expect(pda1).toBe(pda2)
expect(pda1).toHaveLength(44) // base58 encoded 32-byte pubkey
})
it('produces different PDAs for different listing ids', async () => {
const [pda1] = await findListingPda(SELLER, 1001n)
const [pda2] = await findListingPda(SELLER, 1002n)
expect(pda1).not.toBe(pda2)
})
})
- Step 5: Run tests
cd sdk && yarn test
Expected: 4 passing tests. If findListingPda seed params differ from what's in pda.ts, fix the wrapper signature based on what Task 3 Step 3 revealed.
- Step 6: Type-check
cd sdk && yarn typecheck
Expected: No errors.
Task 5: SDK index.ts + Final Check
Files:
-
Create:
sdk/src/index.ts -
Step 1: Write
sdk/src/index.ts
// Generated bindings — accounts, instructions, pdas, types, errors, program id
export * from './generated/solisting'
// Hand-written helpers
export * from './pda'
export * from './listing'
export * from './order'
// Re-export Address for consumers
export type { Address, Account } from '@solana/kit'
- Step 2: Type-check once more
cd sdk && yarn typecheck
Expected: No errors.
- Step 3: Run tests again to confirm nothing regressed
cd sdk && yarn test
Expected: 4 passing.
- Step 4: Commit
git add sdk/src/
git commit -m "feat: add @solisting/sdk helpers, tests, and index exports"
Next: Proceed to Part 2 — 2026-06-22-solisting-app-foundation.md