@codama/dynamic-client
v0.1.3
Published
Client to dynamically interact with Solana programs using Codama IDLs
Maintainers
Readme
Codama ➤ Dynamic Client
This package provides a runtime Solana program client to dynamically interact with Solana programs using Codama IDLs — with optional TypeScript type generation for full type safety. It contains PDA derivation, and an instruction builder powered by @codama/dynamic-instructions.
Installation
pnpm install @codama/dynamic-client[!NOTE] This package is not included in the main
codamapackage.
Quick Start
Untyped
import { createProgramClient } from '@codama/dynamic-client';
import idl from './my-program-idl.json';
const client = createProgramClient(idl);
const instruction = await client.methods
.transferSol({ amount: 1_000_000_000 })
.accounts({ source: senderAddress, destination: receiverAddress })
.instruction();Typed with generated types
import { createProgramClient } from '@codama/dynamic-client';
import type { MyProgramClient } from './generated/my-program-types';
import idl from './my-program-idl.json';
const client = createProgramClient<MyProgramClient>(idl);
// client.methods, .accounts(), args are now fully typedAPI Reference
createProgramClient<T>(idl, options?)
Creates a program client from a Codama IDL.
| Parameter | Type | Description |
| ------------------- | ------------------ | ----------------------------------------- |
| idl | object \| string | Codama IDL object or JSON string |
| options.programId | AddressInput | Override the program address from the IDL |
Returns a ProgramClient (or T when a type parameter is provided).
ProgramClient
type InstructionName = CamelCaseString;
type AccountName = CamelCaseString;
type ProgramClient = {
methods: Record<InstructionName, (args?) => ProgramMethodBuilder>;
pdas?: Record<AccountName, (seeds?) => Promise<ProgramDerivedAddress>>;
programAddress: Address;
instructions: Map<InstructionName, InstructionNode>;
root: RootNode;
};ProgramMethodBuilder (fluent API)
client.methods
.myInstruction(args) // provide instruction arguments
.accounts(accounts) // provide account addresses
.signers(['accountName']) // optionally mark ambiguous accounts as signers
.resolvers({ customResolver: async (argumentsInput, accountsInput) => {} }) // optionally provide custom resolver according to resolverValueNode in IDL
.instruction(); // Promise<Instruction>AddressInput
Accepts any of:
Address(from@solana/addresses)- Legacy
PublicKey(any object with.toBase58()) - Base58 string
Accounts
Accounts with a defaultValue in the IDL (PDAs, program ids, constants) are resolved automatically and may be omitted from .accounts().
Automatic resolution rules
For the full table of automatic resolution rules, see @codama/dynamic-address-resolution.
Optional accounts
Pass null for optional accounts to be resolved according to optionalAccountStrategy (either will be omitted or replaced on programId):
.accounts({
authority,
program: programAddress,
programData: null, // optional - resolved via optionalAccountStrategy
})Ambiguous signers
When an account has isSigner: 'either' in the IDL, use .signers() to explicitly mark it:
.accounts({ owner: ownerAddress })
.signers(['owner'])Custom resolvers
When an account or argument is resolverValueNode in the IDL, provide a custom resolver function .resolvers({ [resolverName]: async fn }) to help with account/arguments resolution:
client.methods
.create({ tokenStandard: 'NonFungible' })
.accounts({ owner: ownerAddress })
.resolvers({
resolveIsNonFungible: async (argumentsInput, accountsInput) => {
return argumentsInput.tokenStandard === 'NonFungible';
},
});PDA Derivation
Standalone
const [address, bump] = await client.pdas.canonical({
program: programAddress,
seed: 'idl',
});Auto-derived in instructions
Accounts with pdaValueNode defaults are resolved automatically. Seeds are pulled from other accounts and arguments in the instruction:
// metadata PDA is auto-derived from program + seed
const ix = await client.methods
.initialize({ seed: 'idl', data: myData /* ... */ })
.accounts({ authority, program: programAddress, programData })
.instruction();Nested/dependent PDAs (where one PDA seed references another PDA) are resolved recursively.
Arguments
Arguments with defaultValueStrategy: 'omitted' (e.g., discriminators) are auto-encoded and should not be provided.
Error Handling
All errors are instances of CodamaError from @codama/errors:
import { CodamaError, isCodamaError, CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_MISSING } from '@codama/dynamic-client';
try {
const ix = await client.methods.transferSol({ amount: 100 }).accounts({}).instruction();
} catch (err) {
if (isCodamaError(err, CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_MISSING)) {
console.error(`Missing account: ${err.context.accountName}`);
}
}CLI
The package includes a CLI for generating TypeScript types from Codama IDL files.
npx @codama/dynamic-client generate-client-types <codama-idl.json> <output-dir>Example:
npx @codama/dynamic-client generate-client-types ./idl/codama.json ./generatedThis reads the IDL file and writes a *-types.ts file to the output directory containing strongly-typed interfaces for all instructions, accounts, arguments, PDAs, and the program client.
generateClientTypes(idl)
The same is available as a TypeScript function:
import { generateClientTypes } from '@codama/dynamic-client';
import type { RootNode } from 'codama';
import { readFileSync, writeFileSync } from 'node:fs';
const idl: RootNode = JSON.parse(readFileSync('./my-program-idl.json', 'utf-8'));
const typesSource = generateClientTypes(idl);
writeFileSync('./generated/my-program-idl-types.ts', typesSource);Utilities
import { toAddress, isPublicKeyLike } from '@codama/dynamic-client';
// Convert any AddressInput to Address
const addr = toAddress('11111111111111111111111111111111');
const addr2 = toAddress(new PublicKey('...'));
// Type guard for legacy PublicKey objects
if (isPublicKeyLike(value)) {
const addr = toAddress(value);
}