Files
solisting/docs/superpowers/plans/2026-06-22-solisting-sdk.md

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 (confirm idl-build feature 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