Skip to content

Commit 3ed69d8

Browse files
committed
Port the dynamic client to Codama v2
1 parent d543524 commit 3ed69d8

56 files changed

Lines changed: 23776 additions & 26931 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎packages/dynamic-address-resolution/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,12 @@ When no branch matches, optional accounts resolve using the optional account str
8888

8989
## `resolveStandalonePda(input)`
9090

91-
Derives a PDA outside of any instruction, from its path and the values of its variable seeds.
91+
Derives a PDA outside of any instruction, from its path and the values of its variable seeds. It uses the given `programId`, or else the `programId` of the PDA, or else the address of the program defining it. Pass a `programId` for PDAs that instructions derive from another program through their `pdaValueNode.programId`.
9292

9393
```ts
9494
const [address, bump] = await resolveStandalonePda({
9595
path: [root, root.program, metadataPda],
96+
programId: otherProgramAddress, // optional
9697
seedsInput: { authority, seed: 'idl' },
9798
});
9899
```

‎packages/dynamic-address-resolution/src/codegen/generate-pda-types.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { codamaTypeToTS } from './codama-type-to-ts';
44
import { collectPdaNodesFromIdl } from './collect-pda-nodes';
55

66
/**
7-
* Generate `${Pda}Seeds` types and the aggregate `${Program}Pdas` map type.
7+
* Generate `${Pda}Seeds` types and the aggregate `${Program}Pdas` map type, whose
8+
* functions take an optional `programId` overriding the program deriving the PDA.
89
*
910
* Returns the type block with the aggregate map type name. `mapTypeName` is `null` when the program has no PDAs.
1011
*/
@@ -40,7 +41,7 @@ export function generatePdaTypes(idl: RootNode): { mapTypeName: string | null; t
4041
const typeName = pascalCase(pdaName);
4142
const seedsParam =
4243
getVariableSeedNodes(pdaNode).length > 0 ? `seeds: ${typeName}Seeds` : `seeds?: Record<string, unknown>`;
43-
output += ` ${pdaName}: (${seedsParam}) => Promise<ProgramDerivedAddress>;\n`;
44+
output += ` ${pdaName}: (${seedsParam}, options?: { programId?: Address }) => Promise<ProgramDerivedAddress>;\n`;
4445
}
4546
output += '};\n\n';
4647

‎packages/dynamic-address-resolution/src/resolvers/resolve-standalone-pda.ts‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,28 @@ import type { ProgramDerivedAddress } from '@solana/addresses';
22
import type { NodePath, PdaNode } from 'codama';
33
import { ProvidedScope } from 'codama';
44

5+
import { type AddressInput, toAddress } from '../shared/address';
56
import { getLinkables } from './context';
67
import { derivePda, getPdaProgramAddress } from './resolve-pda-address';
78

89
export type ResolveStandalonePdaInput = {
910
/** The path of the PDA from the root node, e.g. `[root, program, pda]`. */
1011
path: NodePath<PdaNode>;
12+
/**
13+
* The program deriving the PDA, overriding the `programId` of the PDA and,
14+
* without one, the program defining it. Instructions may derive a PDA from
15+
* another program through their `pdaValueNode.programId`, which this mirrors
16+
* outside of any instruction.
17+
*/
18+
programId?: AddressInput;
1119
/** The values of the variable seeds of the PDA, keyed by seed identifier. */
1220
seedsInput?: Record<string, unknown>;
1321
};
1422

1523
/**
16-
* Derive a PDA from its seed values, outside of any instruction.
24+
* Derive a PDA from its seed values, outside of any instruction. The PDA is
25+
* derived from the given `programId`, or else the `programId` of the PDA, or
26+
* else the address of the program defining it.
1727
*
1828
* @example
1929
* ```ts
@@ -25,10 +35,10 @@ export type ResolveStandalonePdaInput = {
2535
*/
2636
export async function resolveStandalonePda({
2737
path,
38+
programId,
2839
seedsInput = {},
2940
}: ResolveStandalonePdaInput): Promise<ProgramDerivedAddress> {
3041
const ctx = { linkables: getLinkables(path), scope: new ProvidedScope() };
31-
return await derivePda(path, getPdaProgramAddress(path), ctx, seedNode =>
32-
Promise.resolve(seedsInput[seedNode.identifier]),
33-
);
42+
const programAddress = programId === undefined ? getPdaProgramAddress(path) : toAddress(programId);
43+
return await derivePda(path, programAddress, ctx, seedNode => Promise.resolve(seedsInput[seedNode.identifier]));
3444
}

‎packages/dynamic-address-resolution/test/codegen/generate-pda-types.test.ts‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ describe('generatePdaTypes', () => {
4949
expect(typeBlock).toContain('export type ConfigSeeds');
5050
expect(typeBlock).toContain('authority: Address;');
5151
expect(typeBlock).toContain('export type MyProgramPdas');
52-
expect(typeBlock).toContain('config: (seeds: ConfigSeeds) => Promise<ProgramDerivedAddress>;');
52+
expect(typeBlock).toContain(
53+
'config: (seeds: ConfigSeeds, options?: { programId?: Address }) => Promise<ProgramDerivedAddress>;',
54+
);
5355
});
5456

5557
test('should discover inline PDAs on instruction account defaults', () => {
@@ -79,7 +81,9 @@ describe('generatePdaTypes', () => {
7981
const { mapTypeName, typeBlock } = generatePdaTypes(root);
8082
expect(mapTypeName).toBe('InlineProgramPdas');
8183
expect(typeBlock).toContain('export type InlineSeeds');
82-
expect(typeBlock).toContain('inline: (seeds: InlineSeeds) => Promise<ProgramDerivedAddress>;');
84+
expect(typeBlock).toContain(
85+
'inline: (seeds: InlineSeeds, options?: { programId?: Address }) => Promise<ProgramDerivedAddress>;',
86+
);
8387
});
8488

8589
test('should emit seedless variant for PDAs with only constant seeds', () => {
@@ -97,7 +101,9 @@ describe('generatePdaTypes', () => {
97101
);
98102
const { typeBlock } = generatePdaTypes(root);
99103
expect(typeBlock).not.toContain('FixedSeeds');
100-
expect(typeBlock).toContain('fixed: (seeds?: Record<string, unknown>) => Promise<ProgramDerivedAddress>;');
104+
expect(typeBlock).toContain(
105+
'fixed: (seeds?: Record<string, unknown>, options?: { programId?: Address }) => Promise<ProgramDerivedAddress>;',
106+
);
101107
});
102108

103109
test('should use string seed type', () => {

‎packages/dynamic-address-resolution/test/resolvers/resolve-standalone-pda.test.ts‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { CODAMA_ERROR__DYNAMIC_CLIENT__PDA_SEED_MISSING, CodamaError } from '@codama/errors';
2-
import { getAddressEncoder, getProgramDerivedAddress } from '@solana/addresses';
2+
import { type Address, getAddressEncoder, getProgramDerivedAddress } from '@solana/addresses';
33
import { getU32Encoder, getUtf8Encoder } from '@solana/codecs';
44
import {
55
constantPdaSeedNode,
@@ -92,4 +92,45 @@ describe('resolveStandalonePda', () => {
9292
}),
9393
);
9494
});
95+
96+
describe('program address', () => {
97+
const pdaSeeds = [
98+
constantPdaSeedNode(stringTypeNode('utf8'), stringValueNode('vault')),
99+
constantPdaSeedNode(publicKeyTypeNode(), programIdValueNode()),
100+
];
101+
102+
async function deriveFrom(programAddress: Address) {
103+
return await getProgramDerivedAddress({
104+
programAddress,
105+
seeds: [getUtf8Encoder().encode('vault'), getAddressEncoder().encode(programAddress)],
106+
});
107+
}
108+
109+
test('it derives PDAs from the program defining them by default', async () => {
110+
const pda = pdaNode({ identifier: 'vault', seeds: pdaSeeds });
111+
expect(await resolveStandalonePda({ path: getPdaPath(pda) })).toEqual(await deriveFrom(PROGRAM_ADDRESS));
112+
});
113+
114+
test('it derives PDAs from their own programId', async () => {
115+
const pdaProgram = await generateAddress();
116+
const pda = pdaNode({ identifier: 'vault', programId: pdaProgram, seeds: pdaSeeds });
117+
expect(await resolveStandalonePda({ path: getPdaPath(pda) })).toEqual(await deriveFrom(pdaProgram));
118+
});
119+
120+
test('it derives PDAs from the given programId over the one of the PDA', async () => {
121+
const [pdaProgram, givenProgram] = await Promise.all([generateAddress(), generateAddress()]);
122+
const pda = pdaNode({ identifier: 'vault', programId: pdaProgram, seeds: pdaSeeds });
123+
expect(await resolveStandalonePda({ path: getPdaPath(pda), programId: givenProgram })).toEqual(
124+
await deriveFrom(givenProgram),
125+
);
126+
});
127+
128+
test('it accepts the given programId as a base58 string', async () => {
129+
const givenProgram = await generateAddress();
130+
const pda = pdaNode({ identifier: 'vault', seeds: pdaSeeds });
131+
expect(await resolveStandalonePda({ path: getPdaPath(pda), programId: `${givenProgram}` })).toEqual(
132+
await deriveFrom(givenProgram),
133+
);
134+
});
135+
});
95136
});

‎packages/dynamic-client/README.md‎

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
[npm-image]: https://img.shields.io/npm/v/@codama/dynamic-client.svg?style=flat&label=%40codama%2Fdynamic-client
88
[npm-url]: https://www.npmjs.com/package/@codama/dynamic-client
99

10-
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](../dynamic-instructions/README.md).
10+
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](../dynamic-instructions/README.md).
1111

1212
## Installation
1313

@@ -42,14 +42,20 @@ import type { MyProgramClient } from './generated/my-program-types';
4242
import idl from './my-program-idl.json';
4343

4444
const client = createProgramClient<MyProgramClient>(idl);
45-
// client.methods, .accounts(), args are now fully typed
45+
// client.methods, their data and .accounts() are now fully typed
4646
```
4747

4848
## API Reference
4949

5050
### `createProgramClient<T>(idl, options?)`
5151

52-
Creates a program client from a Codama IDL.
52+
Creates a program client from a Codama IDL of the latest major. IDLs of older majors (e.g. v1) throw `CODAMA_ERROR__VERSION_MISMATCH`, so upgrade them first with [`@codama/upgrade`](../upgrade):
53+
54+
```ts
55+
import { upgrade } from '@codama/upgrade';
56+
57+
const client = createProgramClient(upgrade(v1Idl));
58+
```
5359

5460
| Parameter | Type | Description |
5561
| ------------------- | ------------------ | ----------------------------------------- |
@@ -61,14 +67,11 @@ Returns a `ProgramClient` (or `T` when a type parameter is provided).
6167
### `ProgramClient`
6268

6369
```ts
64-
type InstructionName = CamelCaseString;
65-
type AccountName = CamelCaseString;
66-
6770
type ProgramClient = {
68-
methods: Record<InstructionName, (args?) => ProgramMethodBuilder>;
69-
pdas?: Record<AccountName, (seeds?) => Promise<ProgramDerivedAddress>>;
71+
instructions: Map<string, InstructionNode>;
72+
methods: Record<string, (data?: DataInput) => ProgramMethodBuilder>;
73+
pdas?: Record<string, (seeds?: Record<string, unknown>, options?: PdaOptions) => Promise<ProgramDerivedAddress>>;
7074
programAddress: Address;
71-
instructions: Map<InstructionName, InstructionNode>;
7275
root: RootNode;
7376
};
7477
```
@@ -77,10 +80,9 @@ type ProgramClient = {
7780

7881
```ts
7982
client.methods
80-
.myInstruction(args) // provide instruction arguments
81-
.accounts(accounts) // provide account addresses
83+
.myInstruction(data) // provide the instruction data
84+
.accounts(accounts) // provide account addresses, including remaining accounts
8285
.signers(['accountName']) // optionally mark ambiguous accounts as signers
83-
.resolvers({ customResolver: async (argumentsInput, accountsInput) => {} }) // optionally provide custom resolver according to resolverValueNode in IDL
8486
.instruction(); // Promise<Instruction>
8587
```
8688

@@ -121,19 +123,23 @@ When an account has `isSigner: 'either'` in the IDL, use `.signers()` to explici
121123
.signers(['owner'])
122124
```
123125

124-
### Custom resolvers
126+
### Remaining accounts
125127

126-
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:
128+
Remaining accounts are provided in `.accounts()` as lists of addresses, under the name of their `remainingAccountsNode`:
129+
130+
```ts
131+
.accounts({ delegate, owner: multisig, source, multiSigners: [signer1, signer2] })
132+
```
133+
134+
### Resolved inputs
135+
136+
Accounts and data fields resolved by custom code, i.e. those carrying a `codama.resolver` plugin, have no default value the client can compute. Provide them like any other input (or `null` when they are optional accounts):
127137

128138
```ts
129139
client.methods
130-
.create({ tokenStandard: 'NonFungible' })
131-
.accounts({ owner: ownerAddress })
132-
.resolvers({
133-
resolveIsNonFungible: async (argumentsInput, accountsInput) => {
134-
return argumentsInput.tokenStandard === 'NonFungible';
135-
},
136-
});
140+
.create({ createArgs })
141+
.accounts({ authority, masterEdition: null, mint, payer, splTokenProgram: TOKEN_PROGRAM_ADDRESS })
142+
.instruction();
137143
```
138144

139145
## PDA Derivation
@@ -147,9 +153,15 @@ const [address, bump] = await client.pdas.canonical({
147153
});
148154
```
149155

156+
PDAs are derived from the `programId` of the PDA, or else the program defining it. Instructions may derive a PDA from another program (`pdaValueNode.programId`); pass that program as `programId` to derive it the same way:
157+
158+
```ts
159+
const [ata] = await client.pdas.associatedToken({ mint, owner, tokenProgram }, { programId: ATA_PROGRAM_ADDRESS });
160+
```
161+
150162
### Auto-derived in instructions
151163

152-
Accounts with `pdaValueNode` defaults are resolved automatically. Seeds are pulled from other accounts and arguments in the instruction:
164+
Accounts with `pdaValueNode` defaults are resolved automatically. Seeds are pulled from other accounts and data fields of the instruction:
153165

154166
```ts
155167
// metadata PDA is auto-derived from program + seed
@@ -161,9 +173,9 @@ const ix = await client.methods
161173

162174
Nested/dependent PDAs (where one PDA seed references another PDA) are resolved recursively.
163175

164-
## Arguments
176+
## Data
165177

166-
Arguments with `defaultValueStrategy: 'omitted'` (e.g., discriminators) are auto-encoded and should not be provided.
178+
The instruction data is provided as the value of its `data` type node, e.g. an object keyed by field identifier for a `structTypeNode`. Fields with `defaultValueStrategy: 'omitted'` (e.g. discriminators) are encoded automatically and should not be provided.
167179

168180
## Error Handling
169181

@@ -183,7 +195,7 @@ try {
183195

184196
## CLI
185197

186-
The package includes a CLI for generating TypeScript types from Codama IDL files.
198+
The package includes a CLI for generating TypeScript types from Codama IDL files of the latest major.
187199

188200
```sh
189201
npx @codama/dynamic-client generate-client-types <codama-idl.json> <output-dir>
@@ -195,7 +207,7 @@ Example:
195207
npx @codama/dynamic-client generate-client-types ./idl/codama.json ./generated
196208
```
197209

198-
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.
210+
This reads the IDL file and writes a `*-types.ts` file to the output directory containing strongly-typed interfaces for all instruction data, accounts, PDAs, and the program client.
199211

200212
### `generateClientTypes(idl)`
201213

‎packages/dynamic-client/package.json‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,23 +63,22 @@
6363
},
6464
"dependencies": {
6565
"@codama/dynamic-address-resolution": "workspace:*",
66-
"@codama/dynamic-codecs": "workspace:*",
6766
"@codama/dynamic-instructions": "workspace:*",
6867
"@codama/errors": "workspace:*",
6968
"@solana/addresses": "^8.4.0",
70-
"@solana/codecs": "^8.4.0",
7169
"@solana/instructions": "^8.4.0",
7270
"codama": "workspace:*",
73-
"commander": "^15.0.0",
74-
"superstruct": "^2.0.2"
71+
"commander": "^15.0.0"
7572
},
7673
"devDependencies": {
74+
"@codama/dynamic-codecs": "workspace:*",
7775
"@codama/nodes-from-anchor": "workspace:*",
7876
"@codama/renderers-core": "workspace:*",
7977
"@metaplex-foundation/mpl-token-metadata-kit": "^0.0.3",
8078
"@solana-program/program-metadata": "^0.9.3",
8179
"@solana-program/token": "0.16.1",
8280
"@solana-program/token-2022": "^0.17.0",
81+
"@solana/codecs": "^8.4.0",
8382
"@solana/kit": "8.4.0",
8483
"litesvm": "1.4.1",
8584
"sas-lib": "2.0.0-beta.1"

‎packages/dynamic-client/src/cli/commands/generate-client-types/generate-client-types.ts‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import { generateSignerTypes, getInstructionSignerRef } from '@codama/dynamic-in
77
import { pascalCase, type RootNode } from 'codama';
88

99
/**
10-
* Generate TypeScript type for program client.
10+
* Generate the TypeScript types of a strongly-typed program client for a
11+
* Codama IDL of the latest major. Use `upgrade` from `@codama/upgrade` first
12+
* for IDLs of older majors.
1113
*/
1214
export function generateClientTypes(idl: RootNode): string {
1315
const programName = pascalCase(idl.program.identifier);
@@ -21,7 +23,7 @@ export function generateClientTypes(idl: RootNode): string {
2123
* DO NOT EDIT - Generated by @codama/dynamic-client generate-client-types
2224
*
2325
* Note: When the IDL contains types that cannot be resolved (e.g. unsupported
24-
* kind, missing definition), argument types fall back to \`unknown\` instead of
26+
* kind, missing definition), data types fall back to \`unknown\` instead of
2527
* \`any\`. Consumers should narrow these values before use.
2628
*/
2729
@@ -32,10 +34,9 @@ import type { Instruction } from '@solana/instructions';
3234
/**
3335
* Method builder interface.
3436
*/
35-
export type MethodBuilder<TAccounts, TSigners extends string[], TResolvers = Record<string, never>> = {
36-
accounts(accounts: TAccounts): MethodBuilder<TAccounts, TSigners, TResolvers>;
37-
resolvers(resolvers: Partial<TResolvers>): MethodBuilder<TAccounts, TSigners, TResolvers>;
38-
signers(signers: TSigners): MethodBuilder<TAccounts, TSigners, TResolvers>;
37+
export type MethodBuilder<TAccounts, TSigners extends string[]> = {
38+
accounts(accounts: TAccounts): MethodBuilder<TAccounts, TSigners>;
39+
signers(signers: TSigners): MethodBuilder<TAccounts, TSigners>;
3940
instruction(): Promise<Instruction>;
4041
};
4142
@@ -44,20 +45,19 @@ export type MethodBuilder<TAccounts, TSigners extends string[], TResolvers = Rec
4445
output += generateResolutionInputTypes(idl);
4546
output += generateSignerTypes(idl);
4647

48+
const definedTypes = idl.program.definedTypes ?? [];
4749
for (const ix of idl.program.instructions ?? []) {
4850
const typeName = pascalCase(ix.identifier);
49-
const refs = getResolutionRefs(ix);
51+
const refs = getResolutionRefs(ix, definedTypes);
5052
const signerRef = getInstructionSignerRef(ix);
5153
const signersGeneric = signerRef.signersRef ?? 'string[]';
52-
const resolversGeneric = refs.resolversRef ? `, ${refs.resolversRef}` : '';
5354

54-
let argsParam = '';
55-
if (refs.argsRef) {
56-
const allArgsOptional = !refs.hasRequiredArgs && !refs.hasRequiredRemainingAccounts;
57-
argsParam = allArgsOptional ? `args?: ${refs.argsRef}` : `args: ${refs.argsRef}`;
55+
let dataParam = '';
56+
if (refs.dataRef) {
57+
dataParam = refs.hasRequiredData ? `data: ${refs.dataRef}` : `data?: ${refs.dataRef}`;
5858
}
5959

60-
const methodSignature = `(${argsParam}) => MethodBuilder<${refs.accountsRef}, ${signersGeneric}${resolversGeneric}>`;
60+
const methodSignature = `(${dataParam}) => MethodBuilder<${refs.accountsRef}, ${signersGeneric}>`;
6161
output += `export type ${typeName}Method = ${methodSignature};\n\n`;
6262
}
6363

0 commit comments

Comments
 (0)