feat: add @solisting/sdk helpers and unit tests
Add hand-written SDK helper files (pda.ts, listing.ts, order.ts) with GPA-based account fetching, discriminator filters, PDA derivation helpers, and escrow ID derivation. Add pda.test.ts with 4 unit tests (all passing). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
36
sdk/src/__tests__/pda.test.ts
Normal file
36
sdk/src/__tests__/pda.test.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { address } from '@solana/kit'
|
||||||
|
import { deriveEscrowId, findListingPda } from '../pda.js'
|
||||||
|
|
||||||
|
const SELLER = address('11111111111111111111111111111112')
|
||||||
|
|
||||||
|
describe('deriveEscrowId', () => {
|
||||||
|
it('returns a bigint from the first 8 bytes of the order PDA', () => {
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
54
sdk/src/listing.ts
Normal file
54
sdk/src/listing.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { address, type Account, type Address, type Rpc } from '@solana/kit'
|
||||||
|
import { type GetProgramAccountsApi } from '@solana/rpc-api'
|
||||||
|
import { type GetAccountInfoApi } from '@solana/rpc-api'
|
||||||
|
import type { Base64EncodedBytes } from '@solana/rpc-types'
|
||||||
|
import {
|
||||||
|
decodeSolistingStateListingAccount,
|
||||||
|
fetchSolistingStateListingAccount,
|
||||||
|
SOLISTING_STATE_LISTING_ACCOUNT_DISCRIMINATOR,
|
||||||
|
type SolistingStateListingAccount,
|
||||||
|
} from './generated/solisting/src/generated/index.js'
|
||||||
|
|
||||||
|
const PROGRAM_ADDRESS = address('DzwUAbpRvqcbA8QsEkRbZeXG4TEho5782cMySodrUHBU')
|
||||||
|
|
||||||
|
export type ListingAccountWithPda = Account<SolistingStateListingAccount>
|
||||||
|
export type GpaRpc = Rpc<GetProgramAccountsApi>
|
||||||
|
export type GetRpc = Rpc<GetAccountInfoApi>
|
||||||
|
|
||||||
|
export async function fetchAllListings(rpc: GpaRpc): Promise<ListingAccountWithPda[]> {
|
||||||
|
const disc = SOLISTING_STATE_LISTING_ACCOUNT_DISCRIMINATOR
|
||||||
|
const discBase64 = Buffer.from(disc).toString('base64') as Base64EncodedBytes
|
||||||
|
const results = await rpc
|
||||||
|
.getProgramAccounts(PROGRAM_ADDRESS, {
|
||||||
|
encoding: 'base64',
|
||||||
|
filters: [{ memcmp: { offset: 0n, bytes: discBase64, encoding: 'base64' } }],
|
||||||
|
})
|
||||||
|
.send()
|
||||||
|
return (results as Array<{ pubkey: Address; account: { executable: boolean; lamports: bigint; owner: Address; space: bigint; data: [string, 'base64'] } }>).map((r) => {
|
||||||
|
const data = new Uint8Array(Buffer.from(r.account.data[0], 'base64'))
|
||||||
|
return decodeSolistingStateListingAccount({
|
||||||
|
address: r.pubkey,
|
||||||
|
data,
|
||||||
|
executable: r.account.executable,
|
||||||
|
lamports: r.account.lamports as unknown as import('@solana/rpc-types').Lamports,
|
||||||
|
programAddress: r.account.owner,
|
||||||
|
space: r.account.space,
|
||||||
|
exists: true,
|
||||||
|
}) as ListingAccountWithPda
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchListingsBySeller(
|
||||||
|
rpc: GpaRpc,
|
||||||
|
seller: Address,
|
||||||
|
): Promise<ListingAccountWithPda[]> {
|
||||||
|
const all = await fetchAllListings(rpc)
|
||||||
|
return all.filter((l) => l.data.seller === seller)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchListing(
|
||||||
|
rpc: GetRpc,
|
||||||
|
addr: Address,
|
||||||
|
): Promise<ListingAccountWithPda | null> {
|
||||||
|
return fetchSolistingStateListingAccount(rpc, addr).catch(() => null)
|
||||||
|
}
|
||||||
65
sdk/src/order.ts
Normal file
65
sdk/src/order.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { address, type Account, type Address, type Rpc } from '@solana/kit'
|
||||||
|
import { type GetProgramAccountsApi } from '@solana/rpc-api'
|
||||||
|
import { type GetAccountInfoApi } from '@solana/rpc-api'
|
||||||
|
import type { Base64EncodedBytes } from '@solana/rpc-types'
|
||||||
|
import {
|
||||||
|
decodeSolistingStateOrderAccount,
|
||||||
|
fetchSolistingStateOrderAccount,
|
||||||
|
SOLISTING_STATE_ORDER_ACCOUNT_DISCRIMINATOR,
|
||||||
|
type SolistingStateOrderAccount,
|
||||||
|
} from './generated/solisting/src/generated/index.js'
|
||||||
|
import { deriveEscrowId } from './pda.js'
|
||||||
|
|
||||||
|
const PROGRAM_ADDRESS = address('DzwUAbpRvqcbA8QsEkRbZeXG4TEho5782cMySodrUHBU')
|
||||||
|
|
||||||
|
export type OrderAccountWithPda = Account<SolistingStateOrderAccount>
|
||||||
|
type GpaRpc = Rpc<GetProgramAccountsApi>
|
||||||
|
type GetRpc = Rpc<GetAccountInfoApi>
|
||||||
|
|
||||||
|
async function fetchAllOrders(rpc: GpaRpc): Promise<OrderAccountWithPda[]> {
|
||||||
|
const disc = SOLISTING_STATE_ORDER_ACCOUNT_DISCRIMINATOR
|
||||||
|
const discBase64 = Buffer.from(disc).toString('base64') as Base64EncodedBytes
|
||||||
|
const results = await rpc
|
||||||
|
.getProgramAccounts(PROGRAM_ADDRESS, {
|
||||||
|
encoding: 'base64',
|
||||||
|
filters: [{ memcmp: { offset: 0n, bytes: discBase64, encoding: 'base64' } }],
|
||||||
|
})
|
||||||
|
.send()
|
||||||
|
return (results as Array<{ pubkey: Address; account: { executable: boolean; lamports: bigint; owner: Address; space: bigint; data: [string, 'base64'] } }>).map((r) => {
|
||||||
|
const data = new Uint8Array(Buffer.from(r.account.data[0], 'base64'))
|
||||||
|
return decodeSolistingStateOrderAccount({
|
||||||
|
address: r.pubkey,
|
||||||
|
data,
|
||||||
|
executable: r.account.executable,
|
||||||
|
lamports: r.account.lamports as unknown as import('@solana/rpc-types').Lamports,
|
||||||
|
programAddress: r.account.owner,
|
||||||
|
space: r.account.space,
|
||||||
|
exists: true,
|
||||||
|
}) as OrderAccountWithPda
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchOrdersForListing(
|
||||||
|
rpc: GpaRpc,
|
||||||
|
listingPk: Address,
|
||||||
|
): Promise<OrderAccountWithPda[]> {
|
||||||
|
const all = await fetchAllOrders(rpc)
|
||||||
|
return all.filter((o) => o.data.listing === listingPk)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchOrdersByBuyer(
|
||||||
|
rpc: GpaRpc,
|
||||||
|
buyer: Address,
|
||||||
|
): Promise<OrderAccountWithPda[]> {
|
||||||
|
const all = await fetchAllOrders(rpc)
|
||||||
|
return all.filter((o) => o.data.buyer === buyer)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchOrder(
|
||||||
|
rpc: GetRpc,
|
||||||
|
addr: Address,
|
||||||
|
): Promise<OrderAccountWithPda | null> {
|
||||||
|
return fetchSolistingStateOrderAccount(rpc, addr).catch(() => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { deriveEscrowId }
|
||||||
30
sdk/src/pda.ts
Normal file
30
sdk/src/pda.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { address, getAddressEncoder, type Address, type ProgramDerivedAddress } from '@solana/kit'
|
||||||
|
import { findListingAccountPda, findOrderAccountPda } from './generated/solisting/src/generated/index.js'
|
||||||
|
|
||||||
|
const PROGRAM_ADDRESS = address('DzwUAbpRvqcbA8QsEkRbZeXG4TEho5782cMySodrUHBU')
|
||||||
|
|
||||||
|
export async function findListingPda(
|
||||||
|
seller: Address,
|
||||||
|
listingId: bigint,
|
||||||
|
): Promise<ProgramDerivedAddress> {
|
||||||
|
return findListingAccountPda({ seller, listingId }, { programAddress: PROGRAM_ADDRESS })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findOrderPda(
|
||||||
|
listingAccount: Address,
|
||||||
|
buyer: Address,
|
||||||
|
orderId: bigint,
|
||||||
|
): Promise<ProgramDerivedAddress> {
|
||||||
|
return findOrderAccountPda({ listingAccount, buyer, orderId }, { programAddress: PROGRAM_ADDRESS })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user