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

View File

@@ -1,193 +1,68 @@
import type { Address, GetProgramAccountsApi, Lamports, Rpc } from "@solana/kit";
import { import {
PublicKey, fetchMaybeEscrowAccount,
SystemProgram, fetchEscrowAccount,
TransactionInstruction, decodeEscrowAccount,
} from "@solana/web3.js"; getEscrowAccountDiscriminatorBytes,
import { AnchorProvider, Program, BN } from "@coral-xyz/anchor"; } from "./generated/descro/src/generated/accounts/escrowAccount";
import type { Idl } from "@coral-xyz/anchor"; import { DESCRO_PROGRAM_ADDRESS } from "./generated/descro/src/generated/programs/descro";
import descroIdl from "./idl/descro.json"; import type { EscrowAccountWithPda } from "./types";
import {
REGISTRY_PROGRAM_ID,
deriveEscrowPda,
deriveVaultPda,
deriveEscrowAuthorityPda,
deriveResolverEntryPda,
} from "./pda";
import type { EscrowAccount, EscrowAccountWithPda, Winner } from "./types";
export class EscrowClient { export { fetchEscrowAccount, fetchMaybeEscrowAccount, decodeEscrowAccount };
readonly program: Program;
readonly provider: AnchorProvider;
constructor(provider: AnchorProvider) { export async function fetchEscrowsForWallet(
this.provider = provider; rpc: Rpc<GetProgramAccountsApi>,
this.program = new Program(descroIdl as Idl, provider); walletAddress: Address,
} ): Promise<EscrowAccountWithPda[]> {
const discriminatorBase64 = btoa(
String.fromCharCode(...getEscrowAccountDiscriminatorBytes()),
) as never;
async buildCreateEscrow(params: { const walletBytes = walletAddress as never;
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);
return await (this.program.methods as any) const [asSeller, asBuyer] = await Promise.all([
.createEscrow(amount, disputeResolver, escrowId) rpc
.accounts({ .getProgramAccounts(DESCRO_PROGRAM_ADDRESS, {
seller, filters: [
buyer, { memcmp: { offset: 0n, bytes: discriminatorBase64, encoding: "base64" } },
escrowAccount: escrowPda, { memcmp: { offset: 8n, bytes: walletBytes, encoding: "base58" } },
vault: vaultPda, ],
systemProgram: SystemProgram.programId, encoding: "base64",
}) })
.instruction(); .send(),
} rpc
.getProgramAccounts(DESCRO_PROGRAM_ADDRESS, {
async buildDeposit(params: { filters: [
buyer: PublicKey; { memcmp: { offset: 0n, bytes: discriminatorBase64, encoding: "base64" } },
escrowPda: PublicKey; { memcmp: { offset: 40n, bytes: walletBytes, encoding: "base58" } },
}): Promise<TransactionInstruction> { ],
const { buyer, escrowPda } = params; encoding: "base64",
const [vaultPda] = deriveVaultPda(escrowPda);
return await (this.program.methods as any)
.deposit()
.accounts({
buyer,
escrowAccount: escrowPda,
vault: vaultPda,
systemProgram: SystemProgram.programId,
}) })
.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: { return results;
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;
}
} }

View File

@@ -2,18 +2,4 @@ export * from "./types";
export * from "./pda"; export * from "./pda";
export * from "./escrow"; export * from "./escrow";
export * from "./registry"; export * from "./registry";
export * from "./listener"; 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);
}
}

View File

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

View File

@@ -1,52 +1,17 @@
import { PublicKey } from "@solana/web3.js"; export {
import BN from "bn.js"; findEscrowAccountPda,
type EscrowAccountSeeds,
export const ESCROW_PROGRAM_ID = new PublicKey( } from "./generated/descro/src/generated/pdas/escrowAccount";
"DjVR4EuYV6USMJFfsGZwhZ3y8rtWsmG8EvDY96GTqqi3" export {
); findVaultPda,
export const REGISTRY_PROGRAM_ID = new PublicKey( type VaultSeeds,
"GwUPAKs3HHzCpj8uhet4NAnxk9GWNwfrYbpihu5DyFp" } from "./generated/descro/src/generated/pdas/vault";
); export {
findEscrowAuthorityPda,
export function deriveEscrowPda( } from "./generated/descro/src/generated/pdas/escrowAuthority";
seller: PublicKey, export {
escrowId: BN | bigint | number, findResolverEntryPda,
programId: PublicKey = ESCROW_PROGRAM_ID type ResolverEntrySeeds,
): [PublicKey, number] { } from "./generated/descro_ext_resolvers/src/generated/pdas/resolverEntry";
const id = new BN(escrowId.toString()); export { DESCRO_PROGRAM_ADDRESS } from "./generated/descro/src/generated/programs/descro";
const idBuf = Buffer.alloc(8); export { DESCRO_EXT_RESOLVERS_PROGRAM_ADDRESS } from "./generated/descro_ext_resolvers/src/generated/programs/descroExtResolvers";
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
);
}

View File

@@ -1,84 +1 @@
import { PublicKey, TransactionInstruction, SystemProgram } from "@solana/web3.js"; export * from "./generated/descro_ext_resolvers/src/generated/accounts";
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,
}));
}
}

View File

@@ -1,87 +1,68 @@
import { PublicKey } from "@solana/web3.js"; export type {
import BN from "bn.js"; 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 = import { EscrowState } from "./generated/descro/src/generated/types/escrowState";
| { awaitingDeposit: Record<string, never> } import { ResolverType } from "./generated/descro_ext_resolvers/src/generated/types/resolverType";
| { active: Record<string, never> } import type { EscrowAccount } from "./generated/descro/src/generated/accounts/escrowAccount";
| { disputed: Record<string, never> } import type { ResolverEntry } from "./generated/descro_ext_resolvers/src/generated/accounts/resolverEntry";
| { complete: Record<string, never> } import type { Address } from "@solana/kit";
| { cancelled: Record<string, never> };
export type Winner = { buyer: Record<string, never> } | { seller: Record<string, never> }; export type EscrowAccountWithPda = { pda: Address; account: EscrowAccount };
export type ResolverEntryWithPda = { pda: Address; account: ResolverEntry };
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 function isAwaitingDeposit(state: EscrowState): boolean { export function isAwaitingDeposit(state: EscrowState): boolean {
return "awaitingDeposit" in state; return state === EscrowState.AwaitingDeposit;
} }
export function isActive(state: EscrowState): boolean { export function isActive(state: EscrowState): boolean {
return "active" in state; return state === EscrowState.Active;
} }
export function isDisputed(state: EscrowState): boolean { export function isDisputed(state: EscrowState): boolean {
return "disputed" in state; return state === EscrowState.Disputed;
} }
export function isComplete(state: EscrowState): boolean { export function isComplete(state: EscrowState): boolean {
return "complete" in state; return state === EscrowState.Complete;
} }
export function isCancelled(state: EscrowState): boolean { export function isCancelled(state: EscrowState): boolean {
return "cancelled" in state; return state === EscrowState.Cancelled;
} }
export function escrowStateLabel(state: EscrowState): string { export function escrowStateLabel(state: EscrowState): string {
if (isAwaitingDeposit(state)) return "AwaitingDeposit"; switch (state) {
if (isActive(state)) return "Active"; case EscrowState.AwaitingDeposit: return "AwaitingDeposit";
if (isDisputed(state)) return "Disputed"; case EscrowState.Active: return "Active";
if (isComplete(state)) return "Complete"; case EscrowState.Disputed: return "Disputed";
if (isCancelled(state)) return "Cancelled"; case EscrowState.Complete: return "Complete";
return "Unknown"; case EscrowState.Cancelled: return "Cancelled";
default: return "Unknown";
}
} }
export function resolverTypeLabel(rt: ResolverType): string { export function resolverTypeLabel(rt: ResolverType): string {
if ("centralAuthority" in rt) return "CentralAuthority"; switch (rt) {
if ("juryDAO" in rt) return "JuryDAO"; case ResolverType.CentralAuthority: return "CentralAuthority";
if ("mad" in rt) return "MAD"; case ResolverType.JuryDAO: return "JuryDAO";
if ("algorithmic" in rt) return "Algorithmic"; case ResolverType.MAD: return "MAD";
if ("multisig" in rt) return "Multisig"; case ResolverType.Algorithmic: return "Algorithmic";
return "Unknown"; case ResolverType.Multisig: return "Multisig";
default: return "Unknown";
}
} }