-
Notifications
You must be signed in to change notification settings - Fork 81
feat: add dynamic-client #987
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
lorisleiva
merged 11 commits into
codama-idl:main
from
hoodieshq:main-dynamic-instructions
Apr 14, 2026
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
736aa2b
feat: add dynamic-instructions
mikhd 6a78cb0
fix: dont regenerate codama idl during ci for custom anchor programs …
mikhd 2082d7b
chore: refactor create-program-client getRoot (#8)
mikhd 32704fd
fix: replace concatBytes with mergeBytes (#5)
mikhd dcd4da7
fix: replace TextEncoder with Utf8Encoder (#6)
mikhd d977ed7
feat: update litesvm@1.0.0 (#9)
mikhd 62075c0
chore: update anchor custom codama idls
mikhd 64dc6cc
feat: extract dynamic-instructions errors to @codama/errors
mikhd fd71384
chore: rename dynamic-instructions -> dynamic-client
mikhd 209d73e
chore: dynamic-client changeset
mikhd a4ce63c
fix: dynamic client minor issues
mikhd File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "@codama/dynamic-client": minor | ||
| "@codama/errors": minor | ||
| --- | ||
|
|
||
| Add `dynamic-client` - a runtime Solana instruction builder |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| name: Setup Anchor CLI | ||
| description: Install Rust toolchain and a pinned Anchor CLI version via AVM with caching for CI. | ||
|
|
||
| inputs: | ||
| anchor-version: | ||
| description: Anchor CLI version to install (e.g. 0.32.1) | ||
| required: true | ||
| rust-version: | ||
| description: Rust toolchain version to install (e.g. 1.93.0) | ||
| required: true | ||
| cargo-lock-path: | ||
| description: Path to Cargo.lock for cache key hashing | ||
| required: false | ||
| default: packages/dynamic-client/test/programs/anchor/Cargo.lock | ||
| build-target-path: | ||
| description: Path to the build target directory to cache | ||
| required: false | ||
|
lorisleiva marked this conversation as resolved.
|
||
| default: packages/dynamic-client/test/programs/anchor/target | ||
|
|
||
| runs: | ||
| using: composite | ||
| steps: | ||
| - name: Setup Rust | ||
| uses: dtolnay/rust-toolchain@stable | ||
| with: | ||
| toolchain: ${{ inputs.rust-version }} | ||
|
|
||
| - name: Cache Rust and Anchor toolchain | ||
| uses: actions/cache@v4 | ||
| with: | ||
| path: | | ||
| ~/.cargo/registry | ||
| ~/.cargo/git | ||
| ~/.cargo/bin/avm | ||
| ~/.avm | ||
| ${{ inputs.build-target-path }} | ||
| key: ${{ runner.os }}-rust-anchor-${{ inputs.rust-version }}-${{ inputs.anchor-version }}-${{ inputs.cargo-lock-path && hashFiles(inputs.cargo-lock-path) || 'no-lock' }} | ||
| restore-keys: | | ||
| ${{ runner.os }}-rust-anchor-${{ inputs.rust-version }}-${{ inputs.anchor-version }}- | ||
| ${{ runner.os }}-rust-anchor-${{ inputs.rust-version }}- | ||
|
|
||
| - name: Install Anchor (pinned) | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" | ||
| echo "$HOME/.avm/bin" >> "$GITHUB_PATH" | ||
| export PATH="$HOME/.cargo/bin:$HOME/.avm/bin:$PATH" | ||
|
|
||
| desired="${{ inputs.anchor-version }}" | ||
|
|
||
| # Install AVM if needed. | ||
| if ! command -v avm >/dev/null 2>&1; then | ||
| sudo apt-get update | ||
| sudo apt-get install -y pkg-config libssl-dev | ||
| cargo install --git https://github.com/coral-xyz/anchor avm --locked | ||
| fi | ||
|
|
||
| # Ensure the desired Anchor version exists and is active. | ||
| avm install "$desired" | ||
| avm use "$desired" | ||
|
|
||
| anchor --version | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| dist/ | ||
| test/programs/anchor/.anchor/ | ||
| test/programs/anchor/target/ | ||
| test/programs/generated/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| dist/ | ||
| test/e2e/ | ||
| test-ledger/ | ||
| target/ | ||
| CHANGELOG.md | ||
| generated/ | ||
| idls/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| MIT License | ||
|
|
||
| Copyright (c) 2025 Codama | ||
|
|
||
| Permission is hereby granted, free of charge, to any person obtaining | ||
| a copy of this software and associated documentation files (the | ||
| "Software"), to deal in the Software without restriction, including | ||
| without limitation the rights to use, copy, modify, merge, publish, | ||
| distribute, sublicense, and/or sell copies of the Software, and to | ||
| permit persons to whom the Software is furnished to do so, subject to | ||
| the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice shall be | ||
| included in all copies or substantial portions of the Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||
| EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||
| MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||
| NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | ||
| LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | ||
| OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | ||
| WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,242 @@ | ||
| # Codama ➤ Dynamic Client | ||
|
|
||
| [![npm][npm-image]][npm-url] | ||
| [![npm-downloads][npm-downloads-image]][npm-url] | ||
|
|
||
| [npm-downloads-image]: https://img.shields.io/npm/dm/@codama/dynamic-client.svg?style=flat | ||
| [npm-image]: https://img.shields.io/npm/v/@codama/dynamic-client.svg?style=flat&label=%40codama%2Fdynamic-client | ||
| [npm-url]: https://www.npmjs.com/package/@codama/dynamic-client | ||
|
|
||
| This package provides a runtime Solana instruction builder that dynamically constructs `Instruction` (`@solana/instructions`) from Codama IDL and provides type generation for full TypeScript type safety. | ||
|
|
||
| ## Installation | ||
|
|
||
| ```sh | ||
| pnpm install @codama/dynamic-client | ||
| ``` | ||
|
|
||
| > [!NOTE] | ||
| > This package is **not** included in the main [`codama`](../library) package. | ||
|
|
||
| ## Quick Start | ||
|
|
||
| ### Untyped | ||
|
|
||
| ```ts | ||
| 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 | ||
|
|
||
| ```ts | ||
| 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 typed | ||
| ``` | ||
|
|
||
| ## API 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` | ||
|
|
||
| ```ts | ||
| 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) | ||
|
|
||
| ```ts | ||
| 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 | ||
|
|
||
| ### Automatic resolution rules | ||
|
|
||
| Accounts (pda, program ids) with `defaultValue` are resolved automatically, hence can be omitted. | ||
|
|
||
| | Account scenario | Type in `.accounts()` | Auto resolution | | ||
| | --------------------------------------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------- | | ||
| | Required account without `defaultValue` | `{ system: Address }` | No | | ||
| | Required account with `defaultValue`<br>(PDA, programId, etc.) | `{ system?: Address }` | Auto-resolved to `defaultValue` if omitted | | ||
| | Optional account (`isOptional: true`)<br>without `defaultValue` | `{ system: Address \| null }` | Resolved via `optionalAccountStrategy`,<br>if provided as `null` | | ||
| | Optional account (`isOptional: true`)<br>with `defaultValue` | `{ system?: Address \| null }` | - `null` resolves via `optionalAccountStrategy`<br>- `undefined` resolves via `defaultValue` | | ||
|
|
||
| ### Auto-resolved account addresses | ||
|
|
||
| Accounts with `defaultValue` in the IDL are automatically resolved when omitted from `.accounts()`. This includes: | ||
|
|
||
| - **PDA accounts** — derived from seeds defined in the IDL | ||
| - **Program IDs** — resolved to known program addresses (e.g., System Program, Token Program) | ||
| - **Constants** — resolved from constant value nodes | ||
|
|
||
| You can always override auto-derived accounts by providing an explicit address. | ||
|
|
||
| ### Optional accounts | ||
|
|
||
| Pass `null` for optional accounts to be resolved according to `optionalAccountStrategy` (either will be `omitted` or replaced on `programId`): | ||
|
|
||
| ```ts | ||
| .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: | ||
|
|
||
| ```ts | ||
| .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: | ||
|
|
||
| ```ts | ||
| client.methods | ||
| .create({ tokenStandard: 'NonFungible' }) | ||
| .accounts({ owner: ownerAddress }) | ||
| .resolvers({ | ||
| resolveIsNonFungible: async (argumentsInput, accountsInput) => { | ||
| return argumentsInput.tokenStandard === 'NonFungible'; | ||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| ## PDA Derivation | ||
|
|
||
| ### Standalone | ||
|
|
||
| ```ts | ||
| 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: | ||
|
|
||
| ```ts | ||
| // 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`: | ||
|
|
||
| ```ts | ||
| 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. | ||
|
|
||
| ```sh | ||
| npx @codama/dynamic-client generate-client-types <codama-idl.json> <output-dir> | ||
| ``` | ||
|
|
||
| Example: | ||
|
|
||
| ```sh | ||
| npx @codama/dynamic-client generate-client-types ./idl/codama.json ./generated | ||
| ``` | ||
|
|
||
| This 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: | ||
|
|
||
| ```ts | ||
| 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 | ||
|
|
||
| ```ts | ||
| 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); | ||
| } | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| const run = require('../dist/cli.cjs').run; | ||
|
|
||
| run(process.argv); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.