use codama in sdk

This commit is contained in:
thesn10
2026-05-23 13:29:13 +02:00
parent 186136722d
commit c92e71fe81
7 changed files with 167 additions and 450 deletions

View File

@@ -8,7 +8,8 @@
"generate": "codama run js --config codama.descro.json && codama run js --config codama.registry.json"
},
"dependencies": {
"@solana/kit": "^6.0.0"
"@solana/kit": "^6.0.0",
"@solana/program-client-core": "^6.4.0"
},
"devDependencies": {
"@codama/nodes-from-anchor": "^1.4.1",

View File

@@ -1,193 +1,68 @@
import type { Address, GetProgramAccountsApi, Lamports, Rpc } from "@solana/kit";
import {
PublicKey,
SystemProgram,
TransactionInstruction,
} from "@solana/web3.js";
import { AnchorProvider, Program, BN } from "@coral-xyz/anchor";
import type { Idl } from "@coral-xyz/anchor";
import descroIdl from "./idl/descro.json";
import {
REGISTRY_PROGRAM_ID,
deriveEscrowPda,
deriveVaultPda,
deriveEscrowAuthorityPda,
deriveResolverEntryPda,
} from "./pda";
import type { EscrowAccount, EscrowAccountWithPda, Winner } from "./types";
fetchMaybeEscrowAccount,
fetchEscrowAccount,
decodeEscrowAccount,
getEscrowAccountDiscriminatorBytes,
} from "./generated/descro/src/generated/accounts/escrowAccount";
import { DESCRO_PROGRAM_ADDRESS } from "./generated/descro/src/generated/programs/descro";
import type { EscrowAccountWithPda } from "./types";
export class EscrowClient {
readonly program: Program;
readonly provider: AnchorProvider;
export { fetchEscrowAccount, fetchMaybeEscrowAccount, decodeEscrowAccount };
constructor(provider: AnchorProvider) {
this.provider = provider;
this.program = new Program(descroIdl as Idl, provider);
}
export async function fetchEscrowsForWallet(
rpc: Rpc<GetProgramAccountsApi>,
walletAddress: Address,
): Promise<EscrowAccountWithPda[]> {
const discriminatorBase64 = btoa(
String.fromCharCode(...getEscrowAccountDiscriminatorBytes()),
) as never;
async buildCreateEscrow(params: {
seller: PublicKey;
buyer: PublicKey;
amount: BN;
disputeResolver: PublicKey | null;
escrowId: BN;
}): Promise<TransactionInstruction> {
const { seller, buyer, amount, disputeResolver, escrowId } = params;
const [escrowPda] = deriveEscrowPda(seller, escrowId);
const [vaultPda] = deriveVaultPda(escrowPda);
const walletBytes = walletAddress as never;
return await (this.program.methods as any)
.createEscrow(amount, disputeResolver, escrowId)
.accounts({
seller,
buyer,
escrowAccount: escrowPda,
vault: vaultPda,
systemProgram: SystemProgram.programId,
const [asSeller, asBuyer] = await Promise.all([
rpc
.getProgramAccounts(DESCRO_PROGRAM_ADDRESS, {
filters: [
{ memcmp: { offset: 0n, bytes: discriminatorBase64, encoding: "base64" } },
{ memcmp: { offset: 8n, bytes: walletBytes, encoding: "base58" } },
],
encoding: "base64",
})
.instruction();
}
async buildDeposit(params: {
buyer: PublicKey;
escrowPda: PublicKey;
}): Promise<TransactionInstruction> {
const { buyer, escrowPda } = params;
const [vaultPda] = deriveVaultPda(escrowPda);
return await (this.program.methods as any)
.deposit()
.accounts({
buyer,
escrowAccount: escrowPda,
vault: vaultPda,
systemProgram: SystemProgram.programId,
.send(),
rpc
.getProgramAccounts(DESCRO_PROGRAM_ADDRESS, {
filters: [
{ memcmp: { offset: 0n, bytes: discriminatorBase64, encoding: "base64" } },
{ memcmp: { offset: 40n, bytes: walletBytes, encoding: "base58" } },
],
encoding: "base64",
})
.instruction();
.send(),
]);
const seen = new Set<string>();
const results: EscrowAccountWithPda[] = [];
for (const item of [...asSeller, ...asBuyer]) {
const pda = item.pubkey;
if (seen.has(pda)) continue;
seen.add(pda);
const [base64Data] = item.account.data;
const rawBytes = Uint8Array.from(atob(base64Data), (c) => c.charCodeAt(0));
const decoded = decodeEscrowAccount({
address: pda,
data: rawBytes,
executable: item.account.executable,
lamports: item.account.lamports as Lamports,
programAddress: item.account.owner,
space: item.account.space,
exists: true,
});
if (decoded.exists) results.push({ pda, account: decoded.data });
}
async buildComplete(params: {
buyer: PublicKey;
seller: PublicKey;
escrowPda: PublicKey;
}): Promise<TransactionInstruction> {
const { buyer, seller, escrowPda } = params;
const [vaultPda] = deriveVaultPda(escrowPda);
return await (this.program.methods as any)
.complete()
.accounts({
buyer,
seller,
escrowAccount: escrowPda,
vault: vaultPda,
systemProgram: SystemProgram.programId,
})
.instruction();
}
async buildDispute(params: {
initiator: PublicKey;
escrowPda: PublicKey;
}): Promise<TransactionInstruction> {
const { initiator, escrowPda } = params;
return await (this.program.methods as any)
.dispute()
.accounts({
initiator,
escrowAccount: escrowPda,
})
.instruction();
}
async buildCancel(params: {
seller: PublicKey;
escrowPda: PublicKey;
}): Promise<TransactionInstruction> {
const { seller, escrowPda } = params;
return await (this.program.methods as any)
.cancel()
.accounts({
seller,
escrowAccount: escrowPda,
systemProgram: SystemProgram.programId,
})
.instruction();
}
async buildResolve(params: {
resolver: PublicKey;
winner: "buyer" | "seller";
winnerPubkey: PublicKey;
seller: PublicKey;
escrowPda: PublicKey;
disputeResolver: PublicKey | null;
}): Promise<TransactionInstruction> {
const { resolver, winner, winnerPubkey, seller, escrowPda, disputeResolver } = params;
const [vaultPda] = deriveVaultPda(escrowPda);
const [escrowAuthority] = deriveEscrowAuthorityPda();
const registryProgram = disputeResolver ? REGISTRY_PROGRAM_ID : SystemProgram.programId;
const resolverEntryAuthority = disputeResolver ?? resolver;
const [resolverEntry] = deriveResolverEntryPda(resolverEntryAuthority);
const winnerArg: Winner =
winner === "buyer" ? { buyer: {} } : { seller: {} };
return await (this.program.methods as any)
.resolve(winnerArg)
.accounts({
resolver,
winner: winnerPubkey,
seller,
escrowAccount: escrowPda,
vault: vaultPda,
resolverEntry,
escrowAuthority,
registryProgram,
systemProgram: SystemProgram.programId,
})
.instruction();
}
async fetchEscrow(pda: PublicKey): Promise<EscrowAccount | null> {
try {
const raw = await (this.program.account as any).escrowAccount.fetch(pda);
return raw as EscrowAccount;
} catch {
return null;
}
}
async fetchAllEscrows(): Promise<EscrowAccountWithPda[]> {
const accounts = await (this.program.account as any).escrowAccount.all();
return accounts.map((a: { publicKey: PublicKey; account: EscrowAccount }) => ({
pda: a.publicKey,
account: a.account as EscrowAccount,
}));
}
async fetchEscrowsForWallet(wallet: PublicKey): Promise<EscrowAccountWithPda[]> {
const asSeller = await (this.program.account as any).escrowAccount.all([
{ memcmp: { offset: 8, bytes: wallet.toBase58() } },
]);
const asBuyer = await (this.program.account as any).escrowAccount.all([
{ memcmp: { offset: 8 + 32, bytes: wallet.toBase58() } },
]);
const seen = new Set<string>();
const results: EscrowAccountWithPda[] = [];
for (const a of [...asSeller, ...asBuyer]) {
const key = a.publicKey.toBase58();
if (!seen.has(key)) {
seen.add(key);
results.push({ pda: a.publicKey, account: a.account as EscrowAccount });
}
}
return results;
}
return results;
}

View File

@@ -2,18 +2,4 @@ export * from "./types";
export * from "./pda";
export * from "./escrow";
export * from "./registry";
export * from "./listener";
import { AnchorProvider } from "@coral-xyz/anchor";
import { EscrowClient } from "./escrow";
import { RegistryClient } from "./registry";
export class DescroSdk {
readonly escrow: EscrowClient;
readonly registry: RegistryClient;
constructor(provider: AnchorProvider) {
this.escrow = new EscrowClient(provider);
this.registry = new RegistryClient(provider);
}
}
export * from "./listener";

View File

@@ -1,53 +1,45 @@
import { Connection, PublicKey, AccountInfo } from "@solana/web3.js";
import { AnchorProvider, Program } from "@coral-xyz/anchor";
import type { Idl } from "@coral-xyz/anchor";
import descroIdl from "./idl/descro.json";
import { ESCROW_PROGRAM_ID } from "./pda";
import type { EscrowAccount } from "./types";
function decodeEscrowAccount(
program: Program,
info: AccountInfo<Buffer>
): EscrowAccount | null {
try {
return (program.coder.accounts as any).decode("EscrowAccount", info.data) as EscrowAccount;
} catch {
return null;
}
}
import type { Address, Lamports } from "@solana/kit";
import type { AccountNotificationsApi } from "@solana/rpc-subscriptions-api";
import type { RpcSubscriptions } from "@solana/rpc-subscriptions-spec";
import { decodeEscrowAccount } from "./generated/descro/src/generated/accounts/escrowAccount";
import type { EscrowAccount } from "./generated/descro/src/generated/accounts/escrowAccount";
export function subscribeEscrow(
connection: Connection,
pda: PublicKey,
cb: (account: EscrowAccount | null) => void
rpcSubscriptions: RpcSubscriptions<AccountNotificationsApi>,
address: Address,
cb: (account: EscrowAccount | null) => void,
): () => void {
const provider = new AnchorProvider(connection, {} as never, {});
const program = new Program(descroIdl as Idl, new PublicKey(ESCROW_PROGRAM_ID), provider);
const abortController = new AbortController();
const subId = connection.onAccountChange(pda, (info) => {
cb(decodeEscrowAccount(program, info as AccountInfo<Buffer>));
});
(async () => {
try {
const notifications = await rpcSubscriptions
.accountNotifications(address, { encoding: "base64" })
.subscribe({ abortSignal: abortController.signal });
return () => {
connection.removeAccountChangeListener(subId).catch(() => undefined);
};
}
export function subscribeAll(
connection: Connection,
programId: PublicKey = ESCROW_PROGRAM_ID,
cb: (pda: PublicKey, account: EscrowAccount | null) => void
): () => void {
const provider = new AnchorProvider(connection, {} as never, {});
const program = new Program(descroIdl as Idl, new PublicKey(programId), provider);
const subId = connection.onProgramAccountChange(programId, (keyedAccountInfo) => {
const pda = keyedAccountInfo.accountId;
const info = keyedAccountInfo.accountInfo;
cb(pda, decodeEscrowAccount(program, info as AccountInfo<Buffer>));
});
return () => {
connection.removeProgramAccountChangeListener(subId).catch(() => undefined);
};
for await (const notification of notifications) {
const info = notification.value;
const [base64Data] = info.data;
const rawBytes = Uint8Array.from(atob(base64Data), (c) =>
c.charCodeAt(0),
);
const decoded = decodeEscrowAccount({
address,
data: rawBytes,
executable: info.executable,
lamports: info.lamports as Lamports,
programAddress: info.owner,
space: info.space,
exists: true,
});
cb(decoded.exists ? decoded.data : null);
}
} catch {
// subscription was aborted or connection closed
}
})();
return () => abortController.abort();
}

View File

@@ -1,52 +1,17 @@
import { PublicKey } from "@solana/web3.js";
import BN from "bn.js";
export const ESCROW_PROGRAM_ID = new PublicKey(
"DjVR4EuYV6USMJFfsGZwhZ3y8rtWsmG8EvDY96GTqqi3"
);
export const REGISTRY_PROGRAM_ID = new PublicKey(
"GwUPAKs3HHzCpj8uhet4NAnxk9GWNwfrYbpihu5DyFp"
);
export function deriveEscrowPda(
seller: PublicKey,
escrowId: BN | bigint | number,
programId: PublicKey = ESCROW_PROGRAM_ID
): [PublicKey, number] {
const id = new BN(escrowId.toString());
const idBuf = Buffer.alloc(8);
idBuf.writeBigUInt64LE(BigInt(id.toString()));
return PublicKey.findProgramAddressSync(
[Buffer.from("escrow"), seller.toBuffer(), idBuf],
programId
);
}
export function deriveVaultPda(
escrowPda: PublicKey,
programId: PublicKey = ESCROW_PROGRAM_ID
): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[Buffer.from("vault"), escrowPda.toBuffer()],
programId
);
}
export function deriveEscrowAuthorityPda(
programId: PublicKey = ESCROW_PROGRAM_ID
): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[Buffer.from("escrow_authority")],
programId
);
}
export function deriveResolverEntryPda(
authority: PublicKey,
registryProgramId: PublicKey = REGISTRY_PROGRAM_ID
): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[Buffer.from("resolver"), authority.toBuffer()],
registryProgramId
);
}
export {
findEscrowAccountPda,
type EscrowAccountSeeds,
} from "./generated/descro/src/generated/pdas/escrowAccount";
export {
findVaultPda,
type VaultSeeds,
} from "./generated/descro/src/generated/pdas/vault";
export {
findEscrowAuthorityPda,
} from "./generated/descro/src/generated/pdas/escrowAuthority";
export {
findResolverEntryPda,
type ResolverEntrySeeds,
} from "./generated/descro_ext_resolvers/src/generated/pdas/resolverEntry";
export { DESCRO_PROGRAM_ADDRESS } from "./generated/descro/src/generated/programs/descro";
export { DESCRO_EXT_RESOLVERS_PROGRAM_ADDRESS } from "./generated/descro_ext_resolvers/src/generated/programs/descroExtResolvers";

View File

@@ -1,84 +1 @@
import { PublicKey, TransactionInstruction, SystemProgram } from "@solana/web3.js";
import { AnchorProvider, Program } from "@coral-xyz/anchor";
import type { Idl } from "@coral-xyz/anchor";
import registryIdl from "./idl/descro_ext_resolvers.json";
import { deriveResolverEntryPda } from "./pda";
import type { ResolverEntry, ResolverEntryWithPda, ResolverType } from "./types";
export class RegistryClient {
readonly program: Program;
readonly provider: AnchorProvider;
constructor(provider: AnchorProvider) {
this.provider = provider;
this.program = new Program(registryIdl as Idl, provider);
}
async buildRegisterResolver(params: {
authority: PublicKey;
resolverType: ResolverType;
name: string;
description: string;
feeBps: number;
feeRecipient: PublicKey;
metadataUri: string;
}): Promise<TransactionInstruction> {
const { authority, resolverType, name, description, feeBps, feeRecipient, metadataUri } = params;
const [resolverEntry] = deriveResolverEntryPda(authority);
return await (this.program.methods as any)
.registerResolver(resolverType, name, description, feeBps, feeRecipient, metadataUri)
.accounts({
authority,
resolverEntry,
systemProgram: SystemProgram.programId,
})
.instruction();
}
async buildUpdateResolver(params: {
authority: PublicKey;
name: string;
description: string;
feeBps: number;
metadataUri: string;
}): Promise<TransactionInstruction> {
const { authority, name, description, feeBps, metadataUri } = params;
const [resolverEntry] = deriveResolverEntryPda(authority);
return await (this.program.methods as any)
.updateResolver(name, description, feeBps, metadataUri)
.accounts({
authority,
resolverEntry,
})
.instruction();
}
async fetchResolver(authority: PublicKey): Promise<ResolverEntry | null> {
try {
const [pda] = deriveResolverEntryPda(authority);
const raw = await (this.program.account as any).resolverEntry.fetch(pda);
return raw as ResolverEntry;
} catch {
return null;
}
}
async fetchResolverByPda(pda: PublicKey): Promise<ResolverEntry | null> {
try {
const raw = await (this.program.account as any).resolverEntry.fetch(pda);
return raw as ResolverEntry;
} catch {
return null;
}
}
async fetchAllResolvers(): Promise<ResolverEntryWithPda[]> {
const accounts = await (this.program.account as any).resolverEntry.all();
return accounts.map((a: { publicKey: PublicKey; account: ResolverEntry }) => ({
pda: a.publicKey,
account: a.account as ResolverEntry,
}));
}
}
export * from "./generated/descro_ext_resolvers/src/generated/accounts";

View File

@@ -1,87 +1,68 @@
import { PublicKey } from "@solana/web3.js";
import BN from "bn.js";
export type {
EscrowAccount,
EscrowAccountArgs,
} from "./generated/descro/src/generated/accounts/escrowAccount";
export type {
EscrowState,
EscrowStateArgs,
} from "./generated/descro/src/generated/types/escrowState";
export type {
Winner,
WinnerArgs,
} from "./generated/descro/src/generated/types/winner";
export type {
ResolverEntry,
ResolverEntryArgs,
} from "./generated/descro_ext_resolvers/src/generated/accounts/resolverEntry";
export type {
ResolverType,
ResolverTypeArgs,
} from "./generated/descro_ext_resolvers/src/generated/types/resolverType";
export type { Address } from "@solana/kit";
export type EscrowState =
| { awaitingDeposit: Record<string, never> }
| { active: Record<string, never> }
| { disputed: Record<string, never> }
| { complete: Record<string, never> }
| { cancelled: Record<string, never> };
import { EscrowState } from "./generated/descro/src/generated/types/escrowState";
import { ResolverType } from "./generated/descro_ext_resolvers/src/generated/types/resolverType";
import type { EscrowAccount } from "./generated/descro/src/generated/accounts/escrowAccount";
import type { ResolverEntry } from "./generated/descro_ext_resolvers/src/generated/accounts/resolverEntry";
import type { Address } from "@solana/kit";
export type Winner = { buyer: Record<string, never> } | { seller: Record<string, never> };
export type ResolverType =
| { centralAuthority: Record<string, never> }
| { juryDAO: Record<string, never> }
| { mad: Record<string, never> }
| { algorithmic: Record<string, never> }
| { multisig: Record<string, never> };
export interface EscrowAccount {
seller: PublicKey;
buyer: PublicKey;
amount: BN;
disputeResolver: PublicKey | null;
state: EscrowState;
bump: number;
vaultBump: number;
escrowId: BN;
}
export interface ResolverEntry {
authority: PublicKey;
resolverType: ResolverType;
name: string;
description: string;
feeBps: number;
feeRecipient: PublicKey;
metadataUri: string;
totalResolved: BN;
ruledForBuyer: BN;
ruledForSeller: BN;
registeredAt: BN;
}
export interface EscrowAccountWithPda {
pda: PublicKey;
account: EscrowAccount;
}
export interface ResolverEntryWithPda {
pda: PublicKey;
account: ResolverEntry;
}
export type EscrowAccountWithPda = { pda: Address; account: EscrowAccount };
export type ResolverEntryWithPda = { pda: Address; account: ResolverEntry };
export function isAwaitingDeposit(state: EscrowState): boolean {
return "awaitingDeposit" in state;
return state === EscrowState.AwaitingDeposit;
}
export function isActive(state: EscrowState): boolean {
return "active" in state;
return state === EscrowState.Active;
}
export function isDisputed(state: EscrowState): boolean {
return "disputed" in state;
return state === EscrowState.Disputed;
}
export function isComplete(state: EscrowState): boolean {
return "complete" in state;
return state === EscrowState.Complete;
}
export function isCancelled(state: EscrowState): boolean {
return "cancelled" in state;
return state === EscrowState.Cancelled;
}
export function escrowStateLabel(state: EscrowState): string {
if (isAwaitingDeposit(state)) return "AwaitingDeposit";
if (isActive(state)) return "Active";
if (isDisputed(state)) return "Disputed";
if (isComplete(state)) return "Complete";
if (isCancelled(state)) return "Cancelled";
return "Unknown";
switch (state) {
case EscrowState.AwaitingDeposit: return "AwaitingDeposit";
case EscrowState.Active: return "Active";
case EscrowState.Disputed: return "Disputed";
case EscrowState.Complete: return "Complete";
case EscrowState.Cancelled: return "Cancelled";
default: return "Unknown";
}
}
export function resolverTypeLabel(rt: ResolverType): string {
if ("centralAuthority" in rt) return "CentralAuthority";
if ("juryDAO" in rt) return "JuryDAO";
if ("mad" in rt) return "MAD";
if ("algorithmic" in rt) return "Algorithmic";
if ("multisig" in rt) return "Multisig";
return "Unknown";
switch (rt) {
case ResolverType.CentralAuthority: return "CentralAuthority";
case ResolverType.JuryDAO: return "JuryDAO";
case ResolverType.MAD: return "MAD";
case ResolverType.Algorithmic: return "Algorithmic";
case ResolverType.Multisig: return "Multisig";
default: return "Unknown";
}
}