diff --git a/sdk/src/__tests__/pda.test.ts b/sdk/src/__tests__/pda.test.ts new file mode 100644 index 0000000..2221078 --- /dev/null +++ b/sdk/src/__tests__/pda.test.ts @@ -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) + }) +}) diff --git a/sdk/src/listing.ts b/sdk/src/listing.ts new file mode 100644 index 0000000..adc503c --- /dev/null +++ b/sdk/src/listing.ts @@ -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 +export type GpaRpc = Rpc +export type GetRpc = Rpc + +export async function fetchAllListings(rpc: GpaRpc): Promise { + 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 { + const all = await fetchAllListings(rpc) + return all.filter((l) => l.data.seller === seller) +} + +export async function fetchListing( + rpc: GetRpc, + addr: Address, +): Promise { + return fetchSolistingStateListingAccount(rpc, addr).catch(() => null) +} diff --git a/sdk/src/order.ts b/sdk/src/order.ts new file mode 100644 index 0000000..82c7ea3 --- /dev/null +++ b/sdk/src/order.ts @@ -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 +type GpaRpc = Rpc +type GetRpc = Rpc + +async function fetchAllOrders(rpc: GpaRpc): Promise { + 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 { + const all = await fetchAllOrders(rpc) + return all.filter((o) => o.data.listing === listingPk) +} + +export async function fetchOrdersByBuyer( + rpc: GpaRpc, + buyer: Address, +): Promise { + const all = await fetchAllOrders(rpc) + return all.filter((o) => o.data.buyer === buyer) +} + +export async function fetchOrder( + rpc: GetRpc, + addr: Address, +): Promise { + return fetchSolistingStateOrderAccount(rpc, addr).catch(() => null) +} + +export { deriveEscrowId } diff --git a/sdk/src/pda.ts b/sdk/src/pda.ts new file mode 100644 index 0000000..9c93e7f --- /dev/null +++ b/sdk/src/pda.ts @@ -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 { + return findListingAccountPda({ seller, listingId }, { programAddress: PROGRAM_ADDRESS }) +} + +export async function findOrderPda( + listingAccount: Address, + buyer: Address, + orderId: bigint, +): Promise { + 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 +}