489 lines
13 KiB
Markdown
489 lines
13 KiB
Markdown
# 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**
|
|
|
|
```bash
|
|
anchor --version
|
|
```
|
|
Expected: `anchor-cli 0.30.x` or `1.x`
|
|
|
|
- [ ] **Step 2: Generate the IDL (host-target compilation, not SBF)**
|
|
|
|
```bash
|
|
anchor idl build --program-name solisting
|
|
```
|
|
|
|
If this fails, use the full build (slower):
|
|
```bash
|
|
anchor build
|
|
```
|
|
|
|
Expected: `target/idl/solisting.json` created
|
|
|
|
- [ ] **Step 3: Confirm IDL structure**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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:
|
|
|
|
```json
|
|
{
|
|
"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`**
|
|
|
|
```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`**
|
|
|
|
```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`**
|
|
|
|
```json
|
|
{
|
|
"idl": "./src/idl/solisting.json",
|
|
"scripts": {
|
|
"js": [
|
|
{
|
|
"from": "@codama/renderers-js",
|
|
"args": ["./src/generated/solisting"]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 5: Copy the IDL into the SDK**
|
|
|
|
```bash
|
|
mkdir -p sdk/src/idl
|
|
cp target/idl/solisting.json sdk/src/idl/solisting.json
|
|
```
|
|
|
|
- [ ] **Step 6: Install dependencies**
|
|
|
|
```bash
|
|
yarn install
|
|
```
|
|
|
|
Expected: `sdk/node_modules/` populated, `@codama/renderers-js` available.
|
|
|
|
- [ ] **Step 7: Commit**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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`**
|
|
|
|
```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`**
|
|
|
|
```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`**
|
|
|
|
```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`**
|
|
|
|
```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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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`**
|
|
|
|
```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**
|
|
|
|
```bash
|
|
cd sdk && yarn typecheck
|
|
```
|
|
|
|
Expected: No errors.
|
|
|
|
- [ ] **Step 3: Run tests again to confirm nothing regressed**
|
|
|
|
```bash
|
|
cd sdk && yarn test
|
|
```
|
|
|
|
Expected: 4 passing.
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
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`
|