Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/loud-taxis-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@codama/dynamic-address-resolution': minor
'@codama/dynamic-instructions': minor
'@codama/dynamic-client': patch
---

Split types generation in `@codama/dynamic-address-resolution`, `@codama/dynamic-instructions` and `@codama/dynamic-client`.
20 changes: 18 additions & 2 deletions packages/dynamic-address-resolution/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,23 @@ pnpm install @codama/dynamic-address-resolution

## Types generation

> For now, types for arguments, accounts, custom resolvers, and PDA seeds can be produced via [`@codama/dynamic-client`](../dynamic-client/README.md)'s `generate-client-types` command, and passed as generics to the functions. Types generation for this package will be added in a follow-up release.
This package can emit TypeScript the input types required for address resolution of each instruction — `${Name}Args`, `${Name}Accounts`, `${Name}Resolvers`.

### CLI

```sh
npx @codama/dynamic-address-resolution generate-types <path/to/idl.json> <output-dir>
```

Writes `<idl-name>-address-resolution-types.ts` to the output directory.

### Programmatic

```ts
import { generateTypes } from '@codama/dynamic-address-resolution/codegen';

const source = generateTypes(idl);
```

## Functions

Expand All @@ -49,7 +65,7 @@ const address = await resolveInstructionAccountAddress({
**Typed:**

```ts
import type { TransferSolAccounts, TransferSolArgs } from './generated/system-program-idl-types';
import type { TransferSolAccounts, TransferSolArgs } from './generated/system-program-idl-address-resolution-types';

const address = await resolveInstructionAccountAddress<TransferSolAccounts, TransferSolArgs>({
accountsInput: { source, destination },
Expand Down
5 changes: 5 additions & 0 deletions packages/dynamic-address-resolution/bin/cli.cjs
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);
41 changes: 31 additions & 10 deletions packages/dynamic-address-resolution/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,26 @@
"name": "@codama/dynamic-address-resolution",
"version": "0.2.0",
"description": "Address resolution functionality for instruction accounts in Codama IDLs",
"bin": {
"dynamic-address-resolution": "./bin/cli.cjs"
},
"exports": {
"types": "./dist/types/index.d.ts",
"react-native": "./dist/index.react-native.mjs",
"browser": {
"import": "./dist/index.browser.mjs",
"require": "./dist/index.browser.cjs"
".": {
"types": "./dist/types/index.d.ts",
"react-native": "./dist/index.react-native.mjs",
"browser": {
"import": "./dist/index.browser.mjs",
"require": "./dist/index.browser.cjs"
},
"node": {
"import": "./dist/index.node.mjs",
"require": "./dist/index.node.cjs"
}
},
"node": {
"import": "./dist/index.node.mjs",
"require": "./dist/index.node.cjs"
"./codegen": {
"types": "./dist/types/codegen/index.d.ts",
"import": "./dist/codegen.node.mjs",
"require": "./dist/codegen.node.cjs"
}
},
"browser": {
Expand All @@ -22,10 +32,20 @@
"module": "./dist/index.node.mjs",
"react-native": "./dist/index.react-native.mjs",
"types": "./dist/types/index.d.ts",
"typesVersions": {
"*": {
"codegen": [
"./dist/types/codegen/index.d.ts"
]
}
},
"type": "commonjs",
"files": [
"./dist/types",
"./dist/index.*"
"./dist/index.*",
"./dist/cli.*",
"./dist/codegen.*",
"./bin/*"
],
"sideEffects": false,
"keywords": [
Expand All @@ -50,7 +70,8 @@
"@codama/errors": "workspace:*",
"@solana/addresses": "^5.3.0",
"@solana/codecs": "^5.3.0",
"codama": "workspace:*"
"codama": "workspace:*",
"commander": "^14.0.2"
},
"devDependencies": {
"@solana/kit": "6.5.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { generateTypes } from '../../../codegen/generate-types';
import { generateTypesFromFile as generateTypesFromFileShared } from '../../../codegen/generate-types-from-file';

export function generateTypesFromFile(codamaIdlPath: string, outputDirPath: string): void {
generateTypesFromFileShared({
codamaIdlPath,
generate: generateTypes,
outputDirPath,
outputFileSuffix: 'address-resolution-types',
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Command } from 'commander';

import { generateTypesFromFile } from './generate-types-from-file';

export function registerGenerateTypesCommand(program: Command): void {
program
.command('generate-types')
.description(
'Generate TypeScript address-resolution types (PDA seeds, instruction Args/Accounts/Resolvers) from a Codama IDL JSON file',
)
.argument('<codama-idl>', 'Path to a Codama IDL JSON file (e.g., ./idl/codama.json)')
.argument('<output-dir>', 'Path to the output directory for the generated .ts file, e.g., ./generated')
.action((idlArg: string, outputDirArg: string) => {
try {
generateTypesFromFile(idlArg, outputDirArg);
} catch (err) {
console.error(err);
process.exit(1);
}
});
}
7 changes: 7 additions & 0 deletions packages/dynamic-address-resolution/src/cli/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Command } from 'commander';

import { registerGenerateTypesCommand } from './generate-types/register-command';

export function registerCommands(program: Command): void {
registerGenerateTypesCommand(program);
}
12 changes: 12 additions & 0 deletions packages/dynamic-address-resolution/src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createProgram } from './program';

const program = createProgram();

export function run(argv: string[]): void {
if (argv.length <= 2) {
program.outputHelp();
return;
}

program.parse(argv);
}
15 changes: 15 additions & 0 deletions packages/dynamic-address-resolution/src/cli/program.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Command } from 'commander';

import { registerCommands } from './commands';

export function createProgram(): Command {
const program = new Command();

program
.name('dynamic-address-resolution')
.description('CLI for @codama/dynamic-address-resolution')
.showHelpAfterError(true);

registerCommands(program);
return program;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { DefinedTypeNode, TypeNode } from 'codama';

/**
* Convert Codama type to TypeScript type string.
*/
export function codamaTypeToTS(type: TypeNode | undefined, definedTypes: DefinedTypeNode[]): string {
if (!type || typeof type !== 'object') return 'unknown';

switch (type.kind) {
case 'numberTypeNode':
return ['u64', 'u128', 'i64', 'i128'].includes(type.format) ? 'number | bigint' : 'number';
case 'publicKeyTypeNode':
return 'Address';
case 'stringTypeNode':
return 'string';
case 'booleanTypeNode':
return 'boolean';
case 'optionTypeNode':
return `${codamaTypeToTS(type.item, definedTypes)} | null`;
case 'remainderOptionTypeNode':
case 'zeroableOptionTypeNode':
return `${codamaTypeToTS(type.item, definedTypes)} | null`;
case 'bytesTypeNode':
return 'Uint8Array';
case 'fixedSizeTypeNode':
case 'sizePrefixTypeNode':
case 'hiddenPrefixTypeNode':
case 'preOffsetTypeNode':
case 'postOffsetTypeNode':
case 'hiddenSuffixTypeNode':
case 'sentinelTypeNode':
return codamaTypeToTS(type.type, definedTypes);
case 'amountTypeNode':
case 'solAmountTypeNode':
return 'number | bigint';
case 'structTypeNode': {
if (!type.fields || type.fields.length === 0) return '{}';
const fields = type.fields
.filter(f => f.defaultValueStrategy !== 'omitted')
.map(f => `${f.name}: ${codamaTypeToTS(f.type, definedTypes)}`);
if (fields.length === 0) return '{}';
return `{ ${fields.join('; ')} }`;
}
case 'enumTypeNode': {
if (!type.variants || type.variants.length === 0) return 'unknown /** empty variants in enumTypeNode */';
const allEmpty = type.variants.every(v => v.kind === 'enumEmptyVariantTypeNode');
if (allEmpty) {
return type.variants.map(v => `'${v.name}'`).join(' | ');
}
const variantTypes = type.variants.map(v => {
if (v.kind === 'enumEmptyVariantTypeNode') {
return `{ __kind: '${v.name}' }`;
}
if (v.kind === 'enumStructVariantTypeNode' && v.struct) {
const inner = codamaTypeToTS(v.struct, definedTypes);
return `{ __kind: '${v.name}' } & ${inner}`;
}
if (v.kind === 'enumTupleVariantTypeNode' && v.tuple) {
const inner = codamaTypeToTS(v.tuple, definedTypes);
return `{ __kind: '${v.name}'; fields: ${inner} }`;
}
return `{ __kind: '${v.name}' }`;
});
return variantTypes.join(' | ');
}
case 'tupleTypeNode': {
if (!type.items || type.items.length === 0) return '[]';
const items = type.items.map(i => codamaTypeToTS(i, definedTypes));
return `[${items.join(', ')}]`;
}
case 'arrayTypeNode':
case 'setTypeNode': {
const itemType = codamaTypeToTS(type.item, definedTypes);
const needsParens = itemType.includes(' | ') || itemType.includes(' & ');
return needsParens ? `(${itemType})[]` : `${itemType}[]`;
}
case 'mapTypeNode': {
const v = codamaTypeToTS(type.value, definedTypes);
return `Record<string, ${v}>`;
}
case 'definedTypeLinkNode': {
if (!type.name) return 'unknown /** name missing in definedTypeLinkNode */';
const def = definedTypes.find(d => d.name === type.name);
if (!def) return 'unknown /** DefinedTypeNode not found for definedTypeLinkNode */';
return codamaTypeToTS(def.type, definedTypes);
}
case 'dateTimeTypeNode': {
return codamaTypeToTS(type.number, definedTypes);
}
default:
type['kind'] satisfies never;
return 'unknown';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { PdaNode, RootNode } from 'codama';

/**
* Collects all PdaNodes referenced in an IDL.
*/
export function collectPdaNodesFromIdl(idl: RootNode): Map<string, PdaNode> {
const pdas = new Map<string, PdaNode>();

for (const pda of idl.program.pdas ?? []) {
pdas.set(pda.name, pda);
}

for (const ix of idl.program.instructions) {
for (const acc of ix.accounts) {
if (!acc.defaultValue || acc.defaultValue.kind !== 'pdaValueNode') continue;
const pdaDef = acc.defaultValue.pda;
if (!pdaDef || pdaDef.kind !== 'pdaNode') continue;
if (!pdas.has(pdaDef.name)) {
pdas.set(pdaDef.name, pdaDef);
}
}
}

return pdas;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { InstructionInputValueNode, InstructionNode } from 'codama';

/**
* Collects all unique resolverValueNode names from an instruction's accounts and arguments.
*/
export function collectResolverNames(ix: InstructionNode): Set<string> {
const names = new Set<string>();

for (const acc of ix.accounts) {
extractResolverNodeName(acc.defaultValue, names);
}
for (const arg of ix.arguments) {
extractResolverNodeName(arg.defaultValue, names);
}

return names;
}

function extractResolverNodeName(node: InstructionInputValueNode | undefined, names: Set<string>): void {
if (!node) return;
if (node.kind === 'resolverValueNode' && node.name) {
names.add(node.name);
} else if (node.kind === 'conditionalValueNode') {
extractResolverNodeName(node.condition, names);
extractResolverNodeName(node.ifTrue, names);
extractResolverNodeName(node.ifFalse, names);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { pascalCase, type PdaNode, type RootNode, type VariablePdaSeedNode } from 'codama';

import { codamaTypeToTS } from './codama-type-to-ts';
import { collectPdaNodesFromIdl } from './collect-pda-nodes';

/**
* Generate `${Pda}PdaSeeds` types and the aggregate `${Program}Pdas` map type.
*
* Returns the type block with the aggregate map type name. `mapTypeName` is `null` when the program has no PDAs.
*/
export function generatePdaTypes(idl: RootNode): { mapTypeName: string | null; typeBlock: string } {
const programName = pascalCase(idl.program.name);
const definedTypes = idl.program.definedTypes ?? [];
const pdaMap = collectPdaNodesFromIdl(idl);

if (pdaMap.size === 0) {
return { mapTypeName: null, typeBlock: '' };
}

let output = '';

for (const [pdaName, pdaNode] of pdaMap) {
const variableSeeds = getVariableSeedNodes(pdaNode);
if (variableSeeds.length === 0) continue;
const typeName = pascalCase(pdaName);
output += `export type ${typeName}PdaSeeds = {\n`;
for (const seed of variableSeeds) {
const tsType = seed.type
? codamaTypeToTS(seed.type, definedTypes)
: 'unknown/** missing type in variablePdaSeedNode */';
output += ` ${seed.name}: ${tsType};\n`;
}
output += '};\n\n';
}

const mapTypeName = `${programName}Pdas`;
output += `/**\n * Strongly-typed PDAs for ${programName}.\n */\n`;
output += `export type ${mapTypeName} = {\n`;
for (const [pdaName, pdaNode] of pdaMap) {
const typeName = pascalCase(pdaName);
const seedsParam =
getVariableSeedNodes(pdaNode).length > 0 ? `seeds: ${typeName}PdaSeeds` : `seeds?: Record<string, unknown>`;
output += ` ${pdaName}: (${seedsParam}) => Promise<ProgramDerivedAddress>;\n`;
}
output += '};\n\n';

return { mapTypeName, typeBlock: output };
}

function getVariableSeedNodes(pdaNode: PdaNode): VariablePdaSeedNode[] {
return (pdaNode.seeds ?? []).filter(s => s.kind === 'variablePdaSeedNode');
}
Loading
Loading