Skip to content
Merged
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
178 changes: 72 additions & 106 deletions packages/dynamic-address-resolution/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[npm-image]: https://img.shields.io/npm/v/@codama/dynamic-address-resolution.svg?style=flat&label=%40codama%2Fdynamic-address-resolution
[npm-url]: https://www.npmjs.com/package/@codama/dynamic-address-resolution

This package provides the address resolution functionality for instruction accounts of a Codama IDL. It powers [`@codama/dynamic-client`](../dynamic-client/README.md).
This package resolves the addresses of instruction accounts from a Codama IDL, e.g. by deriving PDAs from their seeds. It powers [`@codama/dynamic-client`](../dynamic-client/README.md).

## Installation

Expand All @@ -18,154 +18,120 @@ pnpm install @codama/dynamic-address-resolution
> [!NOTE]
> This package is **not** included in the main [`codama`](../library) package.

## Types
## Usage

- `AccountsInput`, `ArgumentsInput` — user-input shapes for accounts and arguments.
- `ResolverFn`, `ResolversInput`, `ResolverFnInput` — user-supplied custom resolver functions.
- `AddressInput`, `PublicKeyLike` — accepted address-like inputs (modern `Address` strings, base58 strings, or legacy `PublicKey`-like objects with `.toBase58()`).
Give `resolveInstructionAccountAddress` the path of an instruction account, from the root node, along with the accounts and data provided for the instruction.

## Types generation

This package can emit TypeScript the input types required for address resolution of each instruction — `${Name}Args`, `${Name}Accounts`, `${Name}Resolvers`.

### CLI
```ts
import { resolveInstructionAccountAddress } from '@codama/dynamic-address-resolution';

```sh
npx @codama/dynamic-address-resolution generate-types <path/to/idl.json> <output-dir>
const address = await resolveInstructionAccountAddress({
accountsInput: { authority },
dataInput: { amount: 1_000_000_000n },
path: [root, root.program, instruction, vaultAccount],
});
```

Writes `<idl-name>-address-resolution-types.ts` to the output directory.
The path is needed to follow links, e.g. to PDAs of other programs, and to resolve injected values from the `provides` of the instruction and its parents.

### Programmatic
The `dataInput` uses the value format of [`@codama/dynamic-codecs`](../dynamic-codecs/README.md), e.g. `bigint` integers and `{ __kind, data }` enums.

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

const source = generateTypes(idl);
```
A provided address is always used. Otherwise, the account resolves from its `defaultValue`, or from the `optionalAccountStrategy` of its instruction when it is optional and provided as `null`.

## Functions
| Account | `undefined` | `null` |
| ------------------------------------------ | ------------------------- | ------------------------- |
| Required, without `defaultValue` | Throws | Throws |
| Required, with `defaultValue` | Resolves from its default | Resolves from its default |
| Optional (`isOptional: true`), without one | Throws | Optional account strategy |
| Optional, with `defaultValue` | Resolves from its default | Optional account strategy |

### `resolveInstructionAccountAddress(input)`
Default values resolve as follows.

Resolves the on-chain `Address` for a single `InstructionAccountNode` of an instruction, applying default values, PDA derivation, conditional resolution, and any user-supplied custom resolvers. Returns `null` for optional accounts that resolve to "omitted".
| Default value | Resolves to |
| ---------------------- | ------------------------------------------------------------------------------------------------------------ |
| `publicKeyValueNode` | The given address. |
| `programIdValueNode` | The address of the program of the instruction. |
| `programLinkNode` | The address of the linked program. |
| `accountValueNode` | The address of another account of the instruction, resolved recursively. |
| `dataValueNode` | The address at the given path of the data, e.g. `config.owner`, using default values of fields when missing. |
| `pdaValueNode` | The derived PDA. See [PDAs](#pdas). |
| `conditionalValueNode` | One of its branches. See [Conditions](#conditions). |
| `injectedValueNode` | Its provided value, resolved like any other default value. |
| `payerValueNode` | The provided address, which is required. |
| `identityValueNode` | The provided address, which is required. |

**Untyped:**
`accountBumpValueNode` and `accountDataValueNode` defaults are not supported, since they require fetching accounts.

```ts
const address = await resolveInstructionAccountAddress({
accountsInput: { authority: ownerAddress },
argumentsInput: { amount: 1_000_000_000n },
ixAccountNode,
ixNode,
root,
});
```
### PDAs

**Typed:**
Each seed is encoded using its declared type: constant seeds use their value, and variable seeds use the value provided by the `pdaValueNode`, from an account, the instruction data or a value node. Missing `remainderOptionTypeNode` seeds encode to zero bytes.

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

const address = await resolveInstructionAccountAddress<TransferSolAccounts, TransferSolArgs>({
accountsInput: { source, destination },
argumentsInput: { amount: 1_000_000_000n },
ixAccountNode,
ixNode,
root,
// seeds: [constant('vault'), variable('owner', publicKey), variable('name', string)]
pdaValueNode(pdaLinkNode('vault'), {
seeds: [
pdaSeedValueNode('owner', accountValueNode('owner')),
pdaSeedValueNode('name', dataValueNode('config.name')),
],
});
```

#### Automatic resolution rules

Accounts (PDAs, program ids, constants) with a `defaultValue` are resolved automatically and may be omitted from `accountsInput`.
The PDA is derived using the `programId` of the `pdaValueNode` if any, then the `programId` of the PDA, then the address of the program defining it.

| Account scenario | Type in `accountsInput` | 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` |
### Conditions

Auto-resolved kinds include:
With a `value`, a `conditionalValueNode` takes its `ifTrue` branch when its condition equals that value. Integers compare by value whether they are numbers or bigints, and enum variants compare by identifier, e.g. `'slow'` equals `enumValueNode('mode', 'slow')`. Without a `value`, it takes its `ifTrue` branch when the referenced account or data exists.

- **PDA accounts** — derived from seeds defined in the IDL (`pdaValueNode`).
- **Program IDs** — known program addresses (e.g. System Program, Token Program).
- **Constants** — `constantValueNode` defaults.
- **Conditional values** — `conditionalValueNode` defaults.
- **Custom resolvers** — `resolverValueNode` defaults supplied via `resolversInput`.
When no branch matches, optional accounts resolve using the optional account strategy and required accounts throw.

### `resolveStandalonePda(root, pdaNode, seedInputs?)`
## `resolveStandalonePda(input)`

Derives a `ProgramDerivedAddress` for a `PdaNode` outside any instruction context — seeds are supplied directly as a `Record<string, unknown>`.
Derives a PDA outside of any instruction, from its path and the values of its variable seeds.

```ts
const pdaNode = root.program.pdas.find(p => p.name === 'metadata')!;
const [pda, bump] = await resolveStandalonePda(root, pdaNode, {
authority: ownerAddress,
seed: 'idl',
const [address, bump] = await resolveStandalonePda({
path: [root, root.program, metadataPda],
seedsInput: { authority, seed: 'idl' },
});
```

### `createCodecInputTransformer(typeNode, root, options?)`
## Types generation

Returns a function that transforms user-supplied input into the codec-compatible shape expected by [`@codama/dynamic-codecs`](../dynamic-codecs/README.md) — e.g. `Uint8Array` → `[encoding, hex]`.
This package can generate the TypeScript input types of each instruction: `${Name}InstructionDataArgs` for its data, `${Name}Accounts` for its accounts, including remaining accounts as named `Address[]` lists, and `${Pda}Seeds` for the variable seeds of each PDA.

```ts
import { bytesTypeNode } from 'codama';

const transform = createCodecInputTransformer(bytesTypeNode(), root, {
bytesEncoding: 'base16',
});
### CLI

transform(new Uint8Array([72, 101, 108, 108, 111]));
// => ['base16', '48656c6c6f']
```sh
npx @codama/dynamic-address-resolution generate-types <path/to/idl.json> <output-dir>
```

### `createDefaultValueEncoderVisitor(codec)`
Writes `<idl-name>-address-resolution-types.ts` to the output directory.

Returns a `Visitor<ReadonlyUint8Array>` that encodes default `ValueNode`s.
### Programmatic

```ts
import { getNodeCodec } from '@codama/dynamic-codecs';
import { bytesValueNode, visit } from 'codama';

const codec = getNodeCodec([root, root.program, argNode]);
const encoder = createDefaultValueEncoderVisitor(codec);
const bytes = visit(bytesValueNode('base16', 'a1b2c3'), encoder);
```

### Helpers

#### `toAddress(input)`

Normalizes any `AddressInput` (modern `Address` string, base58 string, or legacy `PublicKey`-like object with `.toBase58()`) into an `Address`.
import { generateTypes } from '@codama/dynamic-address-resolution/codegen';

```ts
const a1 = toAddress('11111111111111111111111111111111');
const a2 = toAddress(new PublicKey());
const source = generateTypes(idl);
```

#### `isPublicKeyLike(value)`

Duck-typed guard for legacy `PublicKey` objects.
The generated types can narrow the inputs of `resolveInstructionAccountAddress`.

```ts
if (isPublicKeyLike(value)) {
const addr = toAddress(value);
}
```

#### `isAddressConvertible(value)`

Returns `true` if `value` is a string `Address` or a `PublicKeyLike` object — i.e. safe to pass to `toAddress`.
import type { TransferAccounts, TransferInstructionDataArgs } from './generated/my-program-address-resolution-types';

```ts
if (isAddressConvertible(input)) {
return toAddress(input);
}
const address = await resolveInstructionAccountAddress<TransferAccounts, TransferInstructionDataArgs>({
accountsInput: { destination, source },
dataInput: { amount: 1_000_000_000n },
path,
});
```

#### Constants
## Helpers

- `OPTIONAL_NODE_KINDS` — type-node kinds treated as optional.
- `toAddress(input)` normalises any `AddressInput`, i.e. an `Address`, a base58 string, or a legacy `PublicKey`-like object with `.toBase58()`, into an `Address`.
- `isPublicKeyLike(value)` is a duck-typed guard for legacy `PublicKey` objects.
- `isAddressConvertible(value)` returns `true` when `value` can be passed to `toAddress`.
- `OPTIONAL_NODE_KINDS` lists the type node kinds treated as optional.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ 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',
'Generate TypeScript address-resolution types (PDA seeds, instruction data and accounts) 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')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,71 +1,62 @@
import type { DefinedTypeNode, TypeNode } from 'codama';

import { OPTIONAL_NODE_KINDS } from '../shared/nodes';

/**
* Convert Codama type to TypeScript type string.
* Convert a Codama type to the TypeScript type of the values its codec
* encodes, e.g. `integerTypeNode('u64')` gives `number | bigint`.
*/
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 'integerTypeNode':
case 'fixedPointTypeNode':
case 'dateTimeTypeNode':
case 'durationTypeNode':
return 'number | bigint';
case 'floatTypeNode':
return '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.identifier}: ${codamaTypeToTS(f.type, definedTypes)}`);
if (fields.length === 0) return '{}';
return `{ ${fields.join('; ')} }`;
// Omitted fields always encode their default value, and fields with a default value may be omitted.
const fields = (type.fields ?? [])
.filter(field => field.defaultValueStrategy !== 'omitted' || field.defaultValue === undefined)
.map(field => {
const isOptional =
field.defaultValue !== undefined || OPTIONAL_NODE_KINDS.includes(field.type.kind);
return `${field.identifier}${isOptional ? '?' : ''}: ${codamaTypeToTS(field.type, definedTypes)}`;
});
return fields.length === 0 ? '{}' : `{ ${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.identifier}'`).join(' | ');
const variants = type.variants ?? [];
if (variants.length === 0) return 'unknown /** empty variants in enumTypeNode */';
// Variants without data may be encoded from their identifier.
if (variants.every(variant => variant.data === undefined)) {
return variants.map(variant => `'${variant.identifier}'`).join(' | ');
}
const variantTypes = type.variants.map(v => {
if (v.kind === 'enumEmptyVariantTypeNode') {
return `{ __kind: '${v.identifier}' }`;
}
if (v.kind === 'enumStructVariantTypeNode' && v.struct) {
const inner = codamaTypeToTS(v.struct, definedTypes);
return `{ __kind: '${v.identifier}' } & ${inner}`;
}
if (v.kind === 'enumTupleVariantTypeNode' && v.tuple) {
const inner = codamaTypeToTS(v.tuple, definedTypes);
return `{ __kind: '${v.identifier}'; fields: ${inner} }`;
}
return `{ __kind: '${v.identifier}' }`;
});
return variantTypes.join(' | ');
return variants
.map(variant =>
variant.data === undefined
? `{ __kind: '${variant.identifier}' }`
: `{ __kind: '${variant.identifier}'; data: ${codamaTypeToTS(variant.data, definedTypes)} }`,
)
.join(' | ');
}
case 'tupleTypeNode': {
if (!type.items || type.items.length === 0) return '[]';
const items = type.items.map(i => codamaTypeToTS(i, definedTypes));
const items = (type.items ?? []).map(item => codamaTypeToTS(item, definedTypes));
return `[${items.join(', ')}]`;
}
case 'arrayTypeNode':
Expand All @@ -74,18 +65,12 @@ export function codamaTypeToTS(type: TypeNode | undefined, definedTypes: Defined
const needsParens = itemType.includes(' | ') || itemType.includes(' & ');
return needsParens ? `(${itemType})[]` : `${itemType}[]`;
}
case 'mapTypeNode': {
const v = codamaTypeToTS(type.value, definedTypes);
return `Record<string, ${v}>`;
}
case 'mapTypeNode':
return `Record<string, ${codamaTypeToTS(type.value, definedTypes)}>`;
case 'definedTypeLinkNode': {
if (!type.identifier) return 'unknown /** name missing in definedTypeLinkNode */';
const def = definedTypes.find(d => d.identifier === type.identifier);
if (!def) return 'unknown /** DefinedTypeNode not found for definedTypeLinkNode */';
return codamaTypeToTS(def.type, definedTypes);
}
case 'dateTimeTypeNode': {
return codamaTypeToTS(type.number, definedTypes);
const definedType = definedTypes.find(definedType => definedType.identifier === type.identifier);
if (!definedType) return 'unknown /** DefinedTypeNode not found for definedTypeLinkNode */';
return codamaTypeToTS(definedType.type, definedTypes);
}
default:
type['kind'] satisfies never;
Expand Down
Loading
Loading